1 /++
2 	Support functions to build a custom unix-style shell.
3 
4 
5 	$(PITFALL
6 		Do NOT use this to try to sanitize, escape, or otherwise parse what another shell would do with a string! Every shell is different and this implements my rules which may differ in subtle ways from any other common shell.
7 
8 		If you want to use this to understand a command, also use it to execute that command so you get what you expect.
9 	)
10 
11 	Some notes about this shell syntax:
12 	$(LIST
13 		* An "execution batch" is a set of command submitted to be run. This is typically a single command line or shell script.
14 		* ; means "execute the current command and wait for it to complete". If it returns a non-zero errorlevel, the current execution batch is aborted.
15 		* ;; means the same as ;, except that if it returned a non-zero errorlevel, the current batch is allowed to proceed.
16 		* & means "execute current command in the background"
17 		* ASCII space, tab, and newline outside of quotes are all collapsed to a single space, html style. If you want multiple commands in a single execution, use ;. Interactively, pressing enter usually means a new execution, but in a script, you need to use ; (or &, &&, or ||), not just newline, to separate commands.
18 	)
19 
20 	History:
21 		Added October 18, 2025
22 
23 	Bugs:
24 	$(LIST
25 		* a failure in a pipeline at any point should mark that command as failing, not just the first command.
26 		* `sleep 1 && sleep 1 &` only puts the second sleep in the background.
27 		* bash supports $'\e' which follows C escape rules inside the single quotes. want?
28 		* ${name:use_if_unset} not implemented. might not bother.
29 		* glob expansion is minimal - * works, but no ?, no [stuff]. The * is all i personally care about.
30 		* `substitution` and $(...) is not implemented
31 		* variable expansion ${IDENT} is not implemented.
32 		* no !history recall. or history command in general
33 		* job control is rudimentary - no fg, bg, jobs, ctrl+z, etc.
34 		* i'd like it to automatically set -o ignoreeof in some circumstances
35 		* prompt could be cooler
36 			PS1 = normal prompt
37 			PS2 = continuation prompt
38 			Bash shell executes the content of the PROMPT_COMMAND just before displaying the PS1 variable.
39 
40 			bash does it with `\u` and stuff but i kinda think using `$USER` and such might make more sense.
41 		* i do `alias thing args...` instead of `alias thing="args..."`. i kinda prefer it this way tho
42 		* the api is not very good
43 		* ulimit? sourcing things too. aliases.
44 		* deeshrc is pulled from cwd
45 		* tab complete of available commands not implemented - get it from path search.
46 	)
47 
48 	Questionable_ideas:
49 	$(LIST
50 		* be able to receive an external command, e.g. from vim hotkey
51 
52 		* separate stdout and stderr more by default, allow stderr pipes.
53 		* custom completion scripts? prolly not bash compatible since the scripts would be more involved
54 		* some kind of scriptable cmdlet? a full on script language with shell stuff embeddable?
55 			see https://hush-shell.github.io/cmd/index.html for some ok ideas
56 		* do something fun with job control. idk what tho really.
57 		* can terminal emulators get notifications when the foreground process group changes? i don't think so but i could make a "poll again now" sequence since i control shell and possibly terminal emulator now.
58 		* change DISPLAY and such when attaching remote sessions
59 	)
60 +/
61 module arsd.shell;
62 
63 import arsd.core;
64 
65 import core.thread.fiber;
66 
67 /++
68 	Holds some context needed for shell expansions.
69 +/
70 struct ShellContext {
71 	// stuff you set to interface with OS data
72 	string delegate(scope const(char)[] name) getEnvironmentVariable;
73 	string delegate(scope const(char)[] username) getUserHome; // for ~ expansion. if the username is null, it should look up the current user.
74 
75 	// something you inform it of
76 	//bool isInteractive;
77 
78 	// state you can set ahead of time and the shell context executor can modify
79 	string scriptName; // $0, special
80 	string[] scriptArgs; // $*, $@, $1...$n, $#. `shift` modifies it.
81 	string[string] vars;
82 	string[][string] aliases;
83 	int mostRecentCommandStatus; // $?
84 
85 	// state managed internally whilst running
86 	ShellCommand[] jobs;
87 	string[] directoryStack;
88 	ShellLoop[] loopStack;
89 
90 	bool exitRequested;
91 
92 	private SchedulableTask jobToForeground;
93 }
94 
95 struct ShellLoop {
96 	string[] args;
97 	int position;
98 	ShellLoop[] commands;
99 }
100 
101 enum QuoteStyle {
102 	none, // shell might do special treatment of characters
103 	nonExpanding, // 'thing'. everything is unmodified in output
104 	expanding, // "thing". $variables can be expanded, but not {a,b}, {1..3}, ~, ? or * or similar glob stuff. note the ~ and {} expansions happen regardless of if such a file exists. ? and * remains ? and * unless there is a match. "thing" can also expand to multiple arguments, but not just because it has a space in it, only if the variable has a space in it. what madness lol. $* and $@ need to expand to multiple args tho
105 	/+
106 		$* = all args as a single string, but can be multiple args when interpreted (basically the command line)
107 		"$*" = the command line as a single arg
108 		$@ = the argv is preserved without converting back into string but any args with spaces can still be split
109 		"$@" = the only sane one tbh, forwards the args as argv w/o modification i think
110 
111 		$1, $2, etc. $# is count of args
112 	+/
113 }
114 
115 /++
116 
117 +/
118 alias Globber = string[] delegate(ShellLexeme[] str, ShellContext context);
119 
120 private bool isVarChar(char next) {
121 	return (next >= 'A' && next <= 'Z') || (next >= 'a' && next <= 'z') || next == '_' || (next >= '0' && next <= '9');
122 }
123 
124 /++
125 	Represents one component of a shell command line as a precursor to parsing.
126 +/
127 struct ShellLexeme {
128 	string l;
129 	QuoteStyle quoteStyle;
130 
131 	/++
132 		Expands shell arguments and escapes the glob characters, if necessary
133 	+/
134 	string[] toExpansions(ShellContext context) {
135 		final switch(quoteStyle) {
136 			case QuoteStyle.none:
137 			case QuoteStyle.expanding:
138 				// FIXME: if it is none it can return multiple arguments...
139 				// and subcommands can be executed here. `foo` and "`foo"` are things.
140 
141 				/+
142 					Expanded in here both cases:
143 						* $VARs
144 						* ${VAR}s
145 						* $?, $@, etc.
146 						* `subcommand` and $(subcommand)
147 						* $((math))
148 					ONLY IF QuoteStyle.none:
149 						* {1..3}
150 						* {a,b}
151 						* ~, ~name
152 
153 						* bash does glob expansions iff files actually match? but i think that's premature for us here. because `*'.d'` should work and we're only going to see the part inside or outside of the quote at this stage. hence why in non-expanding it escapes the glob chars.
154 
155 						..... but echo "*" prints a * so it shouldn't be trying to glob in the expanding context either. glob is only possible if the star appears in the unquoted thing. maybe it is unquoted * and ? that gets the magic internal chars that are forbidden elsewhere instead of escaping the rest
156 				+/
157 
158 				string[] ret;
159 				ret ~= null;
160 				size_t lastIndex = 0;
161 				for(size_t idx = 0; idx < l.length; idx++) {
162 					char ch = l[idx];
163 
164 					if(ch == '$') {
165 						if(idx + 1 < l.length) {
166 							char next = l[idx + 1];
167 							string varName;
168 							size_t finalIndex;
169 							if(isVarChar(next)) {
170 								finalIndex = idx + 1;
171 								while(finalIndex < l.length && isVarChar(l[finalIndex])) {
172 									finalIndex++;
173 								}
174 								varName = l[idx + 1 .. finalIndex];
175 								finalIndex--; // it'll get ++'d again later
176 							} else if(next == '{') {
177 								// FIXME - var name enclosed in {}
178 							} else if(next == '(') {
179 								// FIXME - command substitution or arithmetic
180 							} else if(next == '?' || next == '*' || next == '@' || next == '#') {
181 								varName = l[idx + 1 .. idx + 2];
182 								finalIndex = idx + 1;
183 							}
184 
185 							if(varName.length) {
186 								assert(finalIndex > 0);
187 								string varContent;
188 								bool useVarContent = true;
189 
190 								foreach(ref r; ret)
191 									r ~= l[lastIndex .. idx];
192 
193 								// if we're not in double quotes, these are allowed to expand to multiple args
194 								// but if we are they should be just one. in a normal unix shell anyway. idk
195 								switch(varName) {
196 									case "0":
197 										varContent = context.scriptName;
198 									break;
199 									case "?":
200 										varContent = toStringInternal(context.mostRecentCommandStatus);
201 									break;
202 									case "*":
203 										import arsd.string;
204 										varContent = join(context.scriptArgs, " ");
205 									break;
206 									case "@":
207 										// needs to expand similarly to {a,b,c}
208 										if(context.scriptArgs.length) {
209 											useVarContent = false;
210 
211 											auto origR = ret.length;
212 
213 											// FIXME: if quoteStyle ==  none, we can split each script arg on spaces too...
214 
215 											foreach(irrelevant; 0 .. context.scriptArgs.length - 1)
216 												for(size_t i = 0; i < origR; i++)
217 													ret ~= ret[0].dup;
218 
219 											foreach(exp; 0 .. context.scriptArgs.length)
220 												foreach(ref r; ret[origR * exp .. origR * (exp + 1)])
221 													r ~= context.scriptArgs[exp];
222 										}
223 									break;
224 									case "#":
225 										varContent = toStringInternal(context.scriptArgs.length);
226 									break;
227 									default:
228 									bool wasAllNumbers = true;
229 									foreach(char chn; varName) {
230 										if(!(chn >= '0' && chn <= '9')) {
231 											wasAllNumbers = false;
232 											break;
233 										}
234 									}
235 
236 									if(wasAllNumbers) {
237 										import arsd.conv;
238 										auto idxn = to!int(varName);
239 										if(idxn == 0 || idxn > context.scriptArgs.length)
240 											throw new Exception("Shell variable argument out of range: " ~ varName);
241 										varContent = context.scriptArgs[idxn - 1];
242 									} else {
243 										if(varName !in context.vars) {
244 											if(context.getEnvironmentVariable) {
245 												auto ev = context.getEnvironmentVariable(varName);
246 												if(ev is null)
247 													throw new Exception("No such shell or environment variable: " ~ varName);
248 												varContent = ev;
249 											} else {
250 												throw new Exception("No such shell variable: " ~ varName);
251 											}
252 										} else {
253 											varContent = context.vars[varName];
254 										}
255 									}
256 								}
257 
258 								if(useVarContent) {
259 									// FIXME: if quoteStyle ==  none, we can split varContent on spaces too...
260 									foreach(ref r; ret)
261 										r ~= varContent;
262 								}
263 								idx = finalIndex; // will get ++'d next time through the for loop
264 								lastIndex = finalIndex + 1;
265 							}
266 						}
267 
268 						continue; // dollar sign standing alone is not something to expand
269 					}
270 
271 					if(quoteStyle == QuoteStyle.none) {
272 						if(ch == '{') {
273 							// expand like {a,b} stuff
274 							// FIXME
275 							foreach(ref r; ret)
276 								r ~= l[lastIndex .. idx];
277 
278 							int count = 0;
279 							size_t finalIndex;
280 							foreach(i2, ch2; l[idx .. $]) {
281 								if(ch2 == '{')
282 									count++;
283 								if(ch2 == '}')
284 									count--;
285 								if(count == 0) {
286 									finalIndex = idx + i2;
287 									break;
288 								}
289 							}
290 
291 							if(finalIndex == 0)
292 								throw new Exception("unclosed {");
293 
294 							auto expansionInnards = l[idx + 1 .. finalIndex];
295 
296 							lastIndex = finalIndex + 1; // skip the closing }
297 							idx = finalIndex;
298 
299 							auto origR = ret.length;
300 
301 							import arsd.string;
302 							string[] expandedTo = expansionInnards.split(",");
303 
304 							assert(expandedTo.length > 0);
305 
306 							// FIXME: bash expands all of the first ones before doing any of the next ones
307 							// do i want to do it that way too? or do i not care?
308 							// {a,b}{c,d}
309 							// i do      ac bc ad bd
310 							// bash does ac ad bc bd
311 
312 							// duplicate the original for each item beyond the first
313 							foreach(irrelevant; 0 .. expandedTo.length - 1)
314 								for(size_t i = 0; i < origR; i++)
315 									ret ~= ret[0].dup;
316 
317 							foreach(exp; 0 .. expandedTo.length)
318 								foreach(ref r; ret[origR * exp .. origR * (exp + 1)])
319 									r ~= expandedTo[exp];
320 
321 						} else if(ch == '~') {
322 							// expand home dir stuff
323 
324 							size_t finalIndex = idx + 1;
325 							while(finalIndex < l.length && isVarChar(l[finalIndex])) {
326 								finalIndex++;
327 							}
328 
329 							auto replacement = context.getUserHome(l[idx + 1 .. finalIndex]);
330 							if(replacement is null) {
331 								// no replacement done
332 							} else {
333 								foreach(ref r; ret)
334 									r ~= replacement;
335 								idx = finalIndex - 1;
336 								lastIndex = finalIndex;
337 							}
338 						}
339 					}
340 				}
341 				if(lastIndex)
342 					foreach(ref r; ret)
343 						r ~= l[lastIndex .. $];
344 				else if(ret.length == 1 && ret[0] is null) // was no expansion, reuse the original string
345 					ret[0] = l;
346 
347 				return ret;
348 			case QuoteStyle.nonExpanding:
349 				return [l];
350 		}
351 	}
352 }
353 
354 unittest {
355 	ShellContext context;
356 	context.mostRecentCommandStatus = 0;
357 	assert(ShellLexeme("$", QuoteStyle.none).toExpansions(context) == ["$"]); // stand alone = no replacement
358 	assert(ShellLexeme("$?", QuoteStyle.none).toExpansions(context) == ["0"]);
359 
360 	context.getUserHome = (username) => (username == "me" || username.length == 0) ? "/home/me" : null;
361 	assert(ShellLexeme("~", QuoteStyle.none).toExpansions(context) == ["/home/me"]);
362 	assert(ShellLexeme("~me", QuoteStyle.none).toExpansions(context) == ["/home/me"]);
363 	assert(ShellLexeme("~/lol", QuoteStyle.none).toExpansions(context) == ["/home/me/lol"]);
364 	assert(ShellLexeme("~me/lol", QuoteStyle.none).toExpansions(context) == ["/home/me/lol"]);
365 	assert(ShellLexeme("~other", QuoteStyle.none).toExpansions(context) == ["~other"]); // not found = no replacement
366 }
367 
368 /+
369 	/++
370 		The second thing should be have toSingleArg called on it
371 	+/
372 	EnvironmentPair toEnvironmentPair(ShellLexeme context) {
373 		assert(quoteStyle == QuoteStyle.none);
374 
375 		size_t splitPoint = l.length;
376 		foreach(size_t idx, char ch; l) {
377 			if(ch == '=') {
378 				splitPoint = idx;
379 				break;
380 			}
381 		}
382 
383 		if(splitPoint != l.length) {
384 			return EnvironmentPair(l[0 .. splitPoint], ShellLexeme(l[splitPoint + 1 .. $]));
385 		} else {
386 			return EnvironmentPair(null, ShellLexeme.init);
387 		}
388 	}
389 
390 	/++
391 		Expands variables but not globs while replacing quotes and such. Note it is NOT safe to pass an expanded single arg to another shell
392 	+/
393 	string toExpandedSingleArg(ShellContext context) {
394 		return l;
395 	}
396 
397 	/++
398 		Returns the value as an argv array, after shell expansion of variables, tildes, and globs
399 
400 		Does NOT attempt to execute `subcommands`.
401 	+/
402 	string[] toExpandedArgs(ShellContext context, Globber globber) {
403 		return null;
404 	}
405 +/
406 
407 /++
408 	This function in pure in all but formal annotation; it does not interact with the outside world.
409 +/
410 ShellLexeme[] lexShellCommandLine(string commandLine) {
411 	ShellLexeme[] ret;
412 
413 	enum State {
414 		consumingWhitespace,
415 		readingWord,
416 		readingSingleQuoted,
417 		readingEscaped,
418 		readingExpandingContextEscaped,
419 		readingDoubleQuoted,
420 		readingSpecialSymbol,
421 		// FIXME: readingSubcommand for `thing`
422 		readingComment,
423 	}
424 
425 	State state = State.consumingWhitespace;
426 	size_t first = commandLine.length;
427 
428 	void endWord() {
429 		state = State.consumingWhitespace;
430 		first = commandLine.length; // we'll rewind upon encountering the next word, if there is one
431 	}
432 
433 	foreach(size_t idx, char ch; commandLine) {
434 		again:
435 		final switch(state) {
436 			case State.consumingWhitespace:
437 				switch(ch) {
438 					case ' ', '\t', '\n':
439 						// the arg separators should all be collapsed to exactly one
440 						if(ret.length && !(ret[$-1].quoteStyle == QuoteStyle.none && ret[$-1].l == " "))
441 							ret ~= ShellLexeme(" ");
442 						continue;
443 					case '#':
444 						state = State.readingComment;
445 						continue;
446 					default:
447 						first = idx;
448 						state = State.readingWord;
449 						goto again;
450 				}
451 			case State.readingWord:
452 				switch(ch) {
453 					case '\'':
454 						if(first != idx)
455 							ret ~= ShellLexeme(commandLine[first .. idx]);
456 						first = idx + 1;
457 						state = State.readingSingleQuoted;
458 						break;
459 					case '\\':
460 						// a \ch can be treated as just a single quoted single char...
461 						if(first != idx)
462 							ret ~= ShellLexeme(commandLine[first .. idx]);
463 						first = idx + 1;
464 						state = State.readingEscaped;
465 						break;
466 					case '"':
467 						if(first != idx)
468 							ret ~= ShellLexeme(commandLine[first .. idx]);
469 						first = idx + 1;
470 						state = State.readingDoubleQuoted;
471 						break;
472 					case ' ':
473 						ret ~= ShellLexeme(commandLine[first .. idx]);
474 						ret ~= ShellLexeme(" "); // an argument separator
475 						endWord();
476 						continue;
477 					/+
478 					// single char special symbols
479 					case ';':
480 						if(first != idx)
481 							ret ~= ShellLexeme(commandLine[first .. idx]);
482 						ret ~= ShellLexeme(commandLine[idx .. idx + 1]);
483 						endWord();
484 						continue;
485 					break;
486 					+/
487 					// two-char special symbols
488 					case '|', '<', '>', '&', ';':
489 						if(first != idx)
490 							ret ~= ShellLexeme(commandLine[first .. idx]);
491 						first = idx;
492 						state = State.readingSpecialSymbol;
493 						break;
494 					default:
495 						// keep searching
496 				}
497 			break;
498 			case State.readingSpecialSymbol:
499 				switch(ch) {
500 					case '|', '<', '>', '&', ';':
501 						// include this as a two-char lexeme
502 						ret ~= ShellLexeme(commandLine[first .. idx + 1]);
503 						endWord();
504 						continue;
505 					default:
506 						// only include the previous char and send this back up
507 						ret ~= ShellLexeme(commandLine[first .. idx]);
508 						endWord();
509 						goto again;
510 				}
511 			break;
512 			case State.readingComment:
513 				if(ch == '\n') {
514 					endWord();
515 				}
516 			break;
517 			case State.readingSingleQuoted:
518 				switch(ch) {
519 					case '\'':
520 						ret ~= ShellLexeme(commandLine[first .. idx], QuoteStyle.nonExpanding);
521 						endWord();
522 					break;
523 					default:
524 				}
525 			break;
526 			case State.readingDoubleQuoted:
527 				switch(ch) {
528 					case '"':
529 						ret ~= ShellLexeme(commandLine[first .. idx], QuoteStyle.expanding);
530 						endWord();
531 					break;
532 					case '\\':
533 						state = State.readingExpandingContextEscaped;
534 						break;
535 					default:
536 				}
537 			break;
538 			case State.readingEscaped:
539 				if(ch >= 0x80 && ch <= 0xBF) {
540 					// continuation byte
541 					continue;
542 				} else if(first == idx) {
543 					// first byte, keep searching for continuations
544 					continue;
545 				} else {
546 					// same as if the user wrote the escaped character in single quotes
547 					ret ~= ShellLexeme(commandLine[first .. idx], QuoteStyle.nonExpanding);
548 
549 					if(state == State.readingExpandingContextEscaped) {
550 						state = State.readingDoubleQuoted;
551 						first = idx;
552 					} else {
553 						endWord();
554 					}
555 					goto again;
556 				}
557 			case State.readingExpandingContextEscaped:
558 				if(ch == '"') {
559 					// the -1 trims out the \
560 					ret ~= ShellLexeme(commandLine[first .. idx - 1], QuoteStyle.expanding);
561 					state = State.readingDoubleQuoted;
562 					first = idx; // we need to INCLUDE the " itself
563 				} else {
564 					// this was actually nothing special, the backslash is kept in the double quotes
565 					state = State.readingDoubleQuoted;
566 				}
567 			break;
568 		}
569 	}
570 
571 	if(first != commandLine.length) {
572 		if(state != State.readingWord && state != State.readingComment && state != State.readingSpecialSymbol)
573 			throw new Exception("ran out of data in inappropriate state");
574 		ret ~= ShellLexeme(commandLine[first .. $]);
575 	}
576 
577 	return ret;
578 }
579 
580 unittest {
581 	ShellLexeme[] got;
582 
583 	got = lexShellCommandLine("FOO=bar");
584 	assert(got.length == 1);
585 	assert(got[0].l == "FOO=bar");
586 
587 	// comments can only happen at whitespace contexts, not at the end of a single word
588 	got = lexShellCommandLine("FOO=bar#commentspam");
589 	assert(got.length == 1);
590 	assert(got[0].l == "FOO=bar#commentspam");
591 
592 	got = lexShellCommandLine("FOO=bar #commentspam");
593 	assert(got.length == 2);
594 	assert(got[0].l == "FOO=bar");
595 	assert(got[1].l == " "); // arg separator still there even tho there is no arg cuz of the comment, but that's semantic
596 
597 	got = lexShellCommandLine("#commentspam");
598 	assert(got.length == 0, got[0].l);
599 
600 	got = lexShellCommandLine("FOO=bar ./prog");
601 	assert(got.length == 3);
602 	assert(got[0].l == "FOO=bar");
603 	assert(got[1].l == " "); // argument separator
604 	assert(got[2].l == "./prog");
605 
606 	// all whitespace should be collapsed to a single argument separator
607 	got = lexShellCommandLine("FOO=bar          ./prog");
608 	assert(got.length == 3);
609 	assert(got[0].l == "FOO=bar");
610 	assert(got[1].l == " "); // argument separator
611 	assert(got[2].l == "./prog");
612 
613 	got = lexShellCommandLine("'foo'bar");
614 	assert(got.length == 2);
615 	assert(got[0].l == "foo");
616 	assert(got[0].quoteStyle == QuoteStyle.nonExpanding);
617 	assert(got[1].l == "bar");
618 	assert(got[1].quoteStyle == QuoteStyle.none);
619 
620 	// escaped single char works as if you wrote it in single quotes
621 	got = lexShellCommandLine("test\\'bar");
622 	assert(got.length == 3);
623 	assert(got[0].l == "test");
624 	assert(got[1].l == "'");
625 	assert(got[2].l == "bar");
626 
627 	// checking for utf-8 decode of escaped char
628 	got = lexShellCommandLine("test\\\&raquo;bar");
629 	assert(got.length == 3);
630 	assert(got[0].l == "test");
631 	assert(got[1].l == "\&raquo;");
632 	assert(got[2].l == "bar");
633 
634 	got = lexShellCommandLine(`"ok"`);
635 	assert(got.length == 1);
636 	assert(got[0].l == "ok");
637 	assert(got[0].quoteStyle == QuoteStyle.expanding);
638 
639 	got = lexShellCommandLine(`"ok\"after"`);
640 	assert(got.length == 2);
641 	assert(got[0].l == "ok");
642 	assert(got[0].quoteStyle == QuoteStyle.expanding);
643 	assert(got[1].l == "\"after");
644 	assert(got[1].quoteStyle == QuoteStyle.expanding);
645 
646 	got = lexShellCommandLine(`FOO=bar ./thing 'my ard' second_arg "quoted\"thing"`);
647 	assert(got.length == 10); // because quoted\"thing is two in this weird system
648 	assert(got[0].l == "FOO=bar");
649 	assert(got[1].l == " ");
650 	assert(got[2].l == "./thing");
651 	assert(got[3].l == " ");
652 	assert(got[4].l == "my ard");
653 	assert(got[5].l == " ");
654 	assert(got[6].l == "second_arg");
655 	assert(got[7].l == " ");
656 	assert(got[8].l == "quoted");
657 	assert(got[9].l == "\"thing");
658 
659 	got = lexShellCommandLine("a | b c");
660 	assert(got.length == 7);
661 
662 	got = lexShellCommandLine("a && b c");
663 	assert(got.length == 7);
664 
665 	got = lexShellCommandLine("a > b c");
666 	assert(got.length == 7);
667 
668 	got = lexShellCommandLine("a 2>&1 b c");
669 	assert(got.length == 9); // >& is also considered a special thing
670 
671 }
672 
673 struct ShellIo {
674 	enum Kind {
675 		inherit,
676 		fd,
677 		filename,
678 		pipedCommand,
679 		memoryBuffer
680 	}
681 
682 	Kind kind;
683 	int fd;
684 	string filename;
685 	ShellCommand pipedCommand;
686 
687 	bool append;
688 }
689 
690 class ShellCommand {
691 	ShellIo stdin;
692 	ShellIo stdout;
693 	ShellIo stderr;
694 	// yes i know in unix you can do other fds too. do i care?
695 
696 	string[] argv;
697 	EnvironmentPair[] environmentPairs;
698 
699 	string terminatingToken;
700 
701 	// set by the runners
702 	ShellContext* shellContext;
703 	private RunningCommand runningCommand;
704 	FilePath exePath; /// may be null in which case you might search or do built in, depending on the executor.
705 
706 	private SchedulableTask shellTask;
707 }
708 
709 /++
710 	A shell component - which is likely an argument, but that is a semantic distinction we can't make until parsing - may be made up of several lexemes. Think `foo'bar'`. This will extract them from the given array up to and including the next unquoted space or newline char.
711 +/
712 ShellLexeme[] nextComponent(ref ShellLexeme[] lexemes) {
713 	if(lexemes.length == 0)
714 		return lexemes[$ .. $];
715 
716 	int pos;
717 	while(
718 		pos < lexemes.length &&
719 		!(
720 			// identify an arg or command separator
721 			lexemes[pos].quoteStyle == QuoteStyle.none &&
722 			(
723 				lexemes[pos].l == " " ||
724 				lexemes[pos].l == ";" ||
725 				lexemes[pos].l == ";;" ||
726 				lexemes[pos].l == "&" ||
727 				lexemes[pos].l == "&&" ||
728 				lexemes[pos].l == "||" ||
729 				false
730 			)
731 		)
732 	) {
733 		pos++;
734 	}
735 
736 	if(pos == 0)
737 		pos++; // include the termination condition as its own component
738 
739 	auto ret = lexemes[0 .. pos];
740 	lexemes = lexemes[pos .. $];
741 
742 	return ret;
743 }
744 
745 struct EnvironmentPair {
746 	string environmentVariableName;
747 	string assignedValue;
748 
749 	string toString() {
750 		return environmentVariableName ~ "=" ~ assignedValue;
751 	}
752 }
753 
754 string expandSingleArg(ShellContext context, ShellLexeme[] lexeme) {
755 	string s;
756 	foreach(lex; lexeme) {
757 		auto expansions = lex.toExpansions(context);
758 		if(expansions.length != 1)
759 			throw new Exception("only single argument allowed here");
760 		s ~= expansions[0];
761 	}
762 	return s;
763 }
764 
765 /++
766 	Parses a set of lexemes into set of command objects.
767 
768 	This function in pure in all but formal annotation; it does not interact with the outside world, except through the globber delegate you provide (which should not make any changes to the outside world!).
769 +/
770 ShellCommand[] parseShellCommand(ShellLexeme[] lexemes, ShellContext context, Globber globber) {
771 	ShellCommand[] ret;
772 
773 	ShellCommand currentCommand;
774 	ShellCommand firstCommand;
775 
776 	enum ParseState {
777 		lookingForVarAssignment,
778 		lookingForArg,
779 		lookingForStdinFilename,
780 		lookingForStdoutFilename,
781 		lookingForStderrFilename,
782 	}
783 	ParseState parseState = ParseState.lookingForVarAssignment;
784 
785 	commandLoop: while(lexemes.length) {
786 		auto component = nextComponent(lexemes);
787 		if(component.length) {
788 			/+
789 				Command syntax in bash is basically:
790 
791 				Zero or more `ENV=value` sets, separated by whitespace, followed by zero or more arg things.
792 				OR
793 				a shell builtin which does special things to the rest of the command, and may even require subsequent commands
794 
795 				Argv[0] can be a shell built in which reads the rest of argv separately. It may even require subsequent commands!
796 
797 				For some shell built in keywords, you should not actually do expansion:
798 					$ for $i in one two; do ls $i; done
799 					bash: `$i': not a valid identifier
800 
801 				So there must be some kind of intermediate representation of possible expansions.
802 
803 
804 				BUT THIS IS MY SHELL I CAN DO WHAT I WANT!!!!!!!!!!!!
805 
806 				shell the vars are ... not recursively expanded, it is just already expanded at assignment
807 			+/
808 
809 			bool thisWasEnvironmentPair = false;
810 			EnvironmentPair environmentPair;
811 			bool thisWasRedirection = false;
812 			bool thisWasPipe = false;
813 			ShellLexeme[] arg;
814 
815 			if(component.length == 0) {
816 				// nothing left, should never happen
817 				break;
818 			}
819 			if(component.length == 1) {
820 				if(component[0].quoteStyle == QuoteStyle.none && component[0].l == " ") {
821 					// just an arg separator
822 					continue;
823 				}
824 			}
825 
826 			if(currentCommand is null)
827 				currentCommand = new ShellCommand();
828 			if(firstCommand is null)
829 				firstCommand = currentCommand;
830 
831 			foreach(lexeme; component) {
832 				again:
833 				final switch(parseState) {
834 					case ParseState.lookingForVarAssignment:
835 						if(thisWasEnvironmentPair) {
836 							arg ~= lexeme;
837 						} else {
838 							// assume there is no var until we prove otherwise
839 							parseState = ParseState.lookingForArg;
840 							if(lexeme.quoteStyle == QuoteStyle.none) {
841 								foreach(idx, ch; lexeme.l) {
842 									if(ch == '=') {
843 										// actually found one!
844 										thisWasEnvironmentPair = true;
845 										environmentPair.environmentVariableName = lexeme.l[0 .. idx];
846 										arg ~= ShellLexeme(lexeme.l[idx + 1 .. $], QuoteStyle.none);
847 										parseState = ParseState.lookingForVarAssignment;
848 									}
849 								}
850 							}
851 
852 							if(parseState == ParseState.lookingForArg)
853 								goto case;
854 						}
855 					break;
856 					case ParseState.lookingForArg:
857 						if(lexeme.quoteStyle == QuoteStyle.none) {
858 							if(lexeme.l == "<" || lexeme.l == ">" || lexeme.l == ">>" || lexeme.l == ">&")
859 								thisWasRedirection = true;
860 							if(lexeme.l == "|")
861 								thisWasPipe = true;
862 							if(lexeme.l == ";" || lexeme.l == ";;" || lexeme.l == "&" || lexeme.l == "&&" || lexeme.l == "||") {
863 								if(firstCommand) {
864 									firstCommand.terminatingToken = lexeme.l;
865 									ret ~= firstCommand;
866 								}
867 								firstCommand = null;
868 								currentCommand = null;
869 								continue commandLoop;
870 							}
871 						}
872 						arg ~= lexeme;
873 					break;
874 					case ParseState.lookingForStdinFilename:
875 					case ParseState.lookingForStdoutFilename:
876 					case ParseState.lookingForStderrFilename:
877 						if(lexeme.quoteStyle == QuoteStyle.none) {
878 							if(lexeme.l == "<" || lexeme.l == ">")
879 								throw new Exception("filename needed, not a redirection");
880 							if(lexeme.l == "|")
881 								throw new Exception("filename needed, not a pipe");
882 						}
883 						arg ~= lexeme;
884 					break;
885 				}
886 			}
887 
888 			switch(parseState) {
889 				case ParseState.lookingForStdinFilename:
890 					currentCommand.stdin.filename = expandSingleArg(context, arg);
891 					parseState = ParseState.lookingForArg;
892 				continue;
893 				case ParseState.lookingForStdoutFilename:
894 					currentCommand.stdout.filename = expandSingleArg(context, arg);
895 					parseState = ParseState.lookingForArg;
896 				continue;
897 				case ParseState.lookingForStderrFilename:
898 					currentCommand.stderr.filename = expandSingleArg(context, arg);
899 					parseState = ParseState.lookingForArg;
900 				continue;
901 				default:
902 					break;
903 			}
904 
905 			if(thisWasEnvironmentPair) {
906 				environmentPair.assignedValue = expandSingleArg(context, arg);
907 				currentCommand.environmentPairs ~= environmentPair;
908 			} else if(thisWasRedirection) {
909 				// FIXME: read the fd off this arg
910 				// FIXME: read the filename off the next arg, new parse state
911 				//assert(0, component);
912 
913 				string cmd;
914 				foreach(item; component)
915 					cmd ~= item.l;
916 
917 				switch(cmd) {
918 					case ">":
919 					case ">>":
920 						if(currentCommand.stdout.kind != ShellIo.Kind.inherit)
921 							throw new Exception("command has already been redirected");
922 						currentCommand.stdout.kind = ShellIo.Kind.filename;
923 						if(cmd == ">>")
924 							currentCommand.stdout.append = true;
925 						parseState = ParseState.lookingForStdoutFilename;
926 					break;
927 					case "2>":
928 					case "2>>":
929 						if(currentCommand.stderr.kind != ShellIo.Kind.inherit)
930 							throw new Exception("command has already had stderr redirected");
931 						currentCommand.stderr.kind = ShellIo.Kind.filename;
932 						if(cmd == "2>>")
933 							currentCommand.stderr.append = true;
934 						parseState = ParseState.lookingForStderrFilename;
935 					break;
936 					case "2>&1":
937 						if(currentCommand.stderr.kind != ShellIo.Kind.inherit)
938 							throw new Exception("command has already had stderr redirected");
939 						currentCommand.stderr.kind = ShellIo.Kind.fd;
940 						currentCommand.stderr.fd = 1;
941 					break;
942 					case "<":
943 						if(currentCommand.stdin.kind != ShellIo.Kind.inherit)
944 							throw new Exception("command has already had stdin assigned");
945 						currentCommand.stdin.kind = ShellIo.Kind.filename;
946 						parseState = ParseState.lookingForStdinFilename;
947 					break;
948 					default:
949 						throw new Exception("bad redirection try adding spaces around parts of " ~ cmd);
950 				}
951 			} else if(thisWasPipe) {
952 				// FIXME: read the fd? i kinda wanna support 2| and such
953 				auto newCommand = new ShellCommand();
954 				currentCommand.stdout.kind = ShellIo.Kind.pipedCommand;
955 				currentCommand.stdout.pipedCommand = newCommand;
956 				newCommand.stdin.kind = ShellIo.Kind.pipedCommand;
957 				newCommand.stdin.pipedCommand = currentCommand;
958 
959 				currentCommand = newCommand;
960 			} else {
961 				currentCommand.argv ~= globber(arg, context);
962 			}
963 		}
964 	}
965 
966 	if(firstCommand)
967 		ret ~= firstCommand;
968 
969 	return ret;
970 }
971 
972 unittest {
973 	string[] globber(ShellLexeme[] s, ShellContext context) {
974 		string g;
975 		foreach(l; s)
976 			g ~= l.toExpansions(context)[0];
977 		return [g];
978 	}
979 	ShellContext context;
980 	ShellCommand[] commands;
981 
982 	commands = parseShellCommand(lexShellCommandLine("foo bar"), context, &globber);
983 	assert(commands.length == 1);
984 	assert(commands[0].argv.length == 2);
985 	assert(commands[0].argv[0] == "foo");
986 	assert(commands[0].argv[1] == "bar");
987 
988 	commands = parseShellCommand(lexShellCommandLine("foo bar'baz'"), context, &globber);
989 	assert(commands.length == 1);
990 	assert(commands[0].argv.length == 2);
991 	assert(commands[0].argv[0] == "foo");
992 	assert(commands[0].argv[1] == "barbaz");
993 
994 }
995 
996 /+
997 interface OSInterface {
998 	setEnv
999 	getEnv
1000 	getAllEnv
1001 
1002 	runCommand
1003 	waitForCommand
1004 }
1005 +/
1006 
1007 version(Windows) {
1008 	import core.sys.windows.windows;
1009 	HANDLE duplicate(HANDLE handle) {
1010 		HANDLE n;
1011 		// FIXME: check for error
1012 		DuplicateHandle(
1013 			GetCurrentProcess(),
1014 			handle,
1015 			GetCurrentProcess(),
1016 			&n,
1017 			0,
1018 			false,
1019 			DUPLICATE_SAME_ACCESS
1020 		);
1021 		return n;
1022 	}
1023 }
1024 version(Posix) {
1025 	import unistd = core.sys.posix.unistd;
1026 	alias HANDLE = int;
1027 	int CloseHandle(HANDLE fd) {
1028 		import core.sys.posix.unistd;
1029 		return close(fd);
1030 	}
1031 
1032 	HANDLE duplicate(HANDLE fd) {
1033 		import unix = core.sys.posix.unistd;
1034 		auto n = unix.dup(fd);
1035 		// FIXME: check for error
1036 		setCloExec(n);
1037 		return n;
1038 	}
1039 }
1040 
1041 struct CommandRunningContext {
1042 	// FIXME: environment?
1043 
1044 	HANDLE stdin;
1045 	HANDLE stdout;
1046 	HANDLE stderr;
1047 
1048 	int pgid;
1049 }
1050 
1051 class Shell {
1052 	protected ShellContext context;
1053 
1054 	bool exitRequested() {
1055 		return context.exitRequested;
1056 	}
1057 
1058 	this() {
1059 		setCommandExecutors([
1060 			// providing for, set, export, cd, etc
1061 			cast(Shell.CommandExecutor) new ShellControlExecutor(),
1062 			// runs external programs
1063 			cast(Shell.CommandExecutor) new ExternalCommandExecutor(),
1064 			// runs built-in simplified versions of some common commands
1065 			cast(Shell.CommandExecutor) new CoreutilFallbackExecutor()
1066 		]);
1067 
1068 		context.getEnvironmentVariable = toDelegate(&getEnvironmentVariable);
1069 		context.getUserHome = toDelegate(&getUserHome);
1070 
1071 		context.scriptArgs = ["one 1", "two 2", "three 3"];
1072 	}
1073 
1074 	static private string getUserHome(scope const(char)[] user) {
1075 		if(user.length == 0) {
1076 			import core.stdc.stdlib;
1077 			version(Windows)
1078 				return (stringz(getenv("HOMEDRIVE")).borrow ~ stringz(getenv("HOMEPATH")).borrow).idup;
1079 			else
1080 				return (stringz(getenv("HOME")).borrow).idup;
1081 		}
1082 		// FIXME: look it up from the OS
1083 		return null;
1084 	}
1085 
1086 
1087 	public string prompt() {
1088 		return "[deesh]" ~ getCurrentWorkingDirectory().toString() ~ "$ ";
1089 	}
1090 
1091 	/++
1092 		Expands shell input with filename wildcards into a list of matching filenames or unmodified names in the shell's current context.
1093 	+/
1094 	protected string[] glob(ShellLexeme[] ls, ShellContext context) {
1095 		if(ls.length == 0)
1096 			return null;
1097 
1098 		static struct Helper {
1099 			string[] expansions;
1100 			bool mayHaveSpecialCharInterpretation;
1101 			Helper* next;
1102 		}
1103 
1104 		Helper[] expansions;
1105 		expansions.length = ls.length;
1106 		foreach(idx, ref expansion; expansions)
1107 			expansion = Helper(ls[idx].toExpansions(context), ls[0].quoteStyle == QuoteStyle.none, idx + 1 < expansions.length ? &expansions[idx + 1] : null);
1108 
1109 		string[] helper(Helper* h) {
1110 			import arsd.string;
1111 			string[] ret;
1112 			foreach(exp; h.expansions) {
1113 				if(h.next)
1114 				foreach(next; helper(h.next))
1115 					ret ~= (h.mayHaveSpecialCharInterpretation ? replace(exp, "*", "\xff") : exp) ~ next;
1116 				else
1117 					ret ~= h.mayHaveSpecialCharInterpretation ? replace(exp, "*", "\xff") : exp;
1118 			}
1119 			return ret;
1120 		}
1121 
1122 		string[] res = helper(&expansions[0]);
1123 
1124 		string[] ret;
1125 		foreach(ref r; res) {
1126 			bool needsGlob;
1127 			foreach(ch; r) {
1128 				if(ch == 0xff) {
1129 					needsGlob = true;
1130 					break;
1131 				}
1132 			}
1133 
1134 			if(needsGlob) {
1135 				string[] matchingFiles;
1136 
1137 				// FIXME: wrong dir if there's a slash in the pattern...
1138 				getFiles(".", (string name, bool isDirectory) {
1139 					if(name.length && name[0] == '.' && (r.length == 0 || r[0] != '.'))
1140 						return; // skip hidden unless specifically requested
1141 					if(name.matchesFilePattern(r, '\xff'))
1142 						matchingFiles ~= name;
1143 
1144 				});
1145 
1146 				if(matchingFiles.length == 0) {
1147 					import arsd.string;
1148 					ret ~= r.replace("\xff", "*");
1149 				} else {
1150 					ret ~= matchingFiles;
1151 				}
1152 			} else {
1153 				ret ~= r;
1154 			}
1155 		}
1156 
1157 		return ret;
1158 	}
1159 
1160 	private final string[] globberForwarder(ShellLexeme[] ls, ShellContext context) {
1161 		return glob(ls, context);
1162 	}
1163 
1164 	/++
1165 		Sets the command runners for this shell. It will try each executor in the order given, running the first that can succeed. If none can, it will issue a command not found error.
1166 
1167 		I suggest you try
1168 
1169 		---
1170 		setCommandExecutors([
1171 			// providing for, set, export, cd, etc
1172 			new ShellControlExecutor(),
1173 			// runs external programs
1174 			new ExternalCommandExecutor(),
1175 			// runs built-in simplified versions of some common commands
1176 			new CoreutilFallbackExecutor()
1177 		]);
1178 		---
1179 
1180 		If you are writing your own executor, you should generally not match on any command that includes a slash, thus reserving those full paths for the external command executor.
1181 	+/
1182 	public void setCommandExecutors(CommandExecutor[] commandExecutors) {
1183 		this.commandExecutors = commandExecutors;
1184 	}
1185 
1186 	private CommandExecutor[] commandExecutors;
1187 
1188 	static interface CommandExecutor {
1189 		/++
1190 			Returns the condition if this executor will try to run the command.
1191 
1192 			Tip: when implementing, if there is a slash in the argument, you should generally not attempt to match unless you are implementing an external command runner.
1193 
1194 			Returns:
1195 				[MatchesResult.no] if this executor never matches the given command.
1196 
1197 				[MatchesResult.yes] if this executor always matches the given command. If it is unable to run it, including for cases like file not found or file not executable, this is an error and it will not attempt to fall back to the next executor.
1198 
1199 				[MatchesResult.yesIfSearchSucceeds] means the shell should call [searchPathForCommand] before proceeding. If `searchPathForCommand` returns `FilePath(null)`, the shell will try the next executor. For any other return, it will try to run the command, storing the result in `command.exePath`.
1200 		+/
1201 		MatchesResult matches(string arg0);
1202 
1203 		/// ditto
1204 		enum MatchesResult {
1205 			no,
1206 			yes,
1207 			yesIfSearchSucceeds
1208 		}
1209 		/++
1210 			Returns the [FilePath] to be executed by the command, if there is one. Should be `FilePath(null)` if it does not match or does not use an external file.
1211 		+/
1212 		FilePath searchPathForCommand(string arg0);
1213 		/++
1214 
1215 		+/
1216 		RunningCommand startCommand(ShellCommand command, CommandRunningContext crc);
1217 
1218 		/++
1219 		string[] completionCandidatesForCommandName(string arg0);
1220 		string[] completionCandidatesForArgument(string[] args
1221 		+/
1222 	}
1223 
1224 	void dumpCommand(ShellCommand command, bool includeNl = true) {
1225 		foreach(ep; command.environmentPairs)
1226 			writeln(ep.toString());
1227 
1228 		writeStdout(command.argv);
1229 
1230 		final switch(command.stdin.kind) {
1231 			case ShellIo.Kind.inherit:
1232 			case ShellIo.Kind.memoryBuffer:
1233 			case ShellIo.Kind.pipedCommand:
1234 			break;
1235 			case ShellIo.Kind.fd:
1236 				writeStdout(" <", command.stdin.fd);
1237 			break;
1238 			case ShellIo.kind.filename:
1239 				writeStdout(" < ", command.stdin.filename);
1240 		}
1241 		final switch(command.stderr.kind) {
1242 			case ShellIo.Kind.inherit:
1243 			case ShellIo.Kind.memoryBuffer:
1244 			break;
1245 			case ShellIo.Kind.fd:
1246 				writeStdout(" 2>&", command.stderr.fd);
1247 			break;
1248 			case ShellIo.kind.filename:
1249 				writeStdout(command.stderr.append ? " 2>> " : " 2> ", command.stderr.filename);
1250 			break;
1251 			case ShellIo.Kind.pipedCommand:
1252 				writeStderr(" 2| ");
1253 				dumpCommand(command.stderr.pipedCommand, false);
1254 			break;
1255 		}
1256 		final switch(command.stdout.kind) {
1257 			case ShellIo.Kind.inherit:
1258 			case ShellIo.Kind.memoryBuffer:
1259 			break;
1260 			case ShellIo.Kind.fd:
1261 				writeStdout(" >&", command.stdout.fd);
1262 			break;
1263 			case ShellIo.kind.filename:
1264 				writeStdout(command.stdout.append ? " >> " : " > ", command.stdout.filename);
1265 			break;
1266 			case ShellIo.Kind.pipedCommand:
1267 				writeStdout(" | ");
1268 				dumpCommand(command.stdout.pipedCommand, false);
1269 			break;
1270 		}
1271 
1272 		writeStdout(command.terminatingToken);
1273 
1274 		writeln();
1275 	}
1276 
1277 	static struct WaitResult {
1278 		enum Change {
1279 			stop,
1280 			resume,
1281 			complete
1282 		}
1283 		Change change;
1284 		int status;
1285 	}
1286 	WaitResult waitForCommand(ShellCommand command) {
1287 		//command.runningCommand.waitForChange();
1288 		command.runningCommand.waitForChange();
1289 		if(auto cmd = command.stdout.pipedCommand) {
1290 			waitForCommand(cmd);
1291 		}
1292 		if(command.runningCommand.isComplete)
1293 			return WaitResult(WaitResult.Change.complete, command.runningCommand.status);
1294 		else if(command.runningCommand.isStopped)
1295 			return WaitResult(WaitResult.Change.stop, command.runningCommand.status);
1296 		else
1297 			return WaitResult(WaitResult.Change.resume, command.runningCommand.status);
1298 	}
1299 
1300 	package RunningCommand startCommand(ShellCommand command, CommandRunningContext crc) {
1301 		if(command.argv.length == 0)
1302 			throw new Exception("empty command");
1303 
1304 		CommandExecutor matchingExecutor;
1305 		executorLoop: foreach(executor; commandExecutors) {
1306 			final switch(executor.matches(command.argv[0])) {
1307 				case CommandExecutor.MatchesResult.no:
1308 					continue;
1309 				case CommandExecutor.MatchesResult.yesIfSearchSucceeds:
1310 					auto result = executor.searchPathForCommand(command.argv[0]);
1311 					if(result.isNull())
1312 						continue;
1313 					command.exePath = result;
1314 					goto case;
1315 				case CommandExecutor.MatchesResult.yes:
1316 					matchingExecutor = executor;
1317 					break executorLoop;
1318 			}
1319 		}
1320 		if(matchingExecutor is null)
1321 			throw new Exception("command not found");
1322 
1323 		command.shellContext = &context;
1324 
1325 		HANDLE[2] pipes;
1326 		File[3] redirections;
1327 
1328 		// FIXME: if it is a memory buffer we want the pipe too, just we will read the other side of it
1329 
1330 		final switch(command.stdin.kind) {
1331 			case ShellIo.Kind.pipedCommand:
1332 				// do nothing, set up from the pipe origin
1333 			break;
1334 			case ShellIo.Kind.inherit:
1335 				// nothing here, will be set with stdout blow
1336 			break;
1337 			case ShellIo.kind.filename:
1338 				redirections[0] = new File(FilePath(command.stdin.filename), File.OpenMode.readOnly);
1339 				crc.stdin = redirections[0].nativeHandle;
1340 
1341 				version(Windows)
1342 				if(!SetHandleInformation(crc.stdin, 1/*HANDLE_FLAG_INHERIT*/, 1))
1343 					throw new WindowsApiException("SetHandleInformation", GetLastError());
1344 			break;
1345 			case ShellIo.Kind.memoryBuffer:
1346 				throw new NotYetImplementedException("stdin redirect from mem not implemented");
1347 			break;
1348 			case ShellIo.Kind.fd:
1349 				throw new NotYetImplementedException("stdin redirect from fd not implemented");
1350 			break;
1351 		}
1352 
1353 		final switch(command.stdout.kind) {
1354 			case ShellIo.Kind.inherit:
1355 				pipes[0] = crc.stdin;
1356 				pipes[1] = crc.stdout;
1357 			break;
1358 			case ShellIo.Kind.memoryBuffer:
1359 				throw new NotYetImplementedException("stdout redirect to mem not implemented");
1360 			break;
1361 			case ShellIo.Kind.fd:
1362 				throw new NotYetImplementedException("stdout redirect to fd not implemented");
1363 			break;
1364 			case ShellIo.kind.filename:
1365 				pipes[0] = crc.stdin;
1366 				redirections[1] = new File(FilePath(command.stdout.filename), command.stdout.append ? File.OpenMode.appendOnly : File.OpenMode.writeWithTruncation);
1367 				pipes[1] = redirections[1].nativeHandle;
1368 
1369 				version(Windows)
1370 				if(!SetHandleInformation(pipes[1], 1/*HANDLE_FLAG_INHERIT*/, 1))
1371 					throw new WindowsApiException("SetHandleInformation", GetLastError());
1372 			break;
1373 			case ShellIo.Kind.pipedCommand:
1374 				assert(command.stdout.pipedCommand);
1375 				version(Posix) {
1376 					import core.sys.posix.unistd;
1377 					auto ret = pipe(pipes);
1378 
1379 					setCloExec(pipes[0]);
1380 					setCloExec(pipes[1]);
1381 
1382 					import core.stdc.errno;
1383 
1384 					if(ret == -1)
1385 						throw new ErrnoApiException("stdin pipe", errno);
1386 				} else version(Windows) {
1387 					SECURITY_ATTRIBUTES saAttr;
1388 					saAttr.nLength = SECURITY_ATTRIBUTES.sizeof;
1389 					saAttr.bInheritHandle = true;
1390 					saAttr.lpSecurityDescriptor = null;
1391 
1392 					if(MyCreatePipeEx(&pipes[0], &pipes[1], &saAttr, 0, 0, 0 /* flags */) == 0)
1393 						throw new WindowsApiException("CreatePipe", GetLastError());
1394 
1395 					// don't inherit the read side for the first process
1396 					if(!SetHandleInformation(pipes[0], 1/*HANDLE_FLAG_INHERIT*/, 0))
1397 						throw new WindowsApiException("SetHandleInformation", GetLastError());
1398 				}
1399 			break;
1400 		}
1401 
1402 		auto original_stderr = crc.stderr;
1403 		final switch(command.stderr.kind) {
1404 			case ShellIo.Kind.pipedCommand:
1405 				throw new NotYetImplementedException("stderr redirect to pipe not implemented");
1406 			break;
1407 			case ShellIo.Kind.inherit:
1408 				// nothing here, just keep it
1409 			break;
1410 			case ShellIo.kind.filename:
1411 				redirections[2] = new File(FilePath(command.stderr.filename), command.stderr.append ? File.OpenMode.appendOnly : File.OpenMode.writeWithTruncation);
1412 				crc.stderr = redirections[2].nativeHandle;
1413 			break;
1414 			case ShellIo.Kind.memoryBuffer:
1415 				throw new NotYetImplementedException("stderr redirect to mem not implemented");
1416 			break;
1417 			case ShellIo.Kind.fd:
1418 				assert(command.stderr.fd == 1);
1419 				crc.stderr = duplicate(pipes[1]);
1420 				redirections[2] = new File(crc.stderr); // so we can close it easily later
1421 
1422 				version(Windows)
1423 				if(!SetHandleInformation(crc.stderr, 1/*HANDLE_FLAG_INHERIT*/, 1))
1424 					throw new WindowsApiException("SetHandleInformation", GetLastError());
1425 			break;
1426 		}
1427 
1428 		auto proc = matchingExecutor.startCommand(command, CommandRunningContext(crc.stdin, pipes[1], crc.stderr, crc.pgid));
1429 		assert(command.runningCommand is proc);
1430 
1431 		version(Windows) {
1432 			// can't inherit stdin or modified stderr again beyond this
1433 			if(redirections[0] && !SetHandleInformation(redirections[0].nativeHandle, 1/*HANDLE_FLAG_INHERIT*/, 0))
1434 				throw new WindowsApiException("SetHandleInformation", GetLastError());
1435 			if(redirections[2] && !SetHandleInformation(redirections[2].nativeHandle, 1/*HANDLE_FLAG_INHERIT*/, 0))
1436 				throw new WindowsApiException("SetHandleInformation", GetLastError());
1437 		}
1438 
1439 		if(command.stdout.pipedCommand) {
1440 			version(Windows) {
1441 				// but swap inheriting for the second one
1442 				if(!SetHandleInformation(pipes[0], 1/*HANDLE_FLAG_INHERIT*/, 1))
1443 					throw new WindowsApiException("SetHandleInformation", GetLastError());
1444 				if(!SetHandleInformation(pipes[1], 1/*HANDLE_FLAG_INHERIT*/, 0))
1445 					throw new WindowsApiException("SetHandleInformation", GetLastError());
1446 			}
1447 
1448 			startCommand(command.stdout.pipedCommand, CommandRunningContext(pipes[0], crc.stdout, original_stderr, crc.pgid ? crc.pgid : proc.pid));
1449 
1450 			// we're done with them now, important to close so the receiving program doesn't think more data might be coming down the pipe
1451 			// but if we pass it to a built in command in a thread, it needs to remain... maybe best to duplicate the handle in that case.
1452 			CloseHandle(pipes[0]); // FIXME: check for error?
1453 			CloseHandle(pipes[1]);
1454 		}
1455 
1456 		foreach(ref r; redirections) {
1457 			if(r)
1458 				r.close();
1459 			r = null;
1460 		}
1461 
1462 		return proc;
1463 	}
1464 
1465 	public int executeScript(string commandLine) {
1466 		auto fiber = executeInteractiveCommand(commandLine);
1467 		assert(fiber is null);
1468 		return context.mostRecentCommandStatus;
1469 	}
1470 
1471 	public SchedulableTask executeInteractiveCommand(string commandLine) {
1472 		SchedulableTask fiber;
1473 		bool backgrounded;
1474 		fiber = new SchedulableTask(() {
1475 
1476 		ShellCommand[] commands;
1477 
1478 		try {
1479 			commands = parseShellCommand(lexShellCommandLine(commandLine), context, &globberForwarder);
1480 		} catch(ArsdExceptionBase e) {
1481 			string more;
1482 			e.getAdditionalPrintableInformation((string name, in char[] value) {
1483 				more ~= ", ";
1484 				more ~= name ~ ": " ~ value;
1485 			});
1486 			writelnStderr("deesh: ", e.message, more);
1487 		} catch(Exception e) {
1488 			writelnStderr("deesh: ", e.msg);
1489 		}
1490 
1491 		bool aborted;
1492 		bool skipToNextStatement;
1493 		int errorLevel;
1494 
1495 		commandLoop: foreach(command; commands)
1496 		try {
1497 			if(context.exitRequested)
1498 				return;
1499 
1500 			if(aborted) {
1501 				writelnStderr("Execution aborted");
1502 				break;
1503 			}
1504 			if(skipToNextStatement) {
1505 				switch(command.terminatingToken) {
1506 					case "", ";", "&":
1507 						skipToNextStatement = false;
1508 						if(errorLevel)
1509 							aborted = true;
1510 						continue commandLoop;
1511 					case ";;":
1512 						skipToNextStatement = false;
1513 						continue commandLoop;
1514 					default:
1515 						assert(0);
1516 				}
1517 			}
1518 
1519 			if(command.argv[0] in context.aliases) {
1520 				command.argv = context.aliases[command.argv[0]] ~ command.argv[1 .. $];
1521 			}
1522 
1523 			debug dumpCommand(command);
1524 
1525 			version(Posix) {
1526 				auto crc = CommandRunningContext(0, 1, 2, 0);
1527 			} else version(Windows) {
1528 				auto crc = CommandRunningContext(GetStdHandle(STD_INPUT_HANDLE), GetStdHandle(STD_OUTPUT_HANDLE), GetStdHandle(STD_ERROR_HANDLE));
1529 			}
1530 
1531 			auto proc = this.startCommand(command, crc);
1532 
1533 			if(command.terminatingToken == "&") {
1534 				context.jobs ~= command;
1535 				command.shellTask = fiber;
1536 				backgrounded = true;
1537 				Fiber.yield();
1538 				goto waitMore;
1539 			} else {
1540 				waitMore:
1541 				proc.makeForeground();
1542 				auto waitResult = waitForCommand(command);
1543 				final switch(waitResult.change) {
1544 					case WaitResult.Change.complete:
1545 						break;
1546 					case WaitResult.Change.stop:
1547 						command.shellTask = fiber;
1548 						context.jobs ~= command;
1549 						reassertControlOfTerminal();
1550 						Fiber.yield();
1551 						goto waitMore;
1552 					case WaitResult.Change.resume:
1553 						goto waitMore;
1554 				}
1555 
1556 				auto cmdStatus = waitResult.status;
1557 
1558 				errorLevel = cmdStatus;
1559 				context.mostRecentCommandStatus = cmdStatus;
1560 				reassertControlOfTerminal();
1561 
1562 				switch(command.terminatingToken) {
1563 					case "", ";":
1564 						// by default, throw if the command failed
1565 						if(cmdStatus)
1566 							aborted = true;
1567 					break;
1568 					case "||":
1569 						// if this command succeeded, we skip the rest of this block to the next ;, ;;, or &
1570 						// if it failed, we run the next command
1571 						if(cmdStatus == 0)
1572 							skipToNextStatement = true;
1573 					break;
1574 					case "&&":
1575 						// opposite of ||, if this command *fails*, we proceed
1576 						if(cmdStatus != 0)
1577 							skipToNextStatement = true;
1578 					break;
1579 					case ";;": // on error resume next, let the script inspect
1580 						aborted = false;
1581 					break;
1582 					case "&":
1583 						// handled elsewhere
1584 						break;
1585 					default:
1586 						throw new Exception("invalid command terminator: " ~ command.terminatingToken);
1587 				}
1588 			}
1589 
1590 		} catch(ArsdExceptionBase e) {
1591 			string more;
1592 			e.getAdditionalPrintableInformation((string name, in char[] value) {
1593 				more ~= ", ";
1594 				more ~= name ~ ": " ~ value;
1595 			});
1596 			writelnStderr("deesh: ", command.argv.length ? command.argv[0] : "", ": ", e.message, more);
1597 		} catch(Exception e) {
1598 			writelnStderr("deesh: ", command.argv.length ? command.argv[0] : "", ": ", e.msg);
1599 		}
1600 		});
1601 
1602 		fiber.call();
1603 
1604 		if(fiber.state == Fiber.State.HOLD) {
1605 			if(backgrounded) {
1606 				// user typed &, they should know
1607 			} else {
1608 				writeStdout("Stopped");
1609 			}
1610 		}
1611 
1612 		auto fg = context.jobToForeground;
1613 		context.jobToForeground = null;
1614 		return fg;
1615 	}
1616 
1617 	bool pendingJobs() {
1618 		return context.jobs.length > 0;
1619 	}
1620 
1621 	void reassertControlOfTerminal() {
1622 		version(Posix) {
1623 			import core.sys.posix.unistd;
1624 			import core.sys.posix.signal;
1625 
1626 			// reassert control of the tty to the shell
1627 			ErrnoEnforce!tcsetpgrp(1, getpid());
1628 		}
1629 	}
1630 }
1631 
1632 class RunningCommand {
1633 	void waitForChange() {}
1634 	int status() { return 0; }
1635 	void makeForeground() {}
1636 
1637 	int pid() { return 0; }
1638 
1639 	abstract bool isComplete();
1640 	bool isStopped() { return false; }
1641 }
1642 
1643 class ExternalProcessWrapper : RunningCommand {
1644 	ExternalProcess proc;
1645 	this(ExternalProcess proc) {
1646 		this.proc = proc;
1647 	}
1648 
1649 	override void waitForChange() {
1650 		this.proc.waitForChange();
1651 	}
1652 
1653 	override int status() {
1654 		return this.proc.status;
1655 	}
1656 
1657 	override void makeForeground() {
1658 		// FIXME: save/restore terminal state associated with shell and this process too
1659 		version(Posix) {
1660 			assert(proc.pid > 0);
1661 			import core.sys.posix.unistd;
1662 			import core.sys.posix.signal;
1663 			// put the child group in control of the tty
1664 			ErrnoEnforce!tcsetpgrp(1, proc.pid);
1665 			// writeln(proc.pid);
1666 			kill(-proc.pid, SIGCONT); // and if it beat us to the punch and is waiting, go ahead and wake it up (this is harmless if it is already running)
1667 		}
1668 	}
1669 
1670 	override int pid() { version(Posix) return proc.pid; else return 0; }
1671 
1672 	override bool isStopped() { return proc.isStopped; }
1673 	override bool isComplete() { return proc.isComplete; }
1674 }
1675 class ExternalCommandExecutor : Shell.CommandExecutor {
1676 	MatchesResult matches(string arg0) {
1677 		if(arg0.indexOf("/") != -1)
1678 			return MatchesResult.yes;
1679 		return MatchesResult.yesIfSearchSucceeds;
1680 	}
1681 	FilePath searchPathForCommand(string arg0) {
1682 		if(arg0.indexOf("/") != -1)
1683 			return FilePath(arg0);
1684 		// could also be built-ins and cmdlets...
1685 		// and on Windows we should check .exe, maybe .com, .bat, .cmd but note these need to be called through cmd.exe as the process and do a -c arg so maybe i won't allow it.
1686 
1687 		// so if .exe is not there i should add it.
1688 
1689 		string exeName;
1690 		version(Posix)
1691 			exeName = arg0;
1692 		else version(Windows) {
1693 			exeName = arg0;
1694 			if(exeName.length < 4 || (exeName[$ - 4 .. $] != ".exe" && exeName[$ - 4 .. $] != ".EXE"))
1695 				exeName ~= ".exe";
1696 		} else static assert(0);
1697 
1698 		import arsd.string;
1699 		version(Posix)
1700 			auto searchPaths = getEnvironmentVariable("PATH").split(":");
1701 		else version(Windows)
1702 			auto searchPaths = getEnvironmentVariable("PATH").split(";");
1703 
1704 		//version(Posix) immutable searchPaths = ["/usr/bin", "/bin", "/usr/local/bin", "/home/me/bin"]; // FIXME
1705 		//version(Windows) immutable searchPaths = [`c:/windows`, `c:/windows/system32`, `./`]; // FIXME
1706 		foreach(path; searchPaths) {
1707 			auto t = FilePath(exeName).makeAbsolute(FilePath(path));
1708 
1709 			version(Posix) {
1710 				import core.sys.posix.sys.stat;
1711 				stat_t sbuf;
1712 
1713 				CharzBuffer buf = t.toString();
1714 				auto ret = stat(buf.ptr, &sbuf);
1715 				if(ret != -1)
1716 					return t;
1717 			} else version(Windows) {
1718 				WCharzBuffer nameBuffer = t.toString();
1719 				auto ret = GetFileAttributesW(nameBuffer.ptr);
1720 				if(ret != INVALID_FILE_ATTRIBUTES)
1721 					return t;
1722 			}
1723 		}
1724 		return FilePath(null);
1725 	}
1726 
1727 	RunningCommand startCommand(ShellCommand command, CommandRunningContext crc) {
1728 
1729 		auto fp = command.exePath;
1730 		if(fp.isNull())
1731 			fp = searchPathForCommand(command.argv[0]);
1732 
1733 		if(fp.isNull()) {
1734 			throw new Exception("Command not found");
1735 		}
1736 
1737 		version(Windows) {
1738 			string windowsCommandLine;
1739 			foreach(arg; command.argv) {
1740 				// FIXME: this prolly won't be interpreted right on the other side
1741 				if(windowsCommandLine.length)
1742 					windowsCommandLine ~= " ";
1743 				if(arg.indexOf(" ") != -1)
1744 					windowsCommandLine ~= "\"" ~ arg ~ "\"";
1745 				else
1746 					windowsCommandLine ~= arg;
1747 			}
1748 
1749 			auto proc = new ExternalProcess(fp, windowsCommandLine);
1750 		} else {
1751 			auto proc = new ExternalProcess(fp, command.argv);
1752 			proc.beforeExec = () {
1753 				// reset ignored signals to default behavior
1754 				import core.sys.posix.signal;
1755 				signal (SIGINT, SIG_DFL);
1756 				signal (SIGQUIT, SIG_DFL);
1757 				signal (SIGTSTP, SIG_DFL);
1758 				signal (SIGTTIN, SIG_DFL);
1759 				signal (SIGTTOU, SIG_DFL);
1760 				signal (SIGCHLD, SIG_DFL);
1761 
1762 				//signal (SIGWINCH, SIG_DFL);
1763 				signal (SIGHUP, SIG_DFL);
1764 				signal (SIGCONT, SIG_DFL);
1765 			};
1766 			proc.pgid = crc.pgid; // 0 here means to lead the group, all subsequent pipe programs should inherit the leader
1767 		}
1768 
1769 		// and inherit the standard handles
1770 		proc.overrideStdin = crc.stdin;
1771 		proc.overrideStdout = crc.stdout;
1772 		proc.overrideStderr = crc.stderr;
1773 
1774 		string[string] envOverride;
1775 		foreach(ep; command.environmentPairs)
1776 			envOverride[ep.environmentVariableName] = ep.assignedValue;
1777 
1778 		if(command.environmentPairs.length)
1779 			proc.setEnvironmentWithModifications(envOverride);
1780 
1781 		command.runningCommand = new ExternalProcessWrapper(proc);
1782 		proc.start;
1783 
1784 		return command.runningCommand;
1785 	}
1786 }
1787 
1788 class ImmediateCommandWrapper : RunningCommand {
1789 	override void waitForChange() {
1790 		// it is already complete
1791 	}
1792 
1793 	override int status() {
1794 		return status_;
1795 	}
1796 
1797 	override void makeForeground() {
1798 		// do nothing, immediate commands complete too fast anyway but are also part of the shell
1799 	}
1800 
1801 	private int status_;
1802 	this(int status) {
1803 		this.status_ = status;
1804 	}
1805 
1806 	override bool isStopped() { return false; }
1807 	override bool isComplete() { return true; }
1808 }
1809 
1810 class ShellControlExecutor : Shell.CommandExecutor {
1811 	static struct ShellControlContext {
1812 		ShellContext* context;
1813 		string[] args;
1814 
1815 		HANDLE stdin;
1816 		HANDLE stdout;
1817 		HANDLE stderr;
1818 	}
1819 	__gshared int function(ShellControlContext scc)[string] runners;
1820 	shared static this() {
1821 		runners = [
1822 			"cd": (scc) {
1823 				version(Windows) {
1824 					WCharzBuffer bfr = scc.args.length > 1 ? scc.args[1] : Shell.getUserHome(null);
1825 					if(!SetCurrentDirectory(bfr.ptr))
1826 						// FIXME print the info
1827 						return GetLastError();
1828 					return 0;
1829 				} else {
1830 					import core.sys.posix.unistd;
1831 					import core.stdc.errno;
1832 					CharzBuffer bfr = scc.args.length > 1 ? scc.args[1] : Shell.getUserHome(null);
1833 					if(chdir(bfr.ptr) == -1)
1834 						// FIXME print the info
1835 						return errno;
1836 					return 0;
1837 				}
1838 			},
1839 			"true": (scc) => 0,
1840 			"false": (scc) => 1,
1841 			"alias": (scc) {
1842 				if(scc.args.length <= 1) {
1843 					// FIXME: print all aliases
1844 					return 0;
1845 				} else if(scc.args.length == 2) {
1846 					// FIXME: print the content of aliases[scc.args[1]]
1847 					return 0;
1848 				} else if(scc.args.length >= 3) {
1849 					scc.context.aliases[scc.args[1]] = scc.args[2..$];
1850 					return 0;
1851 				} else {
1852 					// FIXME: print error
1853 					return 1;
1854 				}
1855 			},
1856 			"unalias": (scc) {
1857 				scc.context.aliases.remove(scc.args[1]);
1858 				return 0;
1859 			},
1860 			"shift": (scc) {
1861 				auto n = 1;
1862 				// FIXME: error check and get n off the args if present
1863 				scc.context.scriptArgs = scc.context.scriptArgs[n .. $];
1864 				return 0;
1865 			},
1866 			/++ Assigns a variable to the shell environment for use in this execution context, but that will not be passed to child process' environment. +/
1867 			"let": (scc) {
1868 				scc.context.vars[scc.args[1]] = scc.args[2];
1869 				return 0;
1870 			},
1871 			"exit": (scc) {
1872 				scc.context.exitRequested = true;
1873 				return 0;
1874 			},
1875 			// "pushd" / "popd" / "dirs"
1876 			// "time" - needs the process handle to get more info
1877 			// "which"
1878 			// "set"
1879 			// "export"
1880 			// "source" -- run a script in the current environment
1881 			// "builtin" / "execute" ?
1882 			// "history"
1883 			// "help"
1884 			"jobs": (scc) {
1885 				// FIXME: show the job status (running, done, etc)
1886 				foreach(idx, job; scc.context.jobs)
1887 					writeln(idx, " ", job.argv);
1888 				return 0;
1889 			},
1890 			"fg": (scc) {
1891 				auto task = scc.context.jobs[0].shellTask;
1892 				if(task.state == Fiber.State.HOLD) {
1893 					scc.context.jobToForeground = task;
1894 				} else {
1895 					writeln("Task completed");
1896 					scc.context.jobs = scc.context.jobs[1 .. $];
1897 				}
1898 				return 0;
1899 			},
1900 			"bg": (scc) {
1901 				version(Posix) {
1902 					import core.sys.posix.signal;
1903 					auto pid = scc.context.jobs[0].runningCommand.pid();
1904 					return kill(-pid, SIGCONT);
1905 				}
1906 				return -1; // not implemented on Windows since processes don't stop there anyway
1907 			},
1908 			"wait": (scc) {
1909 				// FIXME: can wait for specific job
1910 				foreach(job; scc.context.jobs) {
1911 					if(job.runningCommand.isStopped) {
1912 						writeln("A job is stopped, waiting would never end. Restart it first with `bg`");
1913 						return 1;
1914 					}
1915 				}
1916 				foreach(job; scc.context.jobs) {
1917 					while(!job.runningCommand.isComplete)
1918 						job.runningCommand.waitForChange();
1919 				}
1920 				scc.context.jobs = null;
1921 
1922 				return 0;
1923 			},
1924 			// "for" / "do" / "done" - i kinda prefer not having do but bash requires it so ... idk. maybe "break" and "continue" too.
1925 			// "if" ?
1926 			// "ulimit"
1927 			// "umask" ?
1928 			//
1929 			// "prompt" ?
1930 
1931 			// "start" ? on Windows especially to shell execute.
1932 
1933 		];
1934 	}
1935 
1936 
1937 	MatchesResult matches(string arg0) {
1938 		return (arg0 in runners) ? MatchesResult.yes : MatchesResult.no;
1939 	}
1940 	FilePath searchPathForCommand(string arg0) {
1941 		return FilePath(null);
1942 	}
1943 	RunningCommand startCommand(ShellCommand command, CommandRunningContext crc) {
1944 		assert(command.shellContext !is null);
1945 
1946 		int ret = 1;
1947 
1948 		try {
1949 			ret = runners[command.argv[0]](ShellControlContext(command.shellContext, command.argv, crc.stdin, crc.stdout, crc.stderr));
1950 		} catch(Exception e) {
1951 			// FIXME
1952 		}
1953 
1954 		command.runningCommand = new ImmediateCommandWrapper(ret);
1955 		return command.runningCommand;
1956 	}
1957 }
1958 
1959 
1960 class InternalCommandWrapper : RunningCommand {
1961 	import core.thread;
1962 	Thread thread;
1963 	this(Thread thread) {
1964 		this.thread = thread;
1965 	}
1966 
1967 	override void waitForChange() {
1968 		auto t = thread.join();
1969 		if(t is null)
1970 			status_ = 0;
1971 		else
1972 			status_ = 1;
1973 	}
1974 
1975 	private int status_ = -1;
1976 
1977 	override int status() {
1978 		return status_;
1979 	}
1980 
1981 	override void makeForeground() {
1982 		// do nothing, built ins share terminal with the shell (maybe)
1983 	}
1984 
1985 	override bool isStopped() { return false; }
1986 	override bool isComplete() { return status_ != -1; }
1987 }
1988 
1989 class CoreutilFallbackExecutor : Shell.CommandExecutor {
1990 	static class Commands {
1991 		private {
1992 			CommandRunningContext crc;
1993 			version(Posix)
1994 				import core.stdc.errno;
1995 
1996 			void writeln(scope const(char)[] msg) {
1997 				msg ~= "\n";
1998 				version(Posix) {
1999 					import unix = core.sys.posix.unistd;
2000 					import core.stdc.errno;
2001 					auto ret = unix.write(crc.stdout, msg.ptr, msg.length);
2002 					if(ret < 0)
2003 						throw new ErrnoApiException("write", errno);
2004 				}
2005 				version(Windows) {
2006 					// FIXME: if it is a console we should convert to wchars and use WriteConsole
2007 					DWORD ret;
2008 					if(!WriteFile(crc.stdout, msg.ptr, cast(int) msg.length, &ret, null))
2009 						throw new WindowsApiException("WriteFile", GetLastError());
2010 				}
2011 				if(ret != msg.length)
2012 					throw new Exception("write failed to do all"); // FIXME
2013 			}
2014 
2015 			void foreachLine(HANDLE file, void delegate(scope const(char)[]) dg) {
2016 				char[] buffer = new char[](1024 * 32 - 512);
2017 				bool eof;
2018 				char[] leftover;
2019 
2020 				getMore:
2021 
2022 				version(Posix) {
2023 					import unix = core.sys.posix.unistd;
2024 					import core.stdc.errno;
2025 					auto ret = unix.read(file, buffer.ptr, buffer.length);
2026 					if(ret < 0)
2027 						throw new ErrnoApiException("read", errno);
2028 				}
2029 				version(Windows) {
2030 					DWORD ret;
2031 					if(!ReadFile(file, buffer.ptr, cast(int) buffer.length, &ret, null)) {
2032 						auto error = GetLastError();
2033 						if(error == ERROR_BROKEN_PIPE)
2034 							eof = true;
2035 						else
2036 							throw new WindowsApiException("ReadFile", error);
2037 					}
2038 				}
2039 
2040 				if(ret == 0)
2041 					eof = true;
2042 
2043 				auto used = leftover;
2044 				if(used.length && ret > 0)
2045 					used ~= buffer[0 .. ret];
2046 				else
2047 					used = buffer[0 .. ret];
2048 
2049 				moreInBuffer:
2050 				auto eol = used.indexOf("\n");
2051 				if(eol != -1) {
2052 					auto line = used[0 .. eol + 1];
2053 					used = used[eol + 1 .. $];
2054 					dg(line);
2055 					goto moreInBuffer;
2056 				} else if(eof) {
2057 					dg(used);
2058 					return;
2059 				} else {
2060 					leftover = used;
2061 					goto getMore;
2062 				}
2063 			}
2064 
2065 			package this(CommandRunningContext crc) {
2066 				this.crc = crc;
2067 			}
2068 		}
2069 
2070 		public:
2071 
2072 		int find(string[] dirs) {
2073 			void delegate(string, bool) makeHandler(string dir) {
2074 				void handler(string name, bool isDirectory) {
2075 					if(name == "." || name == "..")
2076 						return;
2077 					auto fullName = dir;
2078 					if(fullName.length >0 && fullName[$-1] != '/')
2079 						fullName ~= "/";
2080 					fullName ~= name;
2081 					if(isDirectory)
2082 						getFiles(fullName, makeHandler(fullName));
2083 					else
2084 						writeln(fullName);
2085 				}
2086 				return &handler;
2087 			}
2088 
2089 			foreach(dir; dirs)
2090 				getFiles(dir, makeHandler(dir));
2091 			if(dirs.length == 0)
2092 				getFiles(".", makeHandler("."));
2093 
2094 			return 0;
2095 		}
2096 
2097 		// FIXME: need -i and maybe -R at least
2098 		int grep(string[] args) {
2099 			if(args.length) {
2100 				auto find = args[0];
2101 				auto files = args[1 .. $];
2102 				foreachLine(crc.stdin, (line) {
2103 					import arsd.string;
2104 					if(line.indexOf(find) != -1)
2105 						writeln(line.stripRight);
2106 				});
2107 				return 0;
2108 			} else {
2109 				return 1;
2110 			}
2111 		}
2112 
2113 		int echo(string[] args) {
2114 			import arsd.string;
2115 			writeln(args.join(" "));
2116 			return 0;
2117 		}
2118 
2119 		// FIXME: -R, -l, -h all useful to me. also --sort is nice. maybe --color
2120 		int ls(bool a, string[] args) {
2121 			void handler(string name, bool isDirectory) {
2122 				if(!a && name.length && name[0] == '.')
2123 					return;
2124 				writeln(name);
2125 			}
2126 			foreach(arg; args)
2127 				getFiles(arg, &handler);
2128 			if(args.length == 0)
2129 				getFiles(".", &handler);
2130 			return 0;
2131 		}
2132 
2133 		void pwd() {
2134 			writeln(getCurrentWorkingDirectory().toString);
2135 		}
2136 
2137 		void rm(bool R, string[] files) {
2138 			if(R)
2139 				throw new Exception("rm -R not implemented");
2140 			foreach(file; files) {
2141 				version(Windows) {
2142 					WCharzBuffer bfr = file;
2143 					if(!DeleteFileW(bfr.ptr))
2144 						throw new WindowsApiException("DeleteFileW", GetLastError());
2145 				} else version(Posix) {
2146 					CharzBuffer bfr = file;
2147 					if(unistd.unlink(bfr.ptr) == -1)
2148 						throw new ErrnoApiException("unlink", errno);
2149 				}
2150 			}
2151 		}
2152 
2153 		void touch(string[] files) {
2154 			foreach(file; files) {
2155 				auto fo = new File(FilePath(file));
2156 				fo.close();
2157 			}
2158 		}
2159 
2160 		void uniq() {
2161 			const(char)[] previousLine;
2162 			import arsd.string;
2163 			foreachLine(crc.stdin, (line) {
2164 				line = line.stripRight;
2165 				if(line == previousLine)
2166 					return;
2167 				previousLine = line.dup; // dup since the foreach might reuse the buffer
2168 				writeln(line);
2169 			});
2170 		}
2171 
2172 		// FIXME: only prints utc, should do local time by default
2173 		void date() {
2174 			writeln(SimplifiedUtcTimestamp.now.toString);
2175 		}
2176 
2177 		void cat(string[] files) {
2178 			void handler(HANDLE handle) {
2179 				// FIXME actually imprecise af here and inefficient as the lines don't matter
2180 				foreachLine(handle, (line) {
2181 					import arsd.string;
2182 					writeln(line.stripRight);
2183 				});
2184 			}
2185 
2186 			foreach(file; files) {
2187 				auto fo = new File(FilePath(file));
2188 				handler(fo.nativeHandle);
2189 				fo.close();
2190 			}
2191 			if(files.length == 0) {
2192 				handler(crc.stdin);
2193 			}
2194 		}
2195 
2196 		// could do -p which removes parents too
2197 		void rmdir(string[] dirs) {
2198 			foreach(dir; dirs) {
2199 				version(Windows) {
2200 					WCharzBuffer bfr = dir;
2201 					if(!RemoveDirectoryW(bfr.ptr))
2202 						throw new WindowsApiException("DeleteDirectoryW", GetLastError());
2203 				} else version(Posix) {
2204 					CharzBuffer bfr = dir;
2205 					if(unistd.rmdir(bfr.ptr) == -1)
2206 						throw new ErrnoApiException("rmdir", errno);
2207 				}
2208 			}
2209 		}
2210 
2211 		// -p is kinda useful
2212 		void mkdir(string[] dirs) {
2213 			foreach(dir; dirs) {
2214 				version(Windows) {
2215 					WCharzBuffer bfr = dir;
2216 					if(!CreateDirectoryW(bfr.ptr, null))
2217 						throw new WindowsApiException("CreateDirectoryW", GetLastError());
2218 				} else version(Posix) {
2219 					import unix = core.sys.posix.sys.stat;
2220 					CharzBuffer bfr = dir;
2221 					if(unix.mkdir(bfr.ptr, 0x1ff /* 0o777 */) == -1)
2222 						throw new ErrnoApiException("mkdir", errno);
2223 				}
2224 			}
2225 		}
2226 
2227 		// maybe just take off the extension, whatever it is
2228 		int basename(string[] args) {
2229 			if(args.length < 1 || args.length > 2) {
2230 				// FIXME use stderr
2231 				writeln("bad arg count");
2232 				return 1;
2233 			}
2234 			auto path = FilePath(args[0]);
2235 			auto fn = path.filename;
2236 			if(args.length > 1) {
2237 				auto tocut = args[1];
2238 				if(fn.length > tocut.length && fn[$ - tocut.length .. $] == tocut)
2239 					fn = fn[0 .. $ - tocut.length];
2240 			}
2241 			writeln(fn);
2242 			return 0;
2243 		}
2244 	}
2245 
2246 	/+
2247 		gonna want some kind of:
2248 			mv
2249 				rename(2)
2250 				MoveFileW
2251 			cp
2252 				copy_file_range introduced linux 2016.
2253 				CopyFileW
2254 			sort
2255 
2256 			nc
2257 			xsel
2258 
2259 			du ?
2260 
2261 			env ?
2262 
2263 			no chmod, ln, or unlink because Windows doesn't do them anyway...
2264 	+/
2265 	MatchesResult matches(string arg0) {
2266 		switch(arg0) {
2267 			foreach(memberName; __traits(derivedMembers, Commands))
2268 				static if(__traits(getProtection, __traits(getMember, Commands, memberName)) == "public")
2269 					case memberName:
2270 						return MatchesResult.yes;
2271 			default:
2272 				return MatchesResult.no;
2273 		}
2274 	}
2275 	FilePath searchPathForCommand(string arg0) {
2276 		return FilePath(null);
2277 	}
2278 	RunningCommand startCommand(ShellCommand command, CommandRunningContext crc) {
2279 		// basically using a thread as a fake process
2280 
2281 		auto stdin = duplicate(crc.stdin);
2282 		auto stdout = duplicate(crc.stdout);
2283 		auto stderr = duplicate(crc.stderr);
2284 		import core.thread;
2285 		void runner() {
2286 			scope(exit) {
2287 				CloseHandle(stdin);
2288 				CloseHandle(stdout);
2289 				CloseHandle(stderr);
2290 			}
2291 
2292 			import arsd.cli;
2293 			// FIXME: forward status through
2294 			runCli!Commands(["builtin"] ~ command.argv, CommandRunningContext(stdin, stdout, stderr));
2295 		}
2296 
2297 		auto thread = new Thread(&runner);
2298 		thread.start();
2299 		command.runningCommand = new InternalCommandWrapper(thread);
2300 		return command.runningCommand;
2301 	}
2302 
2303 }
2304 
2305 // builtin commands should just be run in a helper thread so they can be as close to the original as reasonable
2306 class BuiltinShellCommand {
2307 	abstract int run(string[] args, AsyncAnonymousPipe stdin, AsyncAnonymousPipe stdout, AsyncAnonymousPipe stderr);
2308 }
2309 
2310 /++
2311 	Constructs an instance of [arsd.terminal.LineGetter] appropriate for use in a repl for this shell.
2312 +/
2313 auto constructLineGetter()() {
2314 	return null;
2315 }
2316 
2317 /++
2318 	Sets up signal handling and progress groups to become an interactive shell.
2319 +/
2320 void enableInteractiveShell() {
2321 	version(Posix) {
2322 		// copy/pasted this from the bash manual
2323 		import core.sys.posix.unistd;
2324 		import core.sys.posix.signal;
2325 		/* Loop until we are in the foreground.  */
2326 		int shell_pgid;
2327 		while (tcgetpgrp (0) != (shell_pgid = getpgrp ()))
2328 			kill (- shell_pgid, SIGTTIN);
2329 
2330 		/* Ignore interactive and job-control signals.  */
2331 		signal (SIGINT, SIG_IGN); // ctrl+c
2332 		signal (SIGQUIT, SIG_IGN); // ctrl+\
2333 		signal (SIGTSTP, SIG_IGN); // ctrl+z. should stop the foreground process. send CONT to continue it. shell can do waitpid on it to get flags if it is suspended.
2334 		signal (SIGTTIN, SIG_IGN);
2335 		signal (SIGTTOU, SIG_IGN);
2336 		signal (SIGCHLD, SIG_IGN); // arsd.core takes care of this
2337 
2338 		/* Put ourselves in our own process group.  */
2339 		shell_pgid = getpid ();
2340 		if (setpgid (shell_pgid, shell_pgid) < 0)
2341 		{
2342 			throw new Exception ("Couldn't put the shell in its own process group");
2343 		}
2344 
2345 		/* Grab control of the terminal.  */
2346 		tcsetpgrp (0, shell_pgid);
2347 
2348 		/* Save default terminal attributes for shell.  */
2349 		//tcgetattr (0, &shell_tmodes);
2350 	}
2351 }
2352 
2353 /+
2354 	Parts of bash I like:
2355 
2356 		glob expansion
2357 		! command recall
2358 		redirection
2359 		f{1..3} expand to f1 f2 f3. can add ..incr btw
2360 		f{a,b,c} expand to fa fb fc
2361 		for i in *; do cmd; done
2362 		`command expansion`. also $( cmd ) is a thing
2363 		~ expansion
2364 
2365 		foo && bar
2366 		foo || bar
2367 
2368 		$(( maybe arithmetic but idk ))
2369 
2370 		ENV=whatever cmd.
2371 		$ENV ...?
2372 
2373 		tab complete!
2374 
2375 	PATH lookup. if requested.
2376 
2377 	Globbing could insert -- before if there's any - in there.
2378 
2379 	Or better yet all the commands must either start with ./
2380 	or be found internally. Internal can be expanded by definition
2381 	files that tell how to expand the real thing.
2382 
2383 		* flags
2384 		* arguments
2385 		* command line
2386 		* i/o
2387 +/