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 }