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 }