1 /** 2 * Simple execution of shell commands, 3 * and wrappers for common utilities. 4 * 5 * License: 6 * This Source Code Form is subject to the terms of 7 * the Mozilla Public License, v. 2.0. If a copy of 8 * the MPL was not distributed with this file, You 9 * can obtain one at http://mozilla.org/MPL/2.0/. 10 * 11 * Authors: 12 * Vladimir Panteleev <ae@cy.md> 13 */ 14 15 module ae.sys.cmd; 16 17 import core.thread; 18 19 import std.exception; 20 import std.process; 21 import std.stdio; 22 import std..string; 23 24 import ae.sys.file; 25 26 /// Returns a very unique name for a temporary file. 27 string getTempFileName(string extension) 28 { 29 import std.random; 30 import std.file; 31 import std.path : buildPath; 32 33 static int counter; 34 return buildPath(tempDir(), format("run-%d-%d-%d-%d.%s", 35 getpid(), 36 getCurrentThreadID(), 37 uniform!uint(), 38 counter++, 39 extension 40 )); 41 } 42 43 /// Like `thisProcessID`, but for threads. 44 ulong getCurrentThreadID() 45 { 46 version (Windows) 47 { 48 import core.sys.windows.windows; 49 return GetCurrentThreadId(); 50 } 51 else 52 version (Posix) 53 { 54 import core.sys.posix.pthread; 55 return cast(ulong)pthread_self(); 56 } 57 } 58 59 // ************************************************************************ 60 61 struct ProcessParams 62 { 63 const(string[string]) environment = null; 64 std.process.Config config = std.process.Config.none; 65 size_t maxOutput = size_t.max; 66 string workDir = null; 67 68 this(Params...)(Params params) 69 { 70 foreach (arg; params) 71 { 72 static if (is(typeof(arg) == string)) 73 workDir = arg; 74 else 75 static if (is(typeof(arg) : const(string[string]))) 76 environment = arg; 77 else 78 static if (is(typeof(arg) == size_t)) 79 maxOutput = arg; 80 else 81 static if (is(typeof(arg) == std.process.Config)) 82 config = arg; 83 else 84 static assert(false, "Unknown type for process invocation parameter: " ~ typeof(arg).stringof); 85 } 86 } 87 } 88 89 private void invoke(alias runner)(string[] args) 90 { 91 //debug scope(failure) std.stdio.writeln("[CWD] ", getcwd()); 92 debug(CMD) std.stdio.stderr.writeln("invoke: ", args); 93 auto status = runner(); 94 enforce(status == 0, 95 "Command `%s` failed with status %d".format(escapeShellCommand(args), status)); 96 } 97 98 /// std.process helper. 99 /// Run a command, and throw if it exited with a non-zero status. 100 void run(Params...)(string[] args, Params params) 101 { 102 auto parsed = ProcessParams(params); 103 invoke!({ return spawnProcess( 104 args, stdin, stdout, stderr, 105 parsed.environment, parsed.config, parsed.workDir 106 ).wait(); })(args); 107 } 108 109 /// std.process helper. 110 /// Run a command and collect its output. 111 /// Throw if it exited with a non-zero status. 112 string query(Params...)(string[] args, Params params) 113 { 114 auto parsed = ProcessParams(params); 115 string output; 116 invoke!({ 117 // Don't use execute due to https://issues.dlang.org/show_bug.cgi?id=17844 118 version (none) 119 { 120 auto result = execute(args, parsed.environment, parsed.config, parsed.maxOutput, parsed.workDir); 121 output = result.output.stripRight(); 122 return result.status; 123 } 124 else 125 { 126 auto pipes = pipeProcess(args, Redirect.stdout, 127 parsed.environment, parsed.config, parsed.workDir); 128 output = cast(string)readFile(pipes.stdout); 129 return pipes.pid.wait(); 130 } 131 })(args); 132 return output; 133 } 134 135 /// std.process helper. 136 /// Run a command, feed it the given input, and collect its output. 137 /// Throw if it exited with non-zero status. Return output. 138 T[] pipe(T, Params...)(string[] args, in T[] input, Params params) 139 { 140 auto parsed = ProcessParams(params); 141 T[] output; 142 invoke!({ 143 auto pipes = pipeProcess(args, Redirect.stdin | Redirect.stdout, 144 parsed.environment, parsed.config, parsed.workDir); 145 auto f = pipes.stdin; 146 auto writer = writeFileAsync(f, input); 147 scope(exit) writer.join(); 148 output = cast(T[])readFile(pipes.stdout); 149 return pipes.pid.wait(); 150 })(args); 151 return output; 152 } 153 154 // ************************************************************************ 155 156 /// Wrapper for the `iconv` program. 157 ubyte[] iconv(in void[] data, string inputEncoding, string outputEncoding) 158 { 159 auto args = ["timeout", "30", "iconv", "-f", inputEncoding, "-t", outputEncoding]; 160 auto result = pipe(args, data); 161 return cast(ubyte[])result; 162 } 163 164 /// ditto 165 string iconv(in void[] data, string inputEncoding) 166 { 167 import std.utf; 168 auto result = cast(string)iconv(data, inputEncoding, "UTF-8"); 169 validate(result); 170 return result; 171 } 172 173 version (HAVE_UNIX) 174 unittest 175 { 176 assert(iconv("Hello"w, "UTF-16LE") == "Hello"); 177 } 178 179 /// Wrapper for the `sha1sum` program. 180 string sha1sum(in void[] data) 181 { 182 auto output = cast(string)pipe(["sha1sum", "-b", "-"], data); 183 return output[0..40]; 184 } 185 186 version (HAVE_UNIX) 187 unittest 188 { 189 assert(sha1sum("") == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); 190 assert(sha1sum("a b\nc\r\nd") == "667c71ffe2ac8a4fe500e3b96435417e4c5ec13b"); 191 } 192 193 // ************************************************************************ 194 195 import ae.utils.path; 196 deprecated alias NULL_FILE = nullFileName; 197 198 // ************************************************************************ 199 200 /// Reverse of std.process.environment.toAA 201 void setEnvironment(string[string] env) 202 { 203 foreach (k, v; env) 204 if (k.length) 205 environment[k] = v; 206 foreach (k, v; environment.toAA()) 207 if (k.length && k !in env) 208 environment.remove(k); 209 } 210 211 /// Expand Windows-like variable placeholders (`"%VAR%"`) in the given string. 212 string expandWindowsEnvVars(alias getenv = environment.get)(string s) 213 { 214 import std.array; 215 auto buf = appender!string(); 216 217 size_t lastPercent = 0; 218 bool inPercent = false; 219 220 foreach (i, c; s) 221 if (c == '%') 222 { 223 if (inPercent) 224 buf.put(lastPercent == i ? "%" : getenv(s[lastPercent .. i])); 225 else 226 buf.put(s[lastPercent .. i]); 227 inPercent = !inPercent; 228 lastPercent = i + 1; 229 } 230 enforce(!inPercent, "Unterminated environment variable name"); 231 buf.put(s[lastPercent .. $]); 232 return buf.data; 233 } 234 235 unittest 236 { 237 std.process.environment[`FOOTEST`] = `bar`; 238 assert("a%FOOTEST%b".expandWindowsEnvVars() == "abarb"); 239 } 240 241 // ************************************************************************ 242 243 /// Like `std.process.wait`, but with a timeout. 244 /// If the timeout is exceeded, the program is killed. 245 int waitTimeout(Pid pid, Duration time) 246 { 247 bool ok = false; 248 auto t = new Thread({ 249 Thread.sleep(time); 250 if (!ok) 251 try 252 pid.kill(); 253 catch (Exception) {} // Ignore race condition 254 }).start(); 255 auto result = pid.wait(); 256 ok = true; 257 return result; 258 } 259 260 /// Wait for process to exit asynchronously. 261 /// Call callback when it exits. 262 /// WARNING: the callback will be invoked in another thread! 263 void waitAsync(Pid pid, void delegate(int) callback = null) 264 { 265 auto t = new Thread({ 266 auto result = pid.wait(); 267 if (callback) 268 callback(result); 269 }).start(); 270 }