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