phobos/std/process.d
jmdavis 27469366a7 Fix modules which incorrectly use std.string's public imports.
Now, they should be correctly importing for the functions publicly
imported by std.string so that if those public imports are ever removed,
they won't break.
2012-09-30 03:29:37 -07:00

1231 lines
32 KiB
D

// Written in the D programming language.
/**
Macros:
WIKI=Phobos/StdProcess
Copyright: Copyright Digital Mars 2007 - 2009.
License: <a href="http://www.boost.org/LICENSE_1_0.txt">Boost License 1.0</a>.
Authors: $(WEB digitalmars.com, Walter Bright),
$(WEB erdani.org, Andrei Alexandrescu),
$(WEB thecybershadow.net, Vladimir Panteleev)
Source: $(PHOBOSSRC std/_process.d)
*/
/*
Copyright Digital Mars 2007 - 2009.
Distributed under the Boost Software License, Version 1.0.
(See accompanying file LICENSE_1_0.txt or copy at
http://www.boost.org/LICENSE_1_0.txt)
*/
module std.process;
import core.stdc.stdlib;
import std.c.stdlib;
import core.stdc.errno;
import core.thread;
import std.c.process;
import std.c.string;
import std.array;
import std.conv;
import std.exception;
import std.internal.processinit;
import std.stdio;
import std.string;
import std.typecons;
version (Windows)
{
import std.format, std.random, std.file;
import core.sys.windows.windows;
import std.utf;
import std.windows.syserror;
}
version (Posix)
{
import core.sys.posix.stdlib;
}
version (unittest)
{
import std.file, std.conv, std.array, std.random;
import std.path : absolutePath;
}
// The following is needed for reading/writing environment variables.
version(Posix)
{
version(OSX)
{
// https://www.gnu.org/software/gnulib/manual/html_node/environ.html
private extern(C) extern __gshared char*** _NSGetEnviron();
__gshared char** environ;
// Run in std.__processinit to avoid cyclic construction errors.
extern(C) void std_process_static_this()
{
environ = *_NSGetEnviron();
}
}
else
{
// Made available by the C runtime:
private extern(C) extern __gshared const char** environ;
}
}
/**
Execute $(D command) in a _command shell.
Returns: If $(D command) is null, returns nonzero if the _command
interpreter is found, and zero otherwise. If $(D command) is not
null, returns -1 on error, or the exit status of command (which may
in turn signal an error in command's execution).
Note: On Unix systems, the homonym C function (which is accessible
to D programs as $(LINK2 std_c_process.html, std.c._system))
returns a code in the same format as $(LUCKY waitpid, waitpid),
meaning that C programs must use the $(D WEXITSTATUS) macro to
extract the actual exit code from the $(D system) call. D's $(D
system) automatically extracts the exit status.
*/
int system(string command)
{
if (!command) return std.c.process.system(null);
const commandz = toStringz(command);
immutable status = std.c.process.system(commandz);
if (status == -1) return status;
version (Posix)
{
if (exited(status))
return exitstatus(status);
// Abnormal termination, return -1.
return -1;
}
else version (Windows)
return status;
else
static assert(0, "system not implemented for this OS.");
}
private void toAStringz(in string[] a, const(char)**az)
{
foreach(string s; a)
{
*az++ = toStringz(s);
}
*az = null;
}
/* ========================================================== */
//version (Windows)
//{
// int spawnvp(int mode, string pathname, string[] argv)
// {
// char** argv_ = cast(char**)alloca((char*).sizeof * (1 + argv.length));
//
// toAStringz(argv, argv_);
//
// return std.c.process.spawnvp(mode, toStringz(pathname), argv_);
// }
//}
// Incorporating idea (for spawnvp() on Posix) from Dave Fladebo
alias std.c.process._P_WAIT P_WAIT;
alias std.c.process._P_NOWAIT P_NOWAIT;
int spawnvp(int mode, string pathname, string[] argv)
{
auto argv_ = cast(const(char)**)alloca((char*).sizeof * (1 + argv.length));
toAStringz(argv, argv_);
version (Posix)
{
return _spawnvp(mode, toStringz(pathname), argv_);
}
else version (Windows)
{
return std.c.process.spawnvp(mode, toStringz(pathname), argv_);
}
else
static assert(0, "spawnvp not implemented for this OS.");
}
version (Posix)
{
private import core.sys.posix.unistd;
private import core.sys.posix.sys.wait;
int _spawnvp(int mode, in char *pathname, in char **argv)
{
int retval = 0;
pid_t pid = fork();
if(!pid)
{ // child
std.c.process.execvp(pathname, argv);
goto Lerror;
}
else if(pid > 0)
{ // parent
if(mode == _P_NOWAIT)
{
retval = pid; // caller waits
}
else
{
while(1)
{
int status;
pid_t wpid = waitpid(pid, &status, 0);
if(exited(status))
{
retval = exitstatus(status);
break;
}
else if(signaled(status))
{
retval = -termsig(status);
break;
}
else if(stopped(status)) // ptrace support
continue;
else
goto Lerror;
}
}
return retval;
}
Lerror:
retval = errno;
char[80] buf = void;
throw new Exception(
"Cannot spawn " ~ to!string(pathname) ~ "; "
~ to!string(strerror_r(retval, buf.ptr, buf.length))
~ " [errno " ~ to!string(retval) ~ "]");
} // _spawnvp
private
{
alias WIFSTOPPED stopped;
alias WIFSIGNALED signaled;
alias WTERMSIG termsig;
alias WIFEXITED exited;
alias WEXITSTATUS exitstatus;
} // private
} // version (Posix)
/* ========================================================== */
/**
* Replace the current process by executing a command, $(D pathname), with
* the arguments in $(D argv). Typically, the first element of $(D argv) is
* the command being executed, i.e. $(D argv[0] == pathname). The 'p'
* versions of $(D exec) search the PATH environment variable for $(D
* pathname). The 'e' versions additionally take the new process'
* environment variables as an array of strings of the form key=value.
*
* Does not return on success (the current process will have been
* replaced). Returns -1 on failure with no indication of the
* underlying error.
*/
int execv(in string pathname, in string[] argv)
{
auto argv_ = cast(const(char)**)alloca((char*).sizeof * (1 + argv.length));
toAStringz(argv, argv_);
return std.c.process.execv(toStringz(pathname), argv_);
}
/** ditto */
int execve(in string pathname, in string[] argv, in string[] envp)
{
auto argv_ = cast(const(char)**)alloca((char*).sizeof * (1 + argv.length));
auto envp_ = cast(const(char)**)alloca((char*).sizeof * (1 + envp.length));
toAStringz(argv, argv_);
toAStringz(envp, envp_);
return std.c.process.execve(toStringz(pathname), argv_, envp_);
}
/** ditto */
int execvp(in string pathname, in string[] argv)
{
auto argv_ = cast(const(char)**)alloca((char*).sizeof * (1 + argv.length));
toAStringz(argv, argv_);
return std.c.process.execvp(toStringz(pathname), argv_);
}
/** ditto */
int execvpe(in string pathname, in string[] argv, in string[] envp)
{
version(Posix)
{
// Is pathname rooted?
if(pathname[0] == '/')
{
// Yes, so just call execve()
return execve(pathname, argv, envp);
}
else
{
// No, so must traverse PATHs, looking for first match
string[] envPaths = std.array.split(
to!string(core.stdc.stdlib.getenv("PATH")), ":");
int iRet = 0;
// Note: if any call to execve() succeeds, this process will cease
// execution, so there's no need to check the execve() result through
// the loop.
foreach(string pathDir; envPaths)
{
string composite = cast(string) (pathDir ~ "/" ~ pathname);
iRet = execve(composite, argv, envp);
}
if(0 != iRet)
{
iRet = execve(pathname, argv, envp);
}
return iRet;
}
}
else version(Windows)
{
auto argv_ = cast(const(char)**)alloca((char*).sizeof * (1 + argv.length));
auto envp_ = cast(const(char)**)alloca((char*).sizeof * (1 + envp.length));
toAStringz(argv, argv_);
toAStringz(envp, envp_);
return std.c.process.execvpe(toStringz(pathname), argv_, envp_);
}
else
{
static assert(0);
} // version
}
/**
* Returns the process ID of the calling process, which is guaranteed to be
* unique on the system. This call is always successful.
*
* Example:
* ---
* writefln("Current process id: %s", getpid());
* ---
*/
alias core.thread.getpid getpid;
/**
Runs $(D_PARAM cmd) in a shell and returns its standard output. If
the process could not be started or exits with an error code,
throws an exception.
Example:
----
auto tempFilename = chomp(shell("mcookie"));
auto f = enforce(fopen(tempFilename), "w");
scope(exit)
{
fclose(f) == 0 || assert(false);
system(escapeShellCommand("rm", tempFilename));
}
... use f ...
----
*/
string shell(string cmd)
{
version(Windows)
{
// Generate a random filename
auto a = appender!string();
foreach (ref e; 0 .. 8)
{
formattedWrite(a, "%x", rndGen.front);
rndGen.popFront();
}
auto filename = a.data;
scope(exit) if (exists(filename)) remove(filename);
// We can't use escapeShellCommands here because we don't know
// if cmd is escaped (wrapped in quotes) or not, without relying
// on shady heuristics. The current code shouldn't cause much
// trouble unless filename contained spaces (it won't).
errnoEnforce(system(cmd ~ "> " ~ filename) == 0);
return readText(filename);
}
else version(Posix)
{
File f;
f.popen(cmd, "r");
char[] line;
string result;
while (f.readln(line))
{
result ~= line;
}
f.close();
return result;
}
else
static assert(0, "shell not implemented for this OS.");
}
unittest
{
auto x = shell("echo wyda");
// @@@ This fails on wine
//assert(x == "wyda" ~ newline, text(x.length));
}
/**
Gets the value of environment variable $(D name) as a string. Calls
$(LINK2 std_c_stdlib.html#_getenv, std.c.stdlib._getenv)
internally. */
string getenv(in char[] name)
{
// Cache the last call's result
static string lastResult;
auto p = core.stdc.stdlib.getenv(toStringz(name));
if (!p) return null;
auto value = p[0 .. strlen(p)];
if (value == lastResult) return lastResult;
return lastResult = value.idup;
}
/**
Sets the value of environment variable $(D name) to $(D value). If the
value was written, or the variable was already present and $(D
overwrite) is false, returns normally. Otherwise, it throws an
exception. Calls $(LINK2 std_c_stdlib.html#_setenv,
std.c.stdlib._setenv) internally. */
version(StdDdoc) void setenv(in char[] name, in char[] value, bool overwrite);
else version(Posix) void setenv(in char[] name, in char[] value, bool overwrite)
{
errnoEnforce(
std.c.stdlib.setenv(toStringz(name), toStringz(value), overwrite) == 0);
}
/**
Removes variable $(D name) from the environment. Calls $(LINK2
std_c_stdlib.html#_unsetenv, std.c.stdlib._unsetenv) internally. */
version(StdDdoc) void unsetenv(in char[] name);
else version(Posix) void unsetenv(in char[] name)
{
errnoEnforce(std.c.stdlib.unsetenv(toStringz(name)) == 0);
}
version (Posix) unittest
{
setenv("wyda", "geeba", true);
assert(getenv("wyda") == "geeba");
// Get again to make sure caching works
assert(getenv("wyda") == "geeba");
unsetenv("wyda");
assert(getenv("wyda") is null);
}
/* ////////////////////////////////////////////////////////////////////////// */
version(MainTest)
{
int main(string[] args)
{
if(args.length < 2)
{
printf("Must supply executable (and optional arguments)\n");
return 1;
}
else
{
string[] dummy_env;
dummy_env ~= "VAL0=value";
dummy_env ~= "VAL1=value";
/+
foreach(string arg; args)
{
printf("%.*s\n", arg);
}
+/
// int i = execv(args[1], args[1 .. args.length]);
// int i = execvp(args[1], args[1 .. args.length]);
int i = execvpe(args[1], args[1 .. args.length], dummy_env);
printf("exec??() has returned! Error code: %d; errno: %d\n", i, /* errno */-1);
return 0;
}
}
}
/* ////////////////////////////////////////////////////////////////////////// */
/** Manipulates environment variables using an associative-array-like
interface.
Examples:
---
// Return variable, or throw an exception if it doesn't exist.
auto path = environment["PATH"];
// Add/replace variable.
environment["foo"] = "bar";
// Remove variable.
environment.remove("foo");
// Return variable, or null if it doesn't exist.
auto foo = environment.get("foo");
// Return variable, or a default value if it doesn't exist.
auto foo = environment.get("foo", "default foo value");
// Return an associative array of type string[string] containing
// all the environment variables.
auto aa = environment.toAA();
---
*/
alias Environment environment;
abstract final class Environment
{
static:
private:
// Return the length of an environment variable (in number of
// wchars, including the null terminator), 0 if it doesn't exist.
version(Windows)
int varLength(LPCWSTR namez)
{
return GetEnvironmentVariableW(namez, null, 0);
}
// Retrieve the environment variable, or return false on failure.
bool getImpl(string name, out string value)
{
version(Posix)
{
const vz = core.sys.posix.stdlib.getenv(toStringz(name));
if (vz == null) return false;
auto v = vz[0 .. strlen(vz)];
// Cache the last call's result.
static string lastResult;
if (v != lastResult) lastResult = v.idup;
value = lastResult;
return true;
}
else version(Windows)
{
const namez = toUTF16z(name);
immutable len = varLength(namez);
if (len == 0) return false;
if (len == 1) return true;
auto buf = new WCHAR[len];
GetEnvironmentVariableW(namez, buf.ptr, to!DWORD(buf.length));
value = toUTF8(buf[0 .. $-1]);
return true;
}
else static assert(0);
}
public:
// Retrieve an environment variable, throw on failure.
string opIndex(string name)
{
string value;
enforce(getImpl(name, value), "Environment variable not found: "~name);
return value;
}
// Assign a value to an environment variable. If the variable
// exists, it is overwritten.
string opIndexAssign(string value, string name)
{
version(Posix)
{
if (core.sys.posix.stdlib.setenv(toStringz(name),
toStringz(value), 1) != -1)
{
return value;
}
// The default errno error message is very uninformative
// in the most common case, so we handle it manually.
enforce(errno != EINVAL,
"Invalid environment variable name: '"~name~"'");
errnoEnforce(false,
"Failed to add environment variable");
assert(0);
}
else version(Windows)
{
enforce(
SetEnvironmentVariableW(toUTF16z(name), toUTF16z(value)),
sysErrorString(GetLastError())
);
return value;
}
else static assert(0);
}
// Remove an environment variable. The function succeeds even
// if the variable isn't in the environment.
void remove(string name)
{
version(Posix)
{
core.sys.posix.stdlib.unsetenv(toStringz(name));
}
else version(Windows)
{
SetEnvironmentVariableW(toUTF16z(name), null);
}
else static assert(0);
}
// Same as opIndex, except return a default value if
// the variable doesn't exist.
string get(string name, string defaultValue = null)
{
string value;
auto found = getImpl(name, value);
return found ? value : defaultValue;
}
// Return all environment variables in an associative array.
string[string] toAA()
{
string[string] aa;
version(Posix)
{
for (int i=0; environ[i] != null; ++i)
{
immutable varDef = to!string(environ[i]);
immutable eq = varDef.indexOf('=');
assert (eq >= 0);
immutable name = varDef[0 .. eq];
immutable value = varDef[eq+1 .. $];
// In POSIX, environment variables may be defined more
// than once. This is a security issue, which we avoid
// by checking whether the key already exists in the array.
// For more info:
// http://www.dwheeler.com/secure-programs/Secure-Programs-HOWTO/environment-variables.html
if (name !in aa) aa[name] = value;
}
}
else version(Windows)
{
auto envBlock = GetEnvironmentStringsW();
enforce (envBlock, "Failed to retrieve environment variables.");
scope(exit) FreeEnvironmentStringsW(envBlock);
for (int i=0; envBlock[i] != '\0'; ++i)
{
auto start = i;
while (envBlock[i] != '=')
{
assert (envBlock[i] != '\0');
++i;
}
immutable name = toUTF8(envBlock[start .. i]);
start = i+1;
while (envBlock[i] != '\0') ++i;
aa[name] = toUTF8(envBlock[start .. i]);
}
}
else static assert(0);
return aa;
}
}
unittest
{
// New variable
environment["std_process"] = "foo";
assert (environment["std_process"] == "foo");
// Set variable again
environment["std_process"] = "bar";
assert (environment["std_process"] == "bar");
// Remove variable
environment.remove("std_process");
// Remove again, should succeed
environment.remove("std_process");
// Throw on not found.
try { environment["std_process"]; assert(0); } catch(Exception e) { }
// get() without default value
assert (environment.get("std.process") == null);
// get() with default value
assert (environment.get("std_process", "baz") == "baz");
// Convert to associative array
auto aa = environment.toAA();
assert (aa.length > 0);
foreach (n, v; aa)
{
// Wine has some bugs related to environment variables:
// - Wine allows the existence of an env. variable with the name
// "\0", but GetEnvironmentVariable refuses to retrieve it.
// - If an env. variable has zero length, i.e. is "\0",
// GetEnvironmentVariable should return 1. Instead it returns
// 0, indicating the variable doesn't exist.
version(Windows) if (n.length == 0 || v.length == 0) continue;
assert (v == environment[n]);
}
}
version(StdDdoc)
{
/****************************************
* Start up the browser and set it to viewing the page at url.
*/
void browse(string url);
}
else
version (Windows)
{
import core.sys.windows.windows;
extern (Windows)
HINSTANCE ShellExecuteA(HWND hwnd, LPCSTR lpOperation, LPCSTR lpFile, LPCSTR lpParameters, LPCSTR lpDirectory, INT nShowCmd);
pragma(lib,"shell32.lib");
void browse(string url)
{
ShellExecuteA(null, "open", toStringz(url), null, null, SW_SHOWNORMAL);
}
}
else version (OSX)
{
import core.stdc.stdio;
import core.stdc.string;
import core.sys.posix.unistd;
void browse(string url)
{
const(char)*[5] args;
const(char)* browser = core.stdc.stdlib.getenv("BROWSER");
if (browser)
{ browser = strdup(browser);
args[0] = browser;
args[1] = toStringz(url);
args[2] = null;
}
else
{
args[0] = "open".ptr;
args[1] = toStringz(url);
args[2] = null;
}
auto childpid = fork();
if (childpid == 0)
{
core.sys.posix.unistd.execvp(args[0], cast(char**)args.ptr);
perror(args[0]); // failed to execute
return;
}
if (browser)
free(cast(void*)browser);
}
}
else version (Posix)
{
import core.stdc.stdio;
import core.stdc.string;
import core.sys.posix.unistd;
void browse(string url)
{
const(char)*[3] args;
const(char)* browser = core.stdc.stdlib.getenv("BROWSER");
if (browser)
{ browser = strdup(browser);
args[0] = browser;
}
else
//args[0] = "x-www-browser".ptr; // doesn't work on some systems
args[0] = "xdg-open".ptr;
args[1] = toStringz(url);
args[2] = null;
auto childpid = fork();
if (childpid == 0)
{
core.sys.posix.unistd.execvp(args[0], cast(char**)args.ptr);
perror(args[0]); // failed to execute
return;
}
if (browser)
free(cast(void*)browser);
}
}
else
static assert(0, "os not supported");
/* ////////////////////////////////////////////////////////////////////////// */
/*
Command line arguments exist in three forms:
1) string or char* array, as received by main.
Also used internally on POSIX systems.
2) Command line string, as used in Windows'
CreateProcess and CommandLineToArgvW functions.
A specific quoting and escaping algorithm is used
to distinguish individual arguments.
3) Shell command string, as written at a shell prompt
or passed to cmd /C - this one may contain shell
control characters, e.g. > or | for redirection /
piping - thus, yet another layer of escaping is
used to distinguish them from program arguments.
Except for escapeWindowsArgument, the intermediary
format (2) is hidden away from the user in this module.
*/
pure @safe nothrow
private char[] charAllocator(size_t size) { return new char[size]; }
/**
Quote an argument in a manner conforming to the behavior of
$(LINK2 http://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx,
CommandLineToArgvW).
*/
pure nothrow
string escapeWindowsArgument(in char[] arg)
{
// Rationale for leaving this function as public:
// this algorithm of escaping paths is also used in other software,
// e.g. DMD's response files.
auto buf = escapeWindowsArgumentImpl!charAllocator(arg);
return assumeUnique(buf);
}
@safe nothrow
private char[] escapeWindowsArgumentImpl(alias allocator)(in char[] arg)
if (is(typeof(allocator(size_t.init)[0] = char.init)))
{
// References:
// * http://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx
// * http://blogs.msdn.com/b/oldnewthing/archive/2010/09/17/10063629.aspx
// Calculate the total string size.
// Trailing backslashes must be escaped
bool escaping = true;
// Result size = input size + 2 for surrounding quotes + 1 for the
// backslash for each escaped character.
size_t size = 1 + arg.length + 1;
foreach_reverse (c; arg)
{
if (c == '"')
{
escaping = true;
size++;
}
else
if (c == '\\')
{
if (escaping)
size++;
}
else
escaping = false;
}
// Construct result string.
auto buf = allocator(size);
size_t p = size;
buf[--p] = '"';
escaping = true;
foreach_reverse (c; arg)
{
if (c == '"')
escaping = true;
else
if (c != '\\')
escaping = false;
buf[--p] = c;
if (escaping)
buf[--p] = '\\';
}
buf[--p] = '"';
assert(p == 0);
return buf;
}
version(Windows) version(unittest)
{
import core.sys.windows.windows;
import core.stdc.stddef;
extern (Windows) wchar_t** CommandLineToArgvW(wchar_t*, int*);
extern (C) size_t wcslen(in wchar *);
unittest
{
string[] testStrings = [
`Hello`,
`Hello, world`,
`Hello, "world"`,
`C:\`,
`C:\dmd`,
`C:\Program Files\`,
];
enum CHARS = `_x\" *&^`; // _ is placeholder for nothing
foreach (c1; CHARS)
foreach (c2; CHARS)
foreach (c3; CHARS)
foreach (c4; CHARS)
testStrings ~= [c1, c2, c3, c4].replace("_", "");
foreach (s; testStrings)
{
auto q = escapeWindowsArgument(s);
LPWSTR lpCommandLine = (to!(wchar[])("Dummy.exe " ~ q) ~ "\0"w).ptr;
int numArgs;
LPWSTR* args = CommandLineToArgvW(lpCommandLine, &numArgs);
scope(exit) LocalFree(args);
assert(numArgs==2, s ~ " => " ~ q ~ " #" ~ text(numArgs-1));
auto arg = to!string(args[1][0..wcslen(args[1])]);
assert(arg == s, s ~ " => " ~ q ~ " => " ~ arg);
}
}
}
pure nothrow
private string escapePosixArgument(in char[] arg)
{
auto buf = escapePosixArgumentImpl!charAllocator(arg);
return assumeUnique(buf);
}
@safe nothrow
private char[] escapePosixArgumentImpl(alias allocator)(in char[] arg)
if (is(typeof(allocator(size_t.init)[0] = char.init)))
{
// '\'' means: close quoted part of argument, append an escaped
// single quote, and reopen quotes
// Below code is equivalent to:
// return `'` ~ std.array.replace(arg, `'`, `'\''`) ~ `'`;
size_t size = 1 + arg.length + 1;
foreach (c; arg)
if (c == '\'')
size += 3;
auto buf = allocator(size);
size_t p = 0;
buf[p++] = '\'';
foreach (c; arg)
if (c == '\'')
{
buf[p..p+4] = `'\''`;
p += 4;
}
else
buf[p++] = c;
buf[p++] = '\'';
assert(p == size);
return buf;
}
@safe nothrow
private auto escapeShellArgument(alias allocator)(in char[] arg)
{
// The unittest for this function requires special
// preparation - see below.
version (Windows)
return escapeWindowsArgumentImpl!allocator(arg);
else
return escapePosixArgumentImpl!allocator(arg);
}
pure nothrow
private string escapeShellArguments(in char[][] args)
{
char[] buf;
@safe nothrow
char[] allocator(size_t size)
{
if (buf.length == 0)
return buf = new char[size];
else
{
auto p = buf.length;
buf.length = buf.length + 1 + size;
buf[p++] = ' ';
return buf[p..p+size];
}
}
foreach (arg; args)
escapeShellArgument!allocator(arg);
return assumeUnique(buf);
}
string escapeWindowsShellCommand(in char[] command)
{
auto result = appender!string();
result.reserve(command.length);
foreach (c; command)
switch (c)
{
case '\0':
assert(0, "Cannot put NUL in command line");
case '\r':
case '\n':
assert(0, "CR/LF are not escapable");
case '\x01': .. case '\x09':
case '\x0B': .. case '\x0C':
case '\x0E': .. case '\x1F':
case '"':
case '^':
case '&':
case '<':
case '>':
case '|':
result.put('^');
goto default;
default:
result.put(c);
}
return result.data();
}
private string escapeShellCommandString(string command)
{
version (Windows)
return escapeWindowsShellCommand(command);
else
return command;
}
/**
Escape an argv-style argument array to be used with the
$(D system) or $(D shell) functions.
Example:
---
string url = "http://dlang.org/";
system(escapeShellCommand("wget", url, "-O", "dlang-index.html"));
---
Concatenate multiple $(D escapeShellCommand) and
$(D escapeShellFileName) results to use shell redirection or
piping operators.
Example:
---
system(
escapeShellCommand("curl", "http://dlang.org/download.html") ~
"|" ~
escapeShellCommand("grep", "-o", `http://\S*\.zip`) ~
">" ~
escapeShellFileName("D download links.txt"));
---
*/
string escapeShellCommand(in char[][] args...)
{
return escapeShellCommandString(escapeShellArguments(args));
}
/**
Escape a filename to be used for shell redirection with
the $(D system) or $(D shell) functions.
*/
pure nothrow
string escapeShellFileName(in char[] fn)
{
// The unittest for this function requires special
// preparation - see below.
version (Windows)
return cast(string)('"' ~ fn ~ '"');
else
return escapePosixArgument(fn);
}
// Loop generating strings with random characters
//version = unittest_burnin;
version(unittest_burnin)
unittest
{
// There are no readily-available commands on all platforms suitable
// for properly testing command escaping. The behavior of CMD's "echo"
// built-in differs from the POSIX program, and Windows ports of POSIX
// environments (Cygwin, msys, gnuwin32) may interfere with their own
// "echo" ports.
// To run this unit test, create std_process_unittest_helper.d with the
// following content and compile it:
// import std.stdio, std.array; void main(string[] args) { write(args.join("\0")); }
// Then, test this module with:
// rdmd --main -unittest -version=unittest_burnin process.d
auto helper = absolutePath("std_process_unittest_helper");
assert(shell(helper ~ " hello").split("\0")[1..$] == ["hello"], "Helper malfunction");
void test(string[] s, string fn)
{
string e;
string[] g;
e = escapeShellCommand(helper ~ s);
{
scope(failure) writefln("shell() failed.\nExpected:\t%s\nEncoded:\t%s", s, [e]);
g = shell(e).split("\0")[1..$];
}
assert(s == g, format("shell() test failed.\nExpected:\t%s\nGot:\t\t%s\nEncoded:\t%s", s, g, [e]));
e = escapeShellCommand(helper ~ s) ~ ">" ~ escapeShellFileName(fn);
{
scope(failure) writefln("system() failed.\nExpected:\t%s\nFilename:\t%s\nEncoded:\t%s", s, [fn], [e]);
system(e);
g = readText(fn).split("\0")[1..$];
}
remove(fn);
assert(s == g, format("system() test failed.\nExpected:\t%s\nGot:\t\t%s\nEncoded:\t%s", s, g, [e]));
}
while (true)
{
string[] args;
foreach (n; 0..uniform(1, 4))
{
string arg;
foreach (l; 0..uniform(0, 10))
{
dchar c;
while (true)
{
version (Windows)
{
// As long as DMD's system() uses CreateProcessA,
// we can't reliably pass Unicode
c = uniform(0, 128);
}
else
c = uniform!ubyte();
if (c == 0)
continue; // argv-strings are zero-terminated
version (Windows)
if (c == '\r' || c == '\n')
continue; // newlines are unescapable on Windows
break;
}
arg ~= c;
}
args ~= arg;
}
// generate filename
string fn = "test_";
foreach (l; 0..uniform(1, 10))
{
dchar c;
while (true)
{
version (Windows)
c = uniform(0, 128); // as above
else
c = uniform!ubyte();
if (c == 0 || c == '/')
continue; // NUL and / are the only characters
// forbidden in POSIX filenames
version (Windows)
if (c < '\x20' || c == '<' || c == '>' || c == ':' ||
c == '"' || c == '\\' || c == '|' || c == '?' || c == '*')
continue; // http://msdn.microsoft.com/en-us/library/aa365247(VS.85).aspx
break;
}
fn ~= c;
}
test(args, fn);
}
}