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