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