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