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\\\»bar"); 629 assert(got.length == 3); 630 assert(got[0].l == "test"); 631 assert(got[1].l == "\»"); 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 +/