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 }