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 }