1 /* 2 * PARSER.C - command parsing. 3 */ 4 5 #include "precomp.h" 6 7 /* Enable this define for "buggy" Windows' CMD command echoer compatibility */ 8 #define MSCMD_ECHO_COMMAND_COMPAT 9 10 /* 11 * Parser debugging support. These flags are global so that their values can be 12 * modified at runtime from a debugger. They correspond to the public Windows' 13 * cmd!fDumpTokens and cmd!fDumpParse booleans. 14 * (Same names are used for compatibility as they are documented online.) 15 */ 16 BOOLEAN fDumpTokens = FALSE; 17 BOOLEAN fDumpParse = FALSE; 18 19 #define C_OP_LOWEST C_MULTI 20 #define C_OP_HIGHEST C_PIPE 21 static const TCHAR OpString[][3] = { _T("&"), _T("||"), _T("&&"), _T("|") }; 22 23 static const TCHAR RedirString[][3] = { _T("<"), _T(">"), _T(">>") }; 24 25 static const TCHAR *const IfOperatorString[] = 26 { 27 /* Standard */ 28 _T("errorlevel"), 29 _T("exist"), 30 31 /* Extended */ 32 _T("cmdextversion"), 33 _T("defined"), 34 #define IF_MAX_UNARY IF_DEFINED 35 36 /* Standard */ 37 _T("=="), 38 39 /* Extended */ 40 _T("equ"), 41 _T("neq"), 42 _T("lss"), 43 _T("leq"), 44 _T("gtr"), 45 _T("geq"), 46 #define IF_MAX_COMPARISON IF_GEQ 47 }; 48 49 static BOOL IsSeparator(TCHAR Char) 50 { 51 return _istspace(Char) || (Char && _tcschr(STANDARD_SEPS, Char)); 52 } 53 54 enum 55 { 56 TOK_END, 57 TOK_NORMAL, 58 TOK_OPERATOR, 59 TOK_REDIRECTION, 60 TOK_BEGIN_BLOCK, 61 TOK_END_BLOCK 62 }; 63 64 /* Scratch buffer for temporary command substitutions / expansions */ 65 static TCHAR TempBuf[CMDLINE_LENGTH]; 66 67 /*static*/ BOOL bParseError; 68 static BOOL bLineContinuations; 69 /*static*/ TCHAR ParseLine[CMDLINE_LENGTH]; 70 static TCHAR *ParsePos; 71 static TCHAR CurChar; 72 73 static TCHAR CurrentToken[CMDLINE_LENGTH]; 74 static int CurrentTokenType; 75 static int InsideBlock; 76 77 static TCHAR ParseChar(void) 78 { 79 TCHAR Char; 80 81 if (bParseError) 82 return (CurChar = 0); 83 84 restart: 85 /* 86 * Although CRs can be injected into a line via an environment 87 * variable substitution, the parser ignores them - they won't 88 * even separate tokens. 89 */ 90 do 91 { 92 Char = *ParsePos++; 93 } 94 while (Char == _T('\r')); 95 96 if (!Char) 97 { 98 ParsePos--; 99 if (bLineContinuations) 100 { 101 if (!ReadLine(ParseLine, TRUE)) 102 { 103 /* ^C pressed, or line was too long */ 104 bParseError = TRUE; 105 } 106 else if (*(ParsePos = ParseLine)) 107 { 108 goto restart; 109 } 110 } 111 } 112 return (CurChar = Char); 113 } 114 115 VOID ParseErrorEx(IN PCTSTR s) 116 { 117 /* Only display the first error we encounter */ 118 if (!bParseError) 119 error_syntax(s); 120 bParseError = TRUE; 121 } 122 123 static __inline VOID ParseError(VOID) 124 { 125 ParseErrorEx(CurrentTokenType != TOK_END ? CurrentToken : NULL); 126 } 127 128 /* 129 * Yes, cmd has a Lexical Analyzer. Whenever the parser gives an "xxx was 130 * unexpected at this time." message, it shows what the last token read was. 131 */ 132 static int ParseToken(TCHAR ExtraEnd, TCHAR *Separators) 133 { 134 TCHAR *Out = CurrentToken; 135 TCHAR Char; 136 int Type; 137 BOOL bInQuote = FALSE; 138 139 for (Char = CurChar; Char && Char != _T('\n'); Char = ParseChar()) 140 { 141 bInQuote ^= (Char == _T('"')); 142 if (!bInQuote) 143 { 144 if (Separators != NULL) 145 { 146 if (_istspace(Char) || _tcschr(Separators, Char)) 147 { 148 /* Skip leading separators */ 149 if (Out == CurrentToken) 150 continue; 151 break; 152 } 153 } 154 155 /* Check for numbered redirection */ 156 if ((Char >= _T('0') && Char <= _T('9') && 157 (ParsePos == &ParseLine[1] || IsSeparator(ParsePos[-2])) 158 && (*ParsePos == _T('<') || *ParsePos == _T('>')))) 159 { 160 break; 161 } 162 163 if (Char == ExtraEnd) 164 break; 165 if (InsideBlock && Char == _T(')')) 166 break; 167 if (_tcschr(_T("&|<>"), Char)) 168 break; 169 170 if (Char == _T('^')) 171 { 172 Char = ParseChar(); 173 /* Eat up a \n, allowing line continuation */ 174 if (Char == _T('\n')) 175 Char = ParseChar(); 176 /* Next character is a forced literal */ 177 } 178 } 179 if (Out == &CurrentToken[CMDLINE_LENGTH - 1]) 180 break; 181 *Out++ = Char; 182 } 183 184 /* Check if we got at least one character before reaching a special one. 185 * If so, return them and leave the special for the next call. */ 186 if (Out != CurrentToken) 187 { 188 Type = TOK_NORMAL; 189 } 190 else if (Char == _T('(')) 191 { 192 Type = TOK_BEGIN_BLOCK; 193 *Out++ = Char; 194 ParseChar(); 195 } 196 else if (Char == _T(')')) 197 { 198 Type = TOK_END_BLOCK; 199 *Out++ = Char; 200 ParseChar(); 201 } 202 else if (Char == _T('&') || Char == _T('|')) 203 { 204 Type = TOK_OPERATOR; 205 *Out++ = Char; 206 Char = ParseChar(); 207 /* check for && or || */ 208 if (Char == Out[-1]) 209 { 210 *Out++ = Char; 211 ParseChar(); 212 } 213 } 214 else if ((Char >= _T('0') && Char <= _T('9')) 215 || (Char == _T('<') || Char == _T('>'))) 216 { 217 Type = TOK_REDIRECTION; 218 if (Char >= _T('0') && Char <= _T('9')) 219 { 220 *Out++ = Char; 221 Char = ParseChar(); 222 } 223 *Out++ = Char; 224 Char = ParseChar(); 225 if (Char == Out[-1]) 226 { 227 /* Strangely, the tokenizer allows << as well as >>... (it 228 * will cause an error when trying to parse it though) */ 229 *Out++ = Char; 230 Char = ParseChar(); 231 } 232 if (Char == _T('&')) 233 { 234 *Out++ = Char; 235 while (IsSeparator(Char = ParseChar())) 236 ; 237 if (Char >= _T('0') && Char <= _T('9')) 238 { 239 *Out++ = Char; 240 ParseChar(); 241 } 242 } 243 } 244 else 245 { 246 Type = TOK_END; 247 } 248 *Out = _T('\0'); 249 250 /* Debugging support */ 251 if (fDumpTokens) 252 ConOutPrintf(_T("ParseToken: (%d) '%s'\n"), Type, CurrentToken); 253 254 return (CurrentTokenType = Type); 255 } 256 257 static BOOL ParseRedirection(REDIRECTION **List) 258 { 259 TCHAR *Tok = CurrentToken; 260 BYTE Number; 261 REDIR_MODE RedirMode; 262 REDIRECTION *Redir; 263 264 if (*Tok >= _T('0') && *Tok <= _T('9')) 265 Number = *Tok++ - _T('0'); 266 else 267 Number = *Tok == _T('<') ? 0 : 1; 268 269 if (*Tok++ == _T('<')) 270 { 271 RedirMode = REDIR_READ; 272 if (*Tok == _T('<')) 273 goto fail; 274 } 275 else 276 { 277 RedirMode = REDIR_WRITE; 278 if (*Tok == _T('>')) 279 { 280 RedirMode = REDIR_APPEND; 281 Tok++; 282 } 283 } 284 285 if (!*Tok) 286 { 287 /* The file name was not part of this token, so it'll be the next one */ 288 if (ParseToken(0, STANDARD_SEPS) != TOK_NORMAL) 289 goto fail; 290 Tok = CurrentToken; 291 } 292 293 /* If a redirection for this handle number already exists, delete it */ 294 while ((Redir = *List)) 295 { 296 if (Redir->Number == Number) 297 { 298 *List = Redir->Next; 299 cmd_free(Redir); 300 continue; 301 } 302 List = &Redir->Next; 303 } 304 305 Redir = cmd_alloc(FIELD_OFFSET(REDIRECTION, Filename[_tcslen(Tok) + 1])); 306 if (!Redir) 307 { 308 WARN("Cannot allocate memory for Redir!\n"); 309 goto fail; 310 } 311 Redir->Next = NULL; 312 Redir->OldHandle = INVALID_HANDLE_VALUE; 313 Redir->Number = Number; 314 Redir->Mode = RedirMode; 315 _tcscpy(Redir->Filename, Tok); 316 *List = Redir; 317 return TRUE; 318 319 fail: 320 ParseError(); 321 FreeRedirection(*List); 322 *List = NULL; 323 return FALSE; 324 } 325 326 static PARSED_COMMAND *ParseCommandOp(int OpType); 327 328 /* Parse a parenthesized block */ 329 static PARSED_COMMAND *ParseBlock(REDIRECTION *RedirList) 330 { 331 PARSED_COMMAND *Cmd, *Sub, **NextPtr; 332 333 Cmd = cmd_alloc(sizeof(PARSED_COMMAND)); 334 if (!Cmd) 335 { 336 WARN("Cannot allocate memory for Cmd!\n"); 337 ParseError(); 338 FreeRedirection(RedirList); 339 return NULL; 340 } 341 Cmd->Type = C_BLOCK; 342 Cmd->Next = NULL; 343 Cmd->Subcommands = NULL; 344 Cmd->Redirections = RedirList; 345 346 /* Read the block contents */ 347 NextPtr = &Cmd->Subcommands; 348 InsideBlock++; 349 while (1) 350 { 351 Sub = ParseCommandOp(C_OP_LOWEST); 352 if (Sub) 353 { 354 *NextPtr = Sub; 355 NextPtr = &Sub->Next; 356 } 357 else if (bParseError) 358 { 359 InsideBlock--; 360 FreeCommand(Cmd); 361 return NULL; 362 } 363 364 if (CurrentTokenType == TOK_END_BLOCK) 365 break; 366 367 /* Skip past the \n */ 368 ParseChar(); 369 } 370 InsideBlock--; 371 372 /* Process any trailing redirections */ 373 while (ParseToken(0, STANDARD_SEPS) == TOK_REDIRECTION) 374 { 375 if (!ParseRedirection(&Cmd->Redirections)) 376 { 377 FreeCommand(Cmd); 378 return NULL; 379 } 380 } 381 return Cmd; 382 } 383 384 /* Parse an IF statement */ 385 static PARSED_COMMAND *ParseIf(void) 386 { 387 PARSED_COMMAND *Cmd; 388 389 Cmd = cmd_alloc(sizeof(PARSED_COMMAND)); 390 if (!Cmd) 391 { 392 WARN("Cannot allocate memory for Cmd!\n"); 393 ParseError(); 394 return NULL; 395 } 396 memset(Cmd, 0, sizeof(PARSED_COMMAND)); 397 Cmd->Type = C_IF; 398 399 if (bEnableExtensions && (_tcsicmp(CurrentToken, _T("/I")) == 0)) 400 { 401 Cmd->If.Flags |= IFFLAG_IGNORECASE; 402 ParseToken(0, STANDARD_SEPS); 403 } 404 if (_tcsicmp(CurrentToken, _T("not")) == 0) 405 { 406 Cmd->If.Flags |= IFFLAG_NEGATE; 407 ParseToken(0, STANDARD_SEPS); 408 } 409 410 if (CurrentTokenType != TOK_NORMAL) 411 goto error; 412 413 /* Check for unary operators */ 414 for (; Cmd->If.Operator <= IF_MAX_UNARY; Cmd->If.Operator++) 415 { 416 /* Skip the extended operators if the extensions are disabled */ 417 if (!bEnableExtensions && (Cmd->If.Operator >= IF_CMDEXTVERSION)) 418 continue; 419 420 if (_tcsicmp(CurrentToken, IfOperatorString[Cmd->If.Operator]) == 0) 421 { 422 if (ParseToken(0, STANDARD_SEPS) != TOK_NORMAL) 423 goto error; 424 Cmd->If.RightArg = cmd_dup(CurrentToken); 425 goto condition_done; 426 } 427 } 428 429 /* It must be a two-argument (comparison) operator. It could be ==, so 430 * the equals sign can't be treated as whitespace here. */ 431 Cmd->If.LeftArg = cmd_dup(CurrentToken); 432 ParseToken(0, _T(",;")); 433 434 /* The right argument can come immediately after == */ 435 if (_tcsnicmp(CurrentToken, _T("=="), 2) == 0 && CurrentToken[2]) 436 { 437 Cmd->If.RightArg = cmd_dup(&CurrentToken[2]); 438 goto condition_done; 439 } 440 441 // Cmd->If.Operator == IF_MAX_UNARY + 1; 442 for (; Cmd->If.Operator <= IF_MAX_COMPARISON; Cmd->If.Operator++) 443 { 444 /* Skip the extended operators if the extensions are disabled */ 445 if (!bEnableExtensions && (Cmd->If.Operator >= IF_EQU)) // (Cmd->If.Operator > IF_STRINGEQ) 446 continue; 447 448 if (_tcsicmp(CurrentToken, IfOperatorString[Cmd->If.Operator]) == 0) 449 { 450 if (ParseToken(0, STANDARD_SEPS) != TOK_NORMAL) 451 goto error; 452 Cmd->If.RightArg = cmd_dup(CurrentToken); 453 goto condition_done; 454 } 455 } 456 goto error; 457 458 condition_done: 459 Cmd->Subcommands = ParseCommandOp(C_OP_LOWEST); 460 if (Cmd->Subcommands == NULL) 461 goto error; 462 if (_tcsicmp(CurrentToken, _T("else")) == 0) 463 { 464 Cmd->Subcommands->Next = ParseCommandOp(C_OP_LOWEST); 465 if (Cmd->Subcommands->Next == NULL) 466 goto error; 467 } 468 469 return Cmd; 470 471 error: 472 FreeCommand(Cmd); 473 ParseError(); 474 return NULL; 475 } 476 477 /* 478 * Parse a FOR command. 479 * Syntax is: FOR [options] %var IN (list) DO command 480 */ 481 static PARSED_COMMAND *ParseFor(void) 482 { 483 PARSED_COMMAND *Cmd; 484 TCHAR* List = TempBuf; 485 TCHAR *Pos = List; 486 487 Cmd = cmd_alloc(sizeof(PARSED_COMMAND)); 488 if (!Cmd) 489 { 490 WARN("Cannot allocate memory for Cmd!\n"); 491 ParseError(); 492 return NULL; 493 } 494 memset(Cmd, 0, sizeof(PARSED_COMMAND)); 495 Cmd->Type = C_FOR; 496 497 /* Skip the extended FOR syntax if extensions are disabled */ 498 if (!bEnableExtensions) 499 goto parseForBody; 500 501 while (1) 502 { 503 if (_tcsicmp(CurrentToken, _T("/D")) == 0) 504 { 505 Cmd->For.Switches |= FOR_DIRS; 506 } 507 else if (_tcsicmp(CurrentToken, _T("/F")) == 0) 508 { 509 Cmd->For.Switches |= FOR_F; 510 if (!Cmd->For.Params) 511 { 512 ParseToken(0, STANDARD_SEPS); 513 if (CurrentToken[0] == _T('/') || CurrentToken[0] == _T('%')) 514 break; 515 Cmd->For.Params = cmd_dup(CurrentToken); 516 } 517 } 518 else if (_tcsicmp(CurrentToken, _T("/L")) == 0) 519 { 520 Cmd->For.Switches |= FOR_LOOP; 521 } 522 else if (_tcsicmp(CurrentToken, _T("/R")) == 0) 523 { 524 Cmd->For.Switches |= FOR_RECURSIVE; 525 if (!Cmd->For.Params) 526 { 527 ParseToken(0, STANDARD_SEPS); 528 if (CurrentToken[0] == _T('/') || CurrentToken[0] == _T('%')) 529 break; 530 StripQuotes(CurrentToken); 531 Cmd->For.Params = cmd_dup(CurrentToken); 532 } 533 } 534 else 535 { 536 break; 537 } 538 539 ParseToken(0, STANDARD_SEPS); 540 } 541 542 /* Make sure there aren't two different switches specified 543 * at the same time, unless they're /D and /R */ 544 if ((Cmd->For.Switches & (Cmd->For.Switches - 1)) != 0 545 && Cmd->For.Switches != (FOR_DIRS | FOR_RECURSIVE)) 546 { 547 goto error; 548 } 549 550 parseForBody: 551 552 /* Variable name should be % and just one other character */ 553 if (CurrentToken[0] != _T('%') || _tcslen(CurrentToken) != 2) 554 goto error; 555 Cmd->For.Variable = CurrentToken[1]; 556 557 ParseToken(0, STANDARD_SEPS); 558 if (_tcsicmp(CurrentToken, _T("in")) != 0) 559 goto error; 560 561 if (ParseToken(_T('('), STANDARD_SEPS) != TOK_BEGIN_BLOCK) 562 goto error; 563 564 while (1) 565 { 566 /* Pretend we're inside a block so the tokenizer will stop on ')' */ 567 InsideBlock++; 568 ParseToken(0, STANDARD_SEPS); 569 InsideBlock--; 570 571 if (CurrentTokenType == TOK_END_BLOCK) 572 break; 573 574 if (CurrentTokenType == TOK_END) 575 { 576 /* Skip past the \n */ 577 ParseChar(); 578 continue; 579 } 580 581 if (CurrentTokenType != TOK_NORMAL) 582 goto error; 583 584 if (Pos != List) 585 *Pos++ = _T(' '); 586 587 if (Pos + _tcslen(CurrentToken) >= &List[CMDLINE_LENGTH]) 588 goto error; 589 Pos = _stpcpy(Pos, CurrentToken); 590 } 591 *Pos = _T('\0'); 592 Cmd->For.List = cmd_dup(List); 593 594 ParseToken(0, STANDARD_SEPS); 595 if (_tcsicmp(CurrentToken, _T("do")) != 0) 596 goto error; 597 598 Cmd->Subcommands = ParseCommandOp(C_OP_LOWEST); 599 if (Cmd->Subcommands == NULL) 600 goto error; 601 602 return Cmd; 603 604 error: 605 FreeCommand(Cmd); 606 ParseError(); 607 return NULL; 608 } 609 610 /* Parse a REM command */ 611 static PARSED_COMMAND *ParseRem(void) 612 { 613 /* "Ignore" the rest of the line. 614 * (Line continuations will still be parsed, though.) */ 615 while (ParseToken(0, NULL) != TOK_END) 616 ; 617 return NULL; 618 } 619 620 static DECLSPEC_NOINLINE PARSED_COMMAND *ParseCommandPart(REDIRECTION *RedirList) 621 { 622 TCHAR ParsedLine[CMDLINE_LENGTH]; 623 PARSED_COMMAND *Cmd; 624 PARSED_COMMAND *(*Func)(void); 625 626 TCHAR *Pos = _stpcpy(ParsedLine, CurrentToken) + 1; 627 DWORD_PTR TailOffset = Pos - ParsedLine; 628 629 /* Check for special forms */ 630 if ((Func = ParseFor, _tcsicmp(ParsedLine, _T("for")) == 0) || 631 (Func = ParseIf, _tcsicmp(ParsedLine, _T("if")) == 0) || 632 (Func = ParseRem, _tcsicmp(ParsedLine, _T("rem")) == 0)) 633 { 634 ParseToken(0, STANDARD_SEPS); 635 /* Do special parsing only if it's not followed by /? */ 636 if (_tcscmp(CurrentToken, _T("/?")) != 0) 637 { 638 if (RedirList) 639 { 640 ParseError(); 641 FreeRedirection(RedirList); 642 return NULL; 643 } 644 return Func(); 645 } 646 Pos = _stpcpy(Pos, _T(" /?")); 647 } 648 649 /* Now get the tail */ 650 while (1) 651 { 652 ParseToken(0, NULL); 653 if (CurrentTokenType == TOK_NORMAL) 654 { 655 if (Pos + _tcslen(CurrentToken) >= &ParsedLine[CMDLINE_LENGTH]) 656 { 657 ParseError(); 658 FreeRedirection(RedirList); 659 return NULL; 660 } 661 Pos = _stpcpy(Pos, CurrentToken); 662 } 663 else if (CurrentTokenType == TOK_REDIRECTION) 664 { 665 if (!ParseRedirection(&RedirList)) 666 return NULL; 667 } 668 else 669 { 670 break; 671 } 672 } 673 *Pos++ = _T('\0'); 674 675 Cmd = cmd_alloc(FIELD_OFFSET(PARSED_COMMAND, Command.First[Pos - ParsedLine])); 676 if (!Cmd) 677 { 678 WARN("Cannot allocate memory for Cmd!\n"); 679 ParseError(); 680 FreeRedirection(RedirList); 681 return NULL; 682 } 683 Cmd->Type = C_COMMAND; 684 Cmd->Next = NULL; 685 Cmd->Subcommands = NULL; 686 Cmd->Redirections = RedirList; 687 memcpy(Cmd->Command.First, ParsedLine, (Pos - ParsedLine) * sizeof(TCHAR)); 688 Cmd->Command.Rest = Cmd->Command.First + TailOffset; 689 return Cmd; 690 } 691 692 static PARSED_COMMAND *ParsePrimary(void) 693 { 694 REDIRECTION *RedirList = NULL; 695 int Type; 696 697 while (IsSeparator(CurChar)) 698 { 699 if (CurChar == _T('\n')) 700 return NULL; 701 ParseChar(); 702 } 703 704 if (!CurChar) 705 return NULL; 706 707 if (CurChar == _T(':')) 708 { 709 /* "Ignore" the rest of the line. 710 * (Line continuations will still be parsed, though.) */ 711 while (ParseToken(0, NULL) != TOK_END) 712 ; 713 return NULL; 714 } 715 716 if (CurChar == _T('@')) 717 { 718 PARSED_COMMAND *Cmd; 719 ParseChar(); 720 Cmd = cmd_alloc(sizeof(PARSED_COMMAND)); 721 if (!Cmd) 722 { 723 WARN("Cannot allocate memory for Cmd!\n"); 724 ParseError(); 725 return NULL; 726 } 727 Cmd->Type = C_QUIET; 728 Cmd->Next = NULL; 729 /* @ acts like a unary operator with low precedence, 730 * so call the top-level parser */ 731 Cmd->Subcommands = ParseCommandOp(C_OP_LOWEST); 732 Cmd->Redirections = NULL; 733 return Cmd; 734 } 735 736 /* Process leading redirections and get the head of the command */ 737 while ((Type = ParseToken(_T('('), STANDARD_SEPS)) == TOK_REDIRECTION) 738 { 739 if (!ParseRedirection(&RedirList)) 740 return NULL; 741 } 742 743 if (Type == TOK_NORMAL) 744 return ParseCommandPart(RedirList); 745 else if (Type == TOK_BEGIN_BLOCK) 746 return ParseBlock(RedirList); 747 else if (Type == TOK_END_BLOCK && !RedirList) 748 return NULL; 749 750 ParseError(); 751 FreeRedirection(RedirList); 752 return NULL; 753 } 754 755 static PARSED_COMMAND *ParseCommandOp(int OpType) 756 { 757 PARSED_COMMAND *Cmd, *Left, *Right; 758 759 if (OpType == C_OP_HIGHEST) 760 Cmd = ParsePrimary(); 761 else 762 Cmd = ParseCommandOp(OpType + 1); 763 764 if (Cmd && !_tcscmp(CurrentToken, OpString[OpType - C_OP_LOWEST])) 765 { 766 Left = Cmd; 767 Right = ParseCommandOp(OpType); 768 if (!Right) 769 { 770 if (!bParseError) 771 { 772 /* & is allowed to have an empty RHS */ 773 if (OpType == C_MULTI) 774 return Left; 775 ParseError(); 776 } 777 FreeCommand(Left); 778 return NULL; 779 } 780 781 Cmd = cmd_alloc(sizeof(PARSED_COMMAND)); 782 if (!Cmd) 783 { 784 WARN("Cannot allocate memory for Cmd!\n"); 785 ParseError(); 786 FreeCommand(Left); 787 FreeCommand(Right); 788 return NULL; 789 } 790 Cmd->Type = OpType; 791 Cmd->Next = NULL; 792 Cmd->Redirections = NULL; 793 Cmd->Subcommands = Left; 794 Left->Next = Right; 795 Right->Next = NULL; 796 } 797 798 return Cmd; 799 } 800 801 VOID 802 DumpCommand(PARSED_COMMAND *Cmd, ULONG SpacePad); 803 804 PARSED_COMMAND * 805 ParseCommand(LPTSTR Line) 806 { 807 PARSED_COMMAND *Cmd; 808 809 if (Line) 810 { 811 if (!SubstituteVars(Line, ParseLine, _T('%'))) 812 return NULL; 813 bLineContinuations = FALSE; 814 } 815 else 816 { 817 if (!ReadLine(ParseLine, FALSE)) 818 return NULL; 819 bLineContinuations = TRUE; 820 } 821 bParseError = FALSE; 822 ParsePos = ParseLine; 823 CurChar = _T(' '); 824 825 Cmd = ParseCommandOp(C_OP_LOWEST); 826 if (Cmd) 827 { 828 bIgnoreEcho = FALSE; 829 830 if (CurrentTokenType != TOK_END) 831 ParseError(); 832 if (bParseError) 833 { 834 FreeCommand(Cmd); 835 return NULL; 836 } 837 838 /* Debugging support */ 839 if (fDumpParse) 840 DumpCommand(Cmd, 0); 841 } 842 else 843 { 844 bIgnoreEcho = TRUE; 845 } 846 return Cmd; 847 } 848 849 850 /* 851 * This function is similar to EchoCommand(), but is used 852 * for dumping the command tree for debugging purposes. 853 */ 854 static VOID 855 DumpRedir(REDIRECTION* Redirections) 856 { 857 REDIRECTION* Redir; 858 859 if (Redirections) 860 #ifndef MSCMD_ECHO_COMMAND_COMPAT 861 ConOutPuts(_T(" Redir: ")); 862 #else 863 ConOutPuts(_T("Redir: ")); 864 #endif 865 for (Redir = Redirections; Redir; Redir = Redir->Next) 866 { 867 ConOutPrintf(_T(" %x %s%s"), Redir->Number, 868 RedirString[Redir->Mode], Redir->Filename); 869 } 870 } 871 872 VOID 873 DumpCommand(PARSED_COMMAND *Cmd, ULONG SpacePad) 874 { 875 /* 876 * This macro is like DumpCommand(Cmd, Pad); 877 * but avoids an extra recursive level. 878 * Note that it can be used ONLY for terminating commands! 879 */ 880 #define DUMP(Command, Pad) \ 881 do { \ 882 Cmd = (Command); \ 883 SpacePad = (Pad); \ 884 goto dump; \ 885 } while (0) 886 887 PARSED_COMMAND *Sub; 888 889 dump: 890 /* Space padding */ 891 ConOutPrintf(_T("%*s"), SpacePad, _T("")); 892 893 switch (Cmd->Type) 894 { 895 case C_COMMAND: 896 { 897 /* Generic command name, and Type */ 898 #ifndef MSCMD_ECHO_COMMAND_COMPAT 899 ConOutPrintf(_T("Cmd: %s Type: %x"), 900 Cmd->Command.First, Cmd->Type); 901 #else 902 ConOutPrintf(_T("Cmd: %s Type: %x "), 903 Cmd->Command.First, Cmd->Type); 904 #endif 905 /* Arguments */ 906 if (Cmd->Command.Rest && *(Cmd->Command.Rest)) 907 #ifndef MSCMD_ECHO_COMMAND_COMPAT 908 ConOutPrintf(_T(" Args: `%s'"), Cmd->Command.Rest); 909 #else 910 ConOutPrintf(_T("Args: `%s' "), Cmd->Command.Rest); 911 #endif 912 /* Redirections */ 913 DumpRedir(Cmd->Redirections); 914 915 ConOutChar(_T('\n')); 916 return; 917 } 918 919 case C_QUIET: 920 { 921 #ifndef MSCMD_ECHO_COMMAND_COMPAT 922 ConOutChar(_T('@')); 923 #else 924 ConOutPuts(_T("@ ")); 925 #endif 926 DumpRedir(Cmd->Redirections); // FIXME: Can we have leading redirections?? 927 ConOutChar(_T('\n')); 928 929 /*DumpCommand*/DUMP(Cmd->Subcommands, SpacePad + 2); 930 return; 931 } 932 933 case C_BLOCK: 934 { 935 #ifndef MSCMD_ECHO_COMMAND_COMPAT 936 ConOutChar(_T('(')); 937 #else 938 ConOutPuts(_T("( ")); 939 #endif 940 DumpRedir(Cmd->Redirections); 941 ConOutChar(_T('\n')); 942 943 SpacePad += 2; 944 945 for (Sub = Cmd->Subcommands; Sub; Sub = Sub->Next) 946 { 947 #if defined(MSCMD_ECHO_COMMAND_COMPAT) && defined(MSCMD_PARSER_BUGS) 948 /* 949 * We will emulate Windows' CMD handling of "CRLF" and "&" multi-command 950 * enumeration within parenthesized command blocks. 951 */ 952 953 if (!Sub->Next) 954 { 955 DumpCommand(Sub, SpacePad); 956 continue; 957 } 958 959 if (Sub->Type != C_MULTI) 960 { 961 ConOutPrintf(_T("%*s"), SpacePad, _T("")); 962 ConOutPuts(_T("CRLF \n")); 963 DumpCommand(Sub, SpacePad); 964 continue; 965 } 966 967 /* Now, Sub->Type == C_MULTI */ 968 969 Cmd = Sub; 970 971 ConOutPrintf(_T("%*s"), SpacePad, _T("")); 972 ConOutPrintf(_T("%s \n"), OpString[Cmd->Type - C_OP_LOWEST]); 973 // FIXME: Can we have redirections on these operator-type commands? 974 975 SpacePad += 2; 976 977 Cmd = Cmd->Subcommands; 978 DumpCommand(Cmd, SpacePad); 979 ConOutPrintf(_T("%*s"), SpacePad, _T("")); 980 ConOutPuts(_T("CRLF \n")); 981 DumpCommand(Cmd->Next, SpacePad); 982 983 // NOTE: Next commands will remain indented. 984 985 #else 986 987 /* 988 * If this command is followed by another one, first display "CRLF". 989 * This also emulates the CRLF placement "bug" of Windows' CMD 990 * for the last two commands. 991 */ 992 if (Sub->Next) 993 { 994 ConOutPrintf(_T("%*s"), SpacePad, _T("")); 995 #ifndef MSCMD_ECHO_COMMAND_COMPAT 996 ConOutPuts(_T("CRLF\n")); 997 #else 998 ConOutPuts(_T("CRLF \n")); 999 #endif 1000 } 1001 DumpCommand(Sub, SpacePad); 1002 1003 #endif // defined(MSCMD_ECHO_COMMAND_COMPAT) && defined(MSCMD_PARSER_BUGS) 1004 } 1005 1006 return; 1007 } 1008 1009 case C_MULTI: 1010 case C_OR: 1011 case C_AND: 1012 case C_PIPE: 1013 { 1014 #ifndef MSCMD_ECHO_COMMAND_COMPAT 1015 ConOutPrintf(_T("%s\n"), OpString[Cmd->Type - C_OP_LOWEST]); 1016 #else 1017 ConOutPrintf(_T("%s \n"), OpString[Cmd->Type - C_OP_LOWEST]); 1018 #endif 1019 // FIXME: Can we have redirections on these operator-type commands? 1020 1021 SpacePad += 2; 1022 1023 Sub = Cmd->Subcommands; 1024 DumpCommand(Sub, SpacePad); 1025 /*DumpCommand*/DUMP(Sub->Next, SpacePad); 1026 return; 1027 } 1028 1029 case C_IF: 1030 { 1031 ConOutPuts(_T("if")); 1032 /* NOTE: IF cannot have leading redirections */ 1033 1034 if (Cmd->If.Flags & IFFLAG_IGNORECASE) 1035 ConOutPuts(_T(" /I")); 1036 1037 ConOutChar(_T('\n')); 1038 1039 SpacePad += 2; 1040 1041 /* 1042 * Show the IF command condition as a command. 1043 * If it is negated, indent the command more. 1044 */ 1045 if (Cmd->If.Flags & IFFLAG_NEGATE) 1046 { 1047 ConOutPrintf(_T("%*s"), SpacePad, _T("")); 1048 ConOutPuts(_T("not\n")); 1049 SpacePad += 2; 1050 } 1051 1052 ConOutPrintf(_T("%*s"), SpacePad, _T("")); 1053 1054 /* 1055 * Command name: 1056 * - Unary operator: its name is the command name, and its argument is the command argument. 1057 * - Binary operator: its LHS is the command name, its RHS is the command argument. 1058 * 1059 * Type: 1060 * Windows' CMD (Win2k3 / Win7-10) values are as follows: 1061 * CMDEXTVERSION Type: 0x32 / 0x34 1062 * ERRORLEVEL Type: 0x33 / 0x35 1063 * DEFINED Type: 0x34 / 0x36 1064 * EXIST Type: 0x35 / 0x37 1065 * == Type: 0x37 / 0x39 (String Comparison) 1066 * 1067 * For the following command: 1068 * NOT Type: 0x36 / 0x38 1069 * Windows only prints it without any type / redirection. 1070 * 1071 * For the following command: 1072 * EQU, NEQ, etc. Type: 0x38 / 0x3a (Generic Comparison) 1073 * Windows displays it as command of unknown type. 1074 */ 1075 #ifndef MSCMD_ECHO_COMMAND_COMPAT 1076 ConOutPrintf(_T("Cmd: %s Type: %x"), 1077 (Cmd->If.Operator <= IF_MAX_UNARY) ? 1078 IfOperatorString[Cmd->If.Operator] : 1079 Cmd->If.LeftArg, 1080 Cmd->If.Operator); 1081 #else 1082 ConOutPrintf(_T("Cmd: %s Type: %x "), 1083 (Cmd->If.Operator <= IF_MAX_UNARY) ? 1084 IfOperatorString[Cmd->If.Operator] : 1085 Cmd->If.LeftArg, 1086 Cmd->If.Operator); 1087 #endif 1088 /* Arguments */ 1089 #ifndef MSCMD_ECHO_COMMAND_COMPAT 1090 ConOutPrintf(_T(" Args: `%s'"), Cmd->If.RightArg); 1091 #else 1092 ConOutPrintf(_T("Args: `%s' "), Cmd->If.RightArg); 1093 #endif 1094 1095 ConOutChar(_T('\n')); 1096 1097 if (Cmd->If.Flags & IFFLAG_NEGATE) 1098 { 1099 SpacePad -= 2; 1100 } 1101 1102 Sub = Cmd->Subcommands; 1103 DumpCommand(Sub, SpacePad); 1104 if (Sub->Next) 1105 { 1106 ConOutPrintf(_T("%*s"), SpacePad - 2, _T("")); 1107 ConOutPuts(_T("else\n")); 1108 DumpCommand(Sub->Next, SpacePad); 1109 } 1110 return; 1111 } 1112 1113 case C_FOR: 1114 { 1115 ConOutPuts(_T("for")); 1116 /* NOTE: FOR cannot have leading redirections */ 1117 1118 if (Cmd->For.Switches & FOR_DIRS) ConOutPuts(_T(" /D")); 1119 if (Cmd->For.Switches & FOR_F) ConOutPuts(_T(" /F")); 1120 if (Cmd->For.Switches & FOR_LOOP) ConOutPuts(_T(" /L")); 1121 if (Cmd->For.Switches & FOR_RECURSIVE) ConOutPuts(_T(" /R")); 1122 if (Cmd->For.Params) 1123 ConOutPrintf(_T(" %s"), Cmd->For.Params); 1124 ConOutPrintf(_T(" %%%c in (%s) do\n"), Cmd->For.Variable, Cmd->For.List); 1125 /*DumpCommand*/DUMP(Cmd->Subcommands, SpacePad + 2); 1126 return; 1127 } 1128 1129 default: 1130 ConOutPrintf(_T("*** Unknown type: %x\n"), Cmd->Type); 1131 break; 1132 } 1133 1134 #undef DUMP 1135 } 1136 1137 /* 1138 * Reconstruct a parse tree into text form; used for echoing 1139 * batch file commands and FOR instances. 1140 */ 1141 VOID 1142 EchoCommand(PARSED_COMMAND *Cmd) 1143 { 1144 PARSED_COMMAND *Sub; 1145 REDIRECTION *Redir; 1146 1147 switch (Cmd->Type) 1148 { 1149 case C_COMMAND: 1150 { 1151 if (SubstituteForVars(Cmd->Command.First, TempBuf)) 1152 ConOutPrintf(_T("%s"), TempBuf); 1153 if (SubstituteForVars(Cmd->Command.Rest, TempBuf)) 1154 { 1155 ConOutPrintf(_T("%s"), TempBuf); 1156 #ifdef MSCMD_ECHO_COMMAND_COMPAT 1157 /* NOTE: For Windows compatibility, add a trailing space after printing the command parameter, if present */ 1158 if (*TempBuf) ConOutChar(_T(' ')); 1159 #endif 1160 } 1161 break; 1162 } 1163 1164 case C_QUIET: 1165 return; 1166 1167 case C_BLOCK: 1168 { 1169 BOOLEAN bIsFirstCmdCRLF; 1170 1171 ConOutChar(_T('(')); 1172 1173 Sub = Cmd->Subcommands; 1174 1175 bIsFirstCmdCRLF = (Sub && Sub->Next); 1176 1177 #if defined(MSCMD_ECHO_COMMAND_COMPAT) && defined(MSCMD_PARSER_BUGS) 1178 /* 1179 * We will emulate Windows' CMD handling of "CRLF" and "&" multi-command 1180 * enumeration within parenthesized command blocks. 1181 */ 1182 bIsFirstCmdCRLF = bIsFirstCmdCRLF && (Sub->Type != C_MULTI); 1183 #endif 1184 1185 /* 1186 * Single-command block: display all on one line. 1187 * Multi-command block: display commands on separate lines. 1188 */ 1189 if (bIsFirstCmdCRLF) 1190 ConOutChar(_T('\n')); 1191 1192 for (; Sub; Sub = Sub->Next) 1193 { 1194 EchoCommand(Sub); 1195 if (Sub->Next) 1196 #ifdef MSCMD_ECHO_COMMAND_COMPAT 1197 ConOutPuts(_T(" \n ")); 1198 #else 1199 ConOutChar(_T('\n')); 1200 #endif 1201 } 1202 1203 if (bIsFirstCmdCRLF) 1204 ConOutChar(_T('\n')); 1205 1206 #ifdef MSCMD_ECHO_COMMAND_COMPAT 1207 /* NOTE: For Windows compatibility, add a trailing space after printing the closing parenthesis */ 1208 ConOutPuts(_T(") ")); 1209 #else 1210 ConOutChar(_T(')')); 1211 #endif 1212 break; 1213 } 1214 1215 case C_MULTI: 1216 case C_OR: 1217 case C_AND: 1218 case C_PIPE: 1219 { 1220 Sub = Cmd->Subcommands; 1221 EchoCommand(Sub); 1222 ConOutPrintf(_T(" %s "), OpString[Cmd->Type - C_OP_LOWEST]); 1223 EchoCommand(Sub->Next); 1224 break; 1225 } 1226 1227 case C_IF: 1228 { 1229 ConOutPuts(_T("if")); 1230 if (Cmd->If.Flags & IFFLAG_IGNORECASE) 1231 ConOutPuts(_T(" /I")); 1232 if (Cmd->If.Flags & IFFLAG_NEGATE) 1233 ConOutPuts(_T(" not")); 1234 if (Cmd->If.LeftArg && SubstituteForVars(Cmd->If.LeftArg, TempBuf)) 1235 ConOutPrintf(_T(" %s"), TempBuf); 1236 ConOutPrintf(_T(" %s"), IfOperatorString[Cmd->If.Operator]); 1237 if (SubstituteForVars(Cmd->If.RightArg, TempBuf)) 1238 ConOutPrintf(_T(" %s "), TempBuf); 1239 Sub = Cmd->Subcommands; 1240 EchoCommand(Sub); 1241 if (Sub->Next) 1242 { 1243 ConOutPuts(_T(" else ")); 1244 EchoCommand(Sub->Next); 1245 } 1246 break; 1247 } 1248 1249 case C_FOR: 1250 { 1251 ConOutPuts(_T("for")); 1252 if (Cmd->For.Switches & FOR_DIRS) ConOutPuts(_T(" /D")); 1253 if (Cmd->For.Switches & FOR_F) ConOutPuts(_T(" /F")); 1254 if (Cmd->For.Switches & FOR_LOOP) ConOutPuts(_T(" /L")); 1255 if (Cmd->For.Switches & FOR_RECURSIVE) ConOutPuts(_T(" /R")); 1256 if (Cmd->For.Params) 1257 ConOutPrintf(_T(" %s"), Cmd->For.Params); 1258 if (Cmd->For.List && SubstituteForVars(Cmd->For.List, TempBuf)) 1259 ConOutPrintf(_T(" %%%c in (%s) do "), Cmd->For.Variable, TempBuf); 1260 else 1261 ConOutPrintf(_T(" %%%c in (%s) do "), Cmd->For.Variable, Cmd->For.List); 1262 EchoCommand(Cmd->Subcommands); 1263 break; 1264 } 1265 1266 default: 1267 ASSERT(FALSE); 1268 break; 1269 } 1270 1271 for (Redir = Cmd->Redirections; Redir; Redir = Redir->Next) 1272 { 1273 if (SubstituteForVars(Redir->Filename, TempBuf)) 1274 { 1275 #ifdef MSCMD_ECHO_COMMAND_COMPAT 1276 ConOutPrintf(_T("%c%s%s "), 1277 _T('0') + Redir->Number, 1278 RedirString[Redir->Mode], TempBuf); 1279 #else 1280 ConOutPrintf(_T(" %c%s%s"), 1281 _T('0') + Redir->Number, 1282 RedirString[Redir->Mode], TempBuf); 1283 #endif 1284 } 1285 } 1286 } 1287 1288 /* 1289 * "Unparse" a command into a text form suitable for passing to CMD /C. 1290 * Used for pipes. This is basically the same thing as EchoCommand, but 1291 * writing into a string instead of to standard output. 1292 */ 1293 TCHAR * 1294 Unparse(PARSED_COMMAND *Cmd, TCHAR *Out, TCHAR *OutEnd) 1295 { 1296 PARSED_COMMAND *Sub; 1297 REDIRECTION *Redir; 1298 1299 /* 1300 * Since this function has the annoying requirement that it must avoid 1301 * overflowing the supplied buffer, define some helper macros to make 1302 * this less painful. 1303 */ 1304 #define CHAR(Char) \ 1305 do { \ 1306 if (Out == OutEnd) return NULL; \ 1307 *Out++ = Char; \ 1308 } while (0) 1309 #define STRING(String) \ 1310 do { \ 1311 if (Out + _tcslen(String) > OutEnd) return NULL; \ 1312 Out = _stpcpy(Out, String); \ 1313 } while (0) 1314 #define PRINTF(Format, ...) \ 1315 do { \ 1316 UINT Len = _sntprintf(Out, OutEnd - Out, Format, __VA_ARGS__); \ 1317 if (Len > (UINT)(OutEnd - Out)) return NULL; \ 1318 Out += Len; \ 1319 } while (0) 1320 #define RECURSE(Subcommand) \ 1321 do { \ 1322 Out = Unparse(Subcommand, Out, OutEnd); \ 1323 if (!Out) return NULL; \ 1324 } while (0) 1325 1326 switch (Cmd->Type) 1327 { 1328 case C_COMMAND: 1329 /* This is fragile since there could be special characters, but 1330 * Windows doesn't bother escaping them, so for compatibility 1331 * we probably shouldn't do it either */ 1332 if (!SubstituteForVars(Cmd->Command.First, TempBuf)) return NULL; 1333 STRING(TempBuf); 1334 if (!SubstituteForVars(Cmd->Command.Rest, TempBuf)) return NULL; 1335 STRING(TempBuf); 1336 break; 1337 1338 case C_QUIET: 1339 CHAR(_T('@')); 1340 RECURSE(Cmd->Subcommands); 1341 break; 1342 1343 case C_BLOCK: 1344 CHAR(_T('(')); 1345 for (Sub = Cmd->Subcommands; Sub; Sub = Sub->Next) 1346 { 1347 RECURSE(Sub); 1348 if (Sub->Next) 1349 CHAR(_T('&')); 1350 } 1351 CHAR(_T(')')); 1352 break; 1353 1354 case C_MULTI: 1355 case C_OR: 1356 case C_AND: 1357 case C_PIPE: 1358 Sub = Cmd->Subcommands; 1359 RECURSE(Sub); 1360 PRINTF(_T(" %s "), OpString[Cmd->Type - C_OP_LOWEST]); 1361 RECURSE(Sub->Next); 1362 break; 1363 1364 case C_IF: 1365 STRING(_T("if")); 1366 if (Cmd->If.Flags & IFFLAG_IGNORECASE) 1367 STRING(_T(" /I")); 1368 if (Cmd->If.Flags & IFFLAG_NEGATE) 1369 STRING(_T(" not")); 1370 if (Cmd->If.LeftArg && SubstituteForVars(Cmd->If.LeftArg, TempBuf)) 1371 PRINTF(_T(" %s"), TempBuf); 1372 PRINTF(_T(" %s"), IfOperatorString[Cmd->If.Operator]); 1373 if (!SubstituteForVars(Cmd->If.RightArg, TempBuf)) return NULL; 1374 PRINTF(_T(" %s "), TempBuf); 1375 Sub = Cmd->Subcommands; 1376 RECURSE(Sub); 1377 if (Sub->Next) 1378 { 1379 STRING(_T(" else ")); 1380 RECURSE(Sub->Next); 1381 } 1382 break; 1383 1384 case C_FOR: 1385 STRING(_T("for")); 1386 if (Cmd->For.Switches & FOR_DIRS) STRING(_T(" /D")); 1387 if (Cmd->For.Switches & FOR_F) STRING(_T(" /F")); 1388 if (Cmd->For.Switches & FOR_LOOP) STRING(_T(" /L")); 1389 if (Cmd->For.Switches & FOR_RECURSIVE) STRING(_T(" /R")); 1390 if (Cmd->For.Params) 1391 PRINTF(_T(" %s"), Cmd->For.Params); 1392 if (Cmd->For.List && SubstituteForVars(Cmd->For.List, TempBuf)) 1393 PRINTF(_T(" %%%c in (%s) do "), Cmd->For.Variable, TempBuf); 1394 else 1395 PRINTF(_T(" %%%c in (%s) do "), Cmd->For.Variable, Cmd->For.List); 1396 RECURSE(Cmd->Subcommands); 1397 break; 1398 1399 default: 1400 ASSERT(FALSE); 1401 break; 1402 } 1403 1404 for (Redir = Cmd->Redirections; Redir; Redir = Redir->Next) 1405 { 1406 if (!SubstituteForVars(Redir->Filename, TempBuf)) 1407 return NULL; 1408 PRINTF(_T(" %c%s%s"), _T('0') + Redir->Number, 1409 RedirString[Redir->Mode], TempBuf); 1410 } 1411 return Out; 1412 1413 #undef CHAR 1414 #undef STRING 1415 #undef PRINTF 1416 #undef RECURSE 1417 } 1418 1419 VOID 1420 FreeCommand(PARSED_COMMAND *Cmd) 1421 { 1422 if (Cmd->Subcommands) 1423 FreeCommand(Cmd->Subcommands); 1424 if (Cmd->Next) 1425 FreeCommand(Cmd->Next); 1426 FreeRedirection(Cmd->Redirections); 1427 if (Cmd->Type == C_IF) 1428 { 1429 cmd_free(Cmd->If.LeftArg); 1430 cmd_free(Cmd->If.RightArg); 1431 } 1432 else if (Cmd->Type == C_FOR) 1433 { 1434 cmd_free(Cmd->For.Params); 1435 cmd_free(Cmd->For.List); 1436 } 1437 cmd_free(Cmd); 1438 } 1439