xref: /reactos/base/shell/cmd/parser.c (revision b5218987)
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