1 module safearg.main; 2 3 import std.algorithm; 4 import std.array; 5 import std.conv; 6 import std.getopt; 7 import std.process; 8 import std.range; 9 import std.stdio; 10 import std.traits; 11 12 import scriptlike.fail; 13 import safearg.packageVersion; 14 15 immutable helpBanner = 16 `safeArg `~packageVersion~`: <https://github.com/Abscissa/safeArg> 17 Built on `~packageTimestamp~` 18 ----------------------------------------------------- 19 Takes a null-delimited list of args on stdin, and passes them as command line 20 arguments to any program you choose. 21 22 This is more secure, less error-prone, and more portable than using the shell's 23 command substitution or otherwise relying on the shell to parse args. 24 25 USAGE: 26 safearg [options] program_to_run [initial-arguments] < INPUT 27 28 INPUT: 29 A null-delimited (by default) list of command line arguments to app. 30 31 EXAMPLE: 32 printf 'mid1\0mid2' | safearg --post=end1 --post=end2 program_to_run first 33 34 The above (effectively) runs: 35 program_to_run first mid1 mid2 end1 end2 36 37 EXAMPLE: 38 printf 'middle 1\0middle 2' | safearg --post=end printf '[%s]\n' first 39 40 The above (effectively) runs: 41 printf '[%s]\n' 'middle 1' 'middle 2' end 42 43 And outputs: 44 [first] 45 [middle 1] 46 [middle 2] 47 [end] 48 49 OPTIONS:`; 50 51 bool useNewlineDelim = false; 52 string alternateDelim = null; 53 string[] postArgs = null; 54 bool verbose = false; 55 56 // Returns: Should program execution continue? 57 bool doGetOpt(ref string[] args) 58 { 59 immutable usageHint = "For usage, run: safearg --help"; 60 bool showVersion; 61 62 try 63 { 64 auto helpInfo = args.getopt( 65 std.getopt.config.stopOnFirstNonOption, 66 "n|newline", `Use \n and \r\n newlines as delimiter instead of \0`, &useNewlineDelim, 67 "delim", `Use alternate character as delimiter instead of \0 (ex: --delim=,)`, &alternateDelim, 68 "p|post", `Extra "post"-args to be added at the end of the command line.`, &postArgs, 69 "v|verbose", "Echo the generated command to stdout before running.", &verbose, 70 "version", "Show safearg's version number and exit", &showVersion, 71 ); 72 73 if(helpInfo.helpWanted) 74 { 75 defaultGetoptPrinter(helpBanner, helpInfo.options); 76 return false; 77 } 78 } 79 catch(GetOptException e) 80 fail(e.msg ~ "\n" ~ usageHint); 81 82 if(showVersion) 83 { 84 writeln(packageVersion); 85 return false; 86 } 87 88 if(alternateDelim.length > 1) 89 fail("Value for --delim=VALUE must be only byte\n" ~ usageHint); 90 91 if(useNewlineDelim && alternateDelim) 92 fail("Cannot use both --newline and --delim=VALUE\n" ~ usageHint); 93 94 if(args.length < 2) 95 fail("Missing program to run\n" ~ usageHint); 96 97 return true; 98 } 99 100 version(unittest) void main() {} else 101 int main(string[] args) 102 { 103 // Handle our own args 104 if(!doGetOpt(args)) 105 return 0; 106 107 // Parse input 108 string[] outArgs; 109 auto inputRange = stdin.byChunk(1024).joiner(); 110 111 if(useNewlineDelim) 112 outArgs = parseNewlineDelimited(inputRange); 113 else if(alternateDelim) 114 outArgs = parseDelimited(inputRange, cast(const(ubyte)) alternateDelim[0]); 115 else 116 outArgs = parseNullDelimited(inputRange); 117 118 // Build and echo command 119 string[] cmd = args[1..$] ~ outArgs ~ postArgs; 120 if(verbose) 121 stdout.rawWrite(escapeShellCommand(cmd)~"\n"); 122 123 // Invoke command 124 try 125 return spawnProcess(cmd).wait(); 126 catch(ProcessException e) 127 { 128 fail(e.msg); 129 assert(0); 130 } 131 } 132 133 string[] parseNullDelimited(T)(T inputRange) 134 if(isInputRange!T && is(ElementType!T == ubyte)) 135 { 136 return parseDelimited(inputRange, cast(const(ubyte)) '\0'); 137 } 138 139 string[] parseNewlineDelimited(T)(T inputRange) 140 if(isInputRange!T && is(ElementType!T == ubyte)) 141 { 142 return parseDelimited(inputRange, cast(const(ubyte)) '\n', true, cast(const(ubyte)) '\r'); 143 } 144 145 string[] parseDelimited(T)(T inputRange, const ubyte delim, bool useLookBehind=false, const ubyte lookBehind=ubyte.init) 146 if(isInputRange!T && is(ElementType!T == ubyte)) 147 { 148 string[] result; 149 ubyte prevByte; 150 size_t length = 0; 151 auto buf = appender!(ubyte[])(); 152 153 foreach(dataByte; inputRange) 154 { 155 length++; 156 157 //writeln(cast(char)dataByte); 158 if(dataByte == delim) 159 { 160 auto str = cast(string) buf.data.idup; 161 if(useLookBehind && length > 1 && prevByte == lookBehind) 162 str = str[0..$-1]; 163 result ~= str; 164 165 buf.clear(); 166 length = 0; 167 } 168 else 169 buf.put(dataByte); 170 171 prevByte = dataByte; 172 } 173 result ~= cast(string) buf.data.idup; 174 175 //writeln("--------------------"); 176 //foreach(elem; result) 177 // writeln(cast(string)elem); 178 179 return result; 180 } 181 182 unittest 183 { 184 auto convert(string str) 185 { 186 return cast(ubyte[]) str.dup; 187 } 188 189 writeln("Testing parseNullDelimited"); 190 assert( 191 parseNullDelimited( convert("abc\0'hello world'\0def\0\0_123") ) == 192 ["abc", "'hello world'", "def", "", "_123"] 193 ); 194 assert( 195 parseNullDelimited( convert("\0'hello world'\0") ) == 196 ["", "'hello world'", ""] 197 ); 198 assert(parseNullDelimited( convert("\0") ) == ["", ""]); 199 assert(parseNullDelimited( convert("a") ) == ["a"]); 200 assert(parseNullDelimited( convert("") ) == [""]); 201 202 writeln("Testing parseNewlineDelimited"); 203 assert( 204 parseNewlineDelimited(convert("abc\n'hello world'\r\ndef\r\n\r\n123\n\n456") ) == 205 ["abc", "'hello world'", "def", "", "123", "", "456"] 206 ); 207 assert( 208 parseNewlineDelimited( convert("\n'hello world'\r\n") ) == 209 ["", "'hello world'", ""] 210 ); 211 assert( 212 parseNewlineDelimited( convert("\r\n'hello world'\n") ) == 213 ["", "'hello world'", ""] 214 ); 215 assert(parseNewlineDelimited( convert("\n") ) == ["", ""]); 216 assert(parseNewlineDelimited( convert("\r\n") ) == ["", ""]); 217 assert(parseNewlineDelimited( convert("a") ) == ["a"]); 218 assert(parseNewlineDelimited( convert("") ) == [""]); 219 assert(parseNewlineDelimited( convert("abc\rdef") ) == ["abc\rdef"]); 220 }