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 }