1 // Scriptlike: Utility to aid in script-like programs.
2 // Written in the D programming language.
3 
4 /// Copyright: Copyright (C) 2014-2015 Nick Sabalausky
5 /// License:   zlib/libpng
6 /// Authors:   Nick Sabalausky
7 
8 module scriptlike.process;
9 
10 import std.array;
11 import std.conv;
12 import std.process;
13 import std.range;
14 
15 import scriptlike.path;
16 import scriptlike.file;
17 
18 /// Indicates a command returned a non-zero errorlevel.
19 class ErrorLevelException : Exception
20 {
21 	int errorLevel;
22 	string command;
23 	
24 	this(int errorLevel, string command, string file=__FILE__, size_t line=__LINE__)
25 	{
26 		this.errorLevel = errorLevel;
27 		this.command = command;
28 		auto msg = text("Command exited with error level ", errorLevel, ": ", command);
29 		super(msg, file, line);
30 	}
31 }
32 
33 /++
34 Runs a command, through the system's command shell interpreter,
35 in typical shell-script style: Synchronously, with the command's
36 stdout/in/err automatically forwarded through your
37 program's stdout/in/err.
38 
39 Optionally takes a working directory to run the command from.
40 
41 The command is echoed if scriptlikeEcho is true.
42 
43 ErrorLevelException is thrown if the process returns a non-zero error level.
44 If you want to handle the error level yourself, use tryRun instead of run.
45 
46 Example:
47 ---------------------
48 Args cmd;
49 cmd ~= Path("some tool");
50 cmd ~= "-o";
51 cmd ~= Path(`dir/out file.txt`);
52 cmd ~= ["--abc", "--def", "-g"];
53 Path("some working dir").run(cmd.data);
54 ---------------------
55 +/
56 void run(string command)
57 {
58 	auto errorLevel = tryRun(command);
59 	if(errorLevel != 0)
60 		throw new ErrorLevelException(errorLevel, command);
61 }
62 
63 ///ditto
64 void run(Path workingDirectory, string command)
65 {
66 	auto saveDir = getcwd();
67 	workingDirectory.chdir();
68 	scope(exit) saveDir.chdir();
69 	
70 	run(command);
71 }
72 
73 /++
74 Runs a command, through the system's command shell interpreter,
75 in typical shell-script style: Synchronously, with the command's
76 stdout/in/err automatically forwarded through your
77 program's stdout/in/err.
78 
79 Optionally takes a working directory to run the command from.
80 
81 The command is echoed if scriptlikeEcho is true.
82 
83 Returns: The error level the process exited with. Or -1 upon failure to
84 start the process.
85 
86 Example:
87 ---------------------
88 Args cmd;
89 cmd ~= Path("some tool");
90 cmd ~= "-o";
91 cmd ~= Path(`dir/out file.txt`);
92 cmd ~= ["--abc", "--def", "-g"];
93 auto errLevel = Path("some working dir").run(cmd.data);
94 ---------------------
95 +/
96 int tryRun(string command)
97 {
98 	echoCommand(command);
99 
100 	if(scriptlikeDryRun)
101 		return 0;
102 	else
103 	{
104 		try
105 			return spawnShell(command).wait();
106 		catch(Exception e)
107 			return -1;
108 	}
109 }
110 
111 ///ditto
112 int tryRun(Path workingDirectory, string command)
113 {
114 	auto saveDir = getcwd();
115 	workingDirectory.chdir();
116 	scope(exit) saveDir.chdir();
117 	
118 	return tryRun(command);
119 }
120 
121 /// Backwards-compatibility alias. runShell may become deprecated in the
122 /// future, so you should use tryRun or run insetad.
123 alias runShell = tryRun;
124 
125 /// Similar to run(), but (like std.process.executeShell) captures and returns
126 /// the output instead of displaying it.
127 string runCollect(string command)
128 {
129 	auto result = tryRunCollect(command);
130 	if(result.status != 0)
131 		throw new ErrorLevelException(result.status, command);
132 
133 	return result.output;
134 }
135 
136 ///ditto
137 string runCollect(Path workingDirectory, string command)
138 {
139 	auto saveDir = getcwd();
140 	workingDirectory.chdir();
141 	scope(exit) saveDir.chdir();
142 	
143 	return runCollect(command);
144 }
145 
146 /// Similar to tryRun(), but (like std.process.executeShell) captures and returns the
147 /// output instead of displaying it.
148 /// 
149 /// Returns same tuple as std.process.executeShell:
150 /// std.typecons.Tuple!(int, "status", string, "output")
151 ///
152 /// Returns: The "status" field will be -1 upon failure to
153 /// start the process.
154 auto tryRunCollect(string command)
155 {
156 	echoCommand(command);
157 	auto result = std.typecons.Tuple!(int, "status", string, "output")(0, null);
158 
159 	if(scriptlikeDryRun)
160 		return result;
161 	else
162 	{
163 		try
164 			return executeShell(command);
165 		catch(Exception e)
166 		{
167 			result.status = -1;
168 			return result;
169 		}
170 	}
171 }
172 
173 ///ditto
174 auto tryRunCollect(Path workingDirectory, string command)
175 {
176 	auto saveDir = getcwd();
177 	workingDirectory.chdir();
178 	scope(exit) saveDir.chdir();
179 	
180 	return tryRunCollect(command);
181 }
182 
183 /++
184 Much like std.array.Appender!string, but specifically geared towards
185 building a command string out of arguments. String and Path can both
186 be appended. All elements added will automatically be escaped,
187 and separated by spaces, as necessary.
188 
189 Example:
190 -------------------
191 Args args;
192 args ~= Path(`some/big path/here/foobar`);
193 args ~= "-A";
194 args ~= "--bcd";
195 args ~= "Hello World";
196 args ~= Path("file.ext");
197 
198 // On windows:
199 assert(args.data == `"some\big path\here\foobar" -A --bcd "Hello World" file.ext`);
200 // On linux:
201 assert(args.data == `'some/big path/here/foobar' -A --bcd 'Hello World' file.ext`);
202 -------------------
203 +/
204 struct Args
205 {
206 	// Internal note: For every element the user adds to ArgsT,
207 	// *two* elements will be added to this internal buf: first a spacer
208 	// (normally a space, or an empty string in the case of the very first
209 	// element the user adds) and then the actual element the user added.
210 	private Appender!(string) buf;
211 	private size_t _length = 0;
212 	
213 	void reserve(size_t newCapacity) @safe pure nothrow
214 	{
215 		// "*2" to account for the spacers
216 		buf.reserve(newCapacity * 2);
217 	}
218 
219 
220 	@property size_t capacity() const @safe pure nothrow
221 	{
222 		// "/2" to account for the spacers
223 		return buf.capacity / 2;
224 	}
225 
226 	@property string data() inout @trusted pure nothrow
227 	{
228 		return buf.data;
229 	}
230 	
231 	@property size_t length()
232 	{
233 		return _length;
234 	}
235 	
236 	private void putSpacer()
237 	{
238 		buf.put(_length==0? "" : " ");
239 	}
240 	
241 	void put(string item)
242 	{
243 		putSpacer();
244 		buf.put(escapeShellArg(item));
245 		_length += 2;
246 	}
247 
248 	void put(Path item)
249 	{
250 		put(item.toRawString());
251 	}
252 
253 	void put(Range)(Range items)
254 		if(
255 			isInputRange!Range &&
256 			(is(ElementType!Range == string) || is(ElementType!Range == Path))
257 		)
258 	{
259 		for(; !items.empty; items.popFront())
260 			put(items.front);
261 	}
262 
263 	void opOpAssign(string op)(string item) if(op == "~")
264 	{
265 		put(item);
266 	}
267 
268 	void opOpAssign(string op)(Path item) if(op == "~")
269 	{
270 		put(item);
271 	}
272 
273 	void opOpAssign(string op, Range)(Range items)
274 		if(
275 			op == "~" &&
276 			isInputRange!Range &&
277 			(is(ElementType!Range == string) || is(ElementType!Range == Path))
278 		)
279 	{
280 		put(items);
281 	}
282 }
283 
284 version(unittest_scriptlike_d)
285 unittest
286 {
287 	import std.stdio : writeln;
288 	writeln("Running Scriptlike unittests: Args");
289 
290 	Args args;
291 	args ~= Path(`some/big path/here/foobar`);
292 	args ~= "-A";
293 	args ~= "--bcd";
294 	args ~= "Hello World";
295 	args ~= Path("file.ext");
296 
297 	version(Windows)
298 		assert(args.data == `"some\big path\here\foobar" -A --bcd "Hello World" file.ext`);
299 	else version(Posix)
300 		assert(args.data == `'some/big path/here/foobar' -A --bcd 'Hello World' file.ext`);
301 }