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