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