1 /** 
2  * DSH provides conveniences that make it easier to write simple scripts in D.
3  * 
4  * Several phobos modules are preemptively imported so that you don't need to
5  * do so in your script. Additionally, helper functions are defined which are
6  * especially useful for scripts.
7  */
8 module dsh;
9 
10 // Public imports from the standard library.
11 
12 public import std.stdio;
13 public import std.file;
14 
15 public void print(string, Args...)(string s, Args args) {
16     writefln(s, args);
17 }
18 
19 public void error(string, Args...)(string s, Args args) {
20     stderr.writefln(s, args);
21 }
22 
23 /** 
24  * Checks if a directory is empty.
25  * Params:
26  *   dir = The directory to check.
27  * Returns: True if the given directory exists, is a directory, and is empty.
28  */
29 public bool isDirEmpty(string dir) {
30     import std.range : empty;
31     import std.array : array;
32     if (!exists(dir) || !isDir(dir)) return false;
33     return empty(dirEntries(dir, SpanMode.shallow).array);
34 }
35 
36 unittest {
37     assert(!isDirEmpty("source"));
38     assert(!isDirEmpty("source/dsh.d"));
39     assert(!isDirEmpty("source/some-non-existant-file.blah"));
40     auto d = ".test_empty_dir";
41     d.mkdir;
42     scope(exit) d.rmdir;
43     assert(isDirEmpty(d));
44 }
45 
46 /** 
47  * Helper for more easily creating processes.
48  */
49 public class ProcessBuilder {
50     private File stdin;
51     private File stdout;
52     private File stderr;
53     private string[string] env;
54     private string dir;
55 
56     public this() {
57         this.stdin = std.stdio.stdin;
58         this.stdout = std.stdio.stdout;
59         this.stderr = std.stdio.stderr;
60         this.dir = getcwd();
61     }
62 
63     public ProcessBuilder inputFrom(string filename) {
64         this.stdin = File(filename, "r");
65         return this;
66     }
67 
68     public ProcessBuilder outputTo(string filename) {
69         this.stdout = File(filename, "w");
70         return this;
71     }
72 
73     public ProcessBuilder errorTo(string filename) {
74         this.stderr = File(filename, "w");
75         return this;
76     }
77 
78     public ProcessBuilder withEnv(string key, string value) {
79         this.env[key] = value;
80         return this;
81     }
82 
83     public ProcessBuilder workingDir(string dir) {
84         this.dir = dir;
85         return this;
86     }
87 
88     public int run(string command) {
89         import std.process;
90         import std.regex;
91         try {
92             auto r = regex("\\s+");
93             auto s = split(command, r);
94             auto pid = spawnProcess(s, this.stdin, this.stdout, this.stderr, this.env, Config.none, this.dir);
95             return wait(pid);
96         } catch (ProcessException e) {
97             error("Could not start process \"%s\": %s", command, e.msg);
98             return -1;
99         }
100     }
101 }
102 
103 unittest {
104     auto p = new ProcessBuilder()
105         .outputTo(".test.txt")
106         .workingDir("source");
107     p.run("ls");
108 
109 }
110 
111 /**
112  * Runs the given command using the user's shell.
113  * Params:
114  *   cmd = The command to execute.
115  * Returns: The exit code from the command. Generally, 0 indicates success. If
116  * the process could not be started, -1 is returned.
117  */
118 public int run(string cmd) {
119     return new ProcessBuilder().run(cmd);
120 }
121 
122 unittest {
123     version(Posix) {
124         assert(run("ls") == 0);
125     }
126     version(Windows) {
127         assert(run("dir") == 0);
128     }
129     assert(run("kjafhdflkuahlkefuhfahlfeuhaf") == -1);
130 }
131 
132 /** 
133  * Runs the given command, and exits the program if the return code is not 0.
134  * Params:
135  *   cmd = The command to run.
136  */
137 public void runOrQuit(string cmd) {
138     import core.stdc.stdlib : exit;
139     int r = run(cmd);
140     if (r != 0) {
141         error("Process \"%s\" exited with code %d", cmd, r);
142         exit(r);
143     }
144 }
145 
146 /** 
147  * Gets an environment variable.
148  * Params:
149  *   key = The name of the environment variable.
150  * Returns: The value of the environment variable, or null.
151  */
152 public string getEnv(string key) {
153     import std.process : environment;
154     try {
155         return environment[key];
156     } catch (Exception e) {
157         return null;
158     }
159 }
160 
161 unittest {
162     assert(getEnv("PATH") !is null);
163     assert(getEnv("flkahelkuhfalukfehlakuefhl") is null);
164 }
165 
166 /** 
167  * Sets an environment variable.
168  * Params:
169  *   key = The name of the environment variable.
170  *   value = The value to set.
171  */
172 public void setEnv(string key, string value) {
173     import std.process : environment;
174     try {
175         environment[key] = value;
176     } catch (Exception e) {
177         error("Could not set environment variable \"%s\": %s", key, e.msg);
178     }
179 }
180 
181 unittest {
182     assert(getEnv("dsh_test_env_1") is null);
183     setEnv("dsh_test_env_1", "yes");
184     assert(getEnv("dsh_test_env_1") == "yes");
185 }
186 
187 /** 
188  * Sleeps for a specified amount of milliseconds.
189  * Params:
190  *   amount = The amount of milliseconds to sleep for.
191  */
192 public void sleepMillis(long amount) {
193     import core.thread;
194     Thread.sleep(msecs(amount));
195 }
196 
197 /** 
198  * Sleeps for a specified amount of seconds.
199  * Params:
200  *   amount = The amount of seconds to sleep for.
201  */
202 public void sleepSeconds(long amount) {
203     import core.thread;
204     Thread.sleep(seconds(amount));
205 }
206 
207 /** 
208  * Convenience method to replace all occurrences of matching patterns with
209  * some other string.
210  * Params:
211  *   s = The string to replace things in.
212  *   pattern = The regular expression to use to match.
213  *   r = The thing to replace each match with.
214  * Returns: The string with all matches replaced.
215  */
216 public string replaceAll(string s, string pattern, string r) {
217     import std.regex;
218     auto re = regex(pattern);
219     return std.regex.replaceAll(s, re, r);
220 }
221 
222 unittest {
223     assert(replaceAll("abc", "a", "d") == "dbc");
224     assert(replaceAll("123abc123", "\\d+", "") == "abc");
225 }