1 #!/usr/bin/env dub 2 /+ dub.sdl: 3 dependency "dsh" version="~>1.6.1" 4 dependency "fswatch" version="~>0.6.0" 5 dependency "requests" version="~>2.0.6" 6 +/ 7 8 /** 9 * A helper program that can be used to make, build, and manage dsh scripts. 10 */ 11 module tools.dshutil; 12 13 import dsh; 14 15 const DSH_VERSION = "1.6.1"; 16 17 int main(string[] args) { 18 import std.string; 19 if (args.length < 2) { 20 stderr.writeln("Missing required command argument."); 21 printHelp(); 22 return 1; 23 } 24 string command = args[1].strip; 25 if (command == "--version" || command == "-v") { 26 writeln(DSH_VERSION); 27 return 0; 28 } 29 if (command == "--help" || command == "-h") printHelp(); 30 if (command == "create") return createScript(args[2..$]); 31 if (command == "build") return buildScript(args[2..$]); 32 if (command == "compile") return compileScript(args[2..$]); 33 version(linux) { 34 if (command == "install") return install(); 35 if (command == "uninstall") return uninstall(); 36 } 37 stderr.writefln!"Unsupported command: %s"(command); 38 return 1; 39 } 40 41 void printHelp() { 42 writeln( 43 "dshutil is a command-line utility that helps you create DSH scripts.\n" ~ 44 "The following subcommands are available:\n" ~ 45 " create [name] [--single] Creates a new DSH script (optionally with the given filename).\n" ~ 46 " If --single, dshs.d will be downloaded to the script's directory.\n" ~ 47 " build <name> Starts watching the given script and compiles it automatically.\n" ~ 48 " compile <name> Compiles the given script to a native executable.\n" ~ 49 " install (Linux only) Installs a native version of dshutil to /usr/local/bin, and\n" ~ 50 " installs dshs.d to /usr/include.\n" ~ 51 " uninstall (Linux only) Removes dshutil from /usr/local/bin removes dshs.d from /usr/include.\n" ~ 52 " --help | -h Show this help message.\n" ~ 53 " --version | -v Show the version of DSH that is being used.\n" 54 ); 55 } 56 57 /** 58 * Creates a new, empty script. 59 * Params: 60 * args = The arguments to the command. Accepts one optional argument to 61 * specify a name for the script file. 62 * Returns: 0 if successful, or 1 otherwise. 63 */ 64 int createScript(string[] args) { 65 import std.conv : to; 66 import std.string : strip; 67 string filePath = "script.d"; 68 int tryCount = 1; 69 while (filePath.exists) { 70 filePath = "script" ~ tryCount.to!string ~ ".d"; 71 } 72 bool useDub = true; 73 if (args.length > 0) { 74 string arg1 = args[0].strip; 75 if (arg1 == "--single") { 76 useDub = false; 77 } else { 78 filePath = arg1; 79 if (filePath.exists) { 80 stderr.writefln!"Cannot create script because %s already exists."(filePath); 81 return 1; 82 } 83 if (args.length > 1 && args[1].strip == "--single") { 84 useDub = false; 85 } 86 } 87 } 88 auto f = new File(filePath, "w"); 89 if (useDub) { 90 // Only include the shebang on Linux. 91 version (linux) { 92 f.writeln("#!/usr/bin/env dub"); 93 } 94 f.writeln("/+ dub.sdl:"); 95 f.writeln(" dependency \"dsh\" version=\"~>" ~ DSH_VERSION ~ "\""); 96 f.writeln("+/"); 97 } 98 f.writeln("import dsh;"); 99 f.writeln(); 100 f.writeln("void main() {"); 101 f.writeln(" print(\"Edit this to start writing your script.\");"); 102 f.writeln("}"); 103 f.writeln(); 104 f.close(); 105 if (useDub) { 106 // If on linux, set the file to be executable. 107 version (linux) { 108 run("chmod +x " ~ filePath); 109 } 110 } else { 111 downloadDshs("."); 112 writeln("Downloaded dshs.d for DSH single-file mode. Include this file when compiling your script."); 113 } 114 writeln("Created script."); 115 return 0; 116 } 117 118 /** 119 * Watches a file to build it using DUB single-file mode, any time a change 120 * is noticed. 121 * Params: 122 * args = The program arguments. Accepts a single required argument being 123 * the file to build/watch. 124 * Returns: 0 if successful, or 1 otherwise. 125 */ 126 int buildScript(string[] args) { 127 import std.string; 128 if (args.length < 1) { 129 stderr.writeln("Missing required file argument."); 130 return 1; 131 } 132 string filePath = args[0].strip; 133 if (!exists(filePath) || !isFile(filePath)) { 134 stderr.writefln!"%s is not a file."(filePath); 135 return 1; 136 } 137 import fswatch; 138 import core.thread; 139 auto watcher = FileWatch(filePath, false); 140 writefln!"Watching %s to build when file changes."(filePath); 141 ProcessBuilder pb = new ProcessBuilder(); 142 pb.run("dub build --single " ~ filePath); 143 while (true) { 144 foreach (event; watcher.getEvents()) { 145 if (event.type == FileChangeEventType.modify) handleFileUpdate(filePath, pb); 146 } 147 Thread.sleep(seconds(1)); 148 } 149 } 150 151 private void handleFileUpdate(string filePath, ProcessBuilder pb) { 152 import std.algorithm; 153 writeln("File changed. Rebuilding..."); 154 if (pb.run("dub build --single " ~ filePath) == 0) { 155 auto f = File(filePath, "r"); 156 foreach (string line; lines(f)) { 157 if (startsWith(line, "// DSHTEST:")) runScriptTest(filePath, line); 158 } 159 f.close(); 160 } 161 } 162 163 private void runScriptTest(string filePath, string line) { 164 import std.string; 165 import std.algorithm; 166 if (line.length < 12) return; 167 string args = line[11 .. $].strip; 168 if (args.length == 0) return; 169 string scriptName = filePath; 170 if (endsWith(filePath, ".d")) { 171 scriptName = filePath[0..$-2]; 172 } 173 version (linux) { 174 scriptName = "./" ~ scriptName; 175 } 176 version (Windows) { 177 scriptName = scriptName ~ ".exe"; 178 } 179 string command = scriptName ~ " " ~ args; 180 writefln!"Running \"%s\""(command); 181 int result = run(command); 182 writefln!"Script exited %d"(result); 183 } 184 185 int compileScript(string[] args) { 186 import std.string; 187 if (args.length < 1) { 188 stderr.writeln("Missing required file argument."); 189 return 1; 190 } 191 string filePath = args[0].strip; 192 if (!exists(filePath) || !isFile(filePath)) { 193 stderr.writefln!"%s is not a file."(filePath); 194 return 1; 195 } 196 writeln("Compiling " ~ filePath); 197 int r = run("dub build --single --build=release " ~ filePath); 198 if (r != 0) { 199 stderr.writefln!"Could not compile: %d"(r); 200 return 1; 201 } 202 return 0; 203 } 204 205 private void downloadDshs(string path) { 206 import requests; 207 auto rq = Request(); 208 rq.useStreaming = true; 209 auto rs = rq.get("https://raw.githubusercontent.com/andrewlalis/dsh/v"~DSH_VERSION~"/tools/dshs.d"); 210 auto stream = rs.receiveAsRange(); 211 import std.path; 212 File dshFile = File(buildPath(path, "dshs.d"), "wb"); 213 while (!stream.empty) { 214 dshFile.rawWrite(stream.front); 215 stream.popFront; 216 } 217 dshFile.close(); 218 } 219 220 version(linux) { 221 int install() { 222 runOrQuit("dub build --single --build=release dshutil.d"); 223 writeln("Copying dshutil to /usr/local/bin/dshutil"); 224 runOrQuit("sudo mv dshutil /usr/local/bin/dshutil"); 225 writeln("Installed dshutil to /usr/local/bin"); 226 writeln("Downloading dshs.d to /usr/include/dshs.d"); 227 runOrQuit("sudo wget https://raw.githubusercontent.com/andrewlalis/dsh/v"~DSH_VERSION~"/tools/dshs.d -O /usr/include/dshs.d"); 228 return 0; 229 } 230 231 int uninstall() { 232 writeln("Removing dshutil from /usr/local/bin"); 233 runOrQuit("sudo rm -f /usr/local/bin/dshutil"); 234 writeln("Uninstalled dshutil from /usr/local/bin"); 235 if (exists("/usr/include/dshs.d")) { 236 runOrQuit("sudo rm -f /usr/include/dshs.d"); 237 writeln("Removed /usr/include/dshs.d"); 238 } 239 return 0; 240 } 241 }