xref: /reactos/base/shell/cmd/parser.c (revision 845faec4)
1 /*
2  *  PARSER.C - command parsing.
3  */
4 
5 #include "precomp.h"
6 
7 #define C_OP_LOWEST C_MULTI
8 #define C_OP_HIGHEST C_PIPE
9 static const TCHAR OpString[][3] = { _T("&"), _T("||"), _T("&&"), _T("|") };
10 
11 static const TCHAR RedirString[][3] = { _T("<"), _T(">"), _T(">>") };
12 
13 static const TCHAR *const IfOperatorString[] =
14 {
15     _T("cmdextversion"),
16     _T("defined"),
17     _T("errorlevel"),
18     _T("exist"),
19 #define IF_MAX_UNARY IF_EXIST
20     _T("=="),
21     _T("equ"),
22     _T("gtr"),
23     _T("geq"),
24     _T("lss"),
25     _T("leq"),
26     _T("neq"),
27 #define IF_MAX_COMPARISON IF_NEQ
28 };
29 
30 /* These three characters act like spaces to the parser in most contexts */
31 #define STANDARD_SEPS _T(",;=")
32 
33 static BOOL IsSeparator(TCHAR Char)
34 {
35     return _istspace(Char) || (Char && _tcschr(STANDARD_SEPS, Char));
36 }
37 
38 enum
39 {
40     TOK_END,
41     TOK_NORMAL,
42     TOK_OPERATOR,
43     TOK_REDIRECTION,
44     TOK_BEGIN_BLOCK,
45     TOK_END_BLOCK
46 };
47 
48 static BOOL bParseError;
49 static BOOL bLineContinuations;
50 static TCHAR ParseLine[CMDLINE_LENGTH];
51 static TCHAR *ParsePos;
52 static TCHAR CurChar;
53 
54 static TCHAR CurrentToken[CMDLINE_LENGTH];
55 static int CurrentTokenType;
56 static int InsideBlock;
57 
58 static TCHAR ParseChar(void)
59 {
60     TCHAR Char;
61 
62     if (bParseError)
63         return CurChar = 0;
64 
65 restart:
66     /*
67      * Although CRs can be injected into a line via an environment
68      * variable substitution, the parser ignores them - they won't
69      * even separate tokens.
70      */
71     do
72     {
73         Char = *ParsePos++;
74     }
75     while (Char == _T('\r'));
76 
77     if (!Char)
78     {
79         ParsePos--;
80         if (bLineContinuations)
81         {
82             if (!ReadLine(ParseLine, TRUE))
83             {
84                 /* ^C pressed, or line was too long */
85                 bParseError = TRUE;
86             }
87             else if (*(ParsePos = ParseLine))
88             {
89                 goto restart;
90             }
91         }
92     }
93     return CurChar = Char;
94 }
95 
96 static void ParseError(void)
97 {
98     error_syntax(CurrentTokenType != TOK_END ? CurrentToken : NULL);
99     bParseError = TRUE;
100 }
101 
102 /*
103  * Yes, cmd has a Lexical Analyzer. Whenever the parser gives an "xxx was
104  * unexpected at this time." message, it shows what the last token read was.
105  */
106 static int ParseToken(TCHAR ExtraEnd, TCHAR *Separators)
107 {
108     TCHAR *Out = CurrentToken;
109     TCHAR Char;
110     int Type;
111     BOOL bInQuote = FALSE;
112 
113     for (Char = CurChar; Char && Char != _T('\n'); Char = ParseChar())
114     {
115         bInQuote ^= (Char == _T('"'));
116         if (!bInQuote)
117         {
118             if (Separators != NULL)
119             {
120                 if (_istspace(Char) || _tcschr(Separators, Char))
121                 {
122                     /* Skip leading separators */
123                     if (Out == CurrentToken)
124                         continue;
125                     break;
126                 }
127             }
128 
129             /* Check for numbered redirection */
130             if ((Char >= _T('0') && Char <= _T('9') &&
131                    (ParsePos == &ParseLine[1] || IsSeparator(ParsePos[-2]))
132                    && (*ParsePos == _T('<') || *ParsePos == _T('>'))))
133             {
134                 break;
135             }
136 
137             if (Char == ExtraEnd)
138                 break;
139             if (InsideBlock && Char == _T(')'))
140                 break;
141             if (_tcschr(_T("&|<>"), Char))
142                 break;
143 
144             if (Char == _T('^'))
145             {
146                 Char = ParseChar();
147                 /* Eat up a \n, allowing line continuation */
148                 if (Char == _T('\n'))
149                     Char = ParseChar();
150                 /* Next character is a forced literal */
151             }
152         }
153         if (Out == &CurrentToken[CMDLINE_LENGTH - 1])
154             break;
155         *Out++ = Char;
156     }
157 
158     /* Check if we got at least one character before reaching a special one.
159      * If so, return them and leave the special for the next call. */
160     if (Out != CurrentToken)
161     {
162         Type = TOK_NORMAL;
163     }
164     else if (Char == _T('('))
165     {
166         Type = TOK_BEGIN_BLOCK;
167         *Out++ = Char;
168         ParseChar();
169     }
170     else if (Char == _T(')'))
171     {
172         Type = TOK_END_BLOCK;
173         *Out++ = Char;
174         ParseChar();
175     }
176     else if (Char == _T('&') || Char == _T('|'))
177     {
178         Type = TOK_OPERATOR;
179         *Out++ = Char;
180         Char = ParseChar();
181         /* check for && or || */
182         if (Char == Out[-1])
183         {
184             *Out++ = Char;
185             ParseChar();
186         }
187     }
188     else if ((Char >= _T('0') && Char <= _T('9'))
189              || (Char == _T('<') || Char == _T('>')))
190     {
191         Type = TOK_REDIRECTION;
192         if (Char >= _T('0') && Char <= _T('9'))
193         {
194             *Out++ = Char;
195             Char = ParseChar();
196         }
197         *Out++ = Char;
198         Char = ParseChar();
199         if (Char == Out[-1])
200         {
201             /* Strangely, the tokenizer allows << as well as >>... (it
202              * will cause an error when trying to parse it though) */
203             *Out++ = Char;
204             Char = ParseChar();
205         }
206         if (Char == _T('&'))
207         {
208             *Out++ = Char;
209             while (IsSeparator(Char = ParseChar()))
210                 ;
211             if (Char >= _T('0') && Char <= _T('9'))
212             {
213                 *Out++ = Char;
214                 ParseChar();
215             }
216         }
217     }
218     else
219     {
220         Type = TOK_END;
221     }
222     *Out = _T('\0');
223     return CurrentTokenType = Type;
224 }
225 
226 static BOOL ParseRedirection(REDIRECTION **List)
227 {
228     TCHAR *Tok = CurrentToken;
229     BYTE Number;
230     REDIR_MODE RedirMode;
231     REDIRECTION *Redir;
232 
233     if (*Tok >= _T('0') && *Tok <= _T('9'))
234         Number = *Tok++ - _T('0');
235     else
236         Number = *Tok == _T('<') ? 0 : 1;
237 
238     if (*Tok++ == _T('<'))
239     {
240         RedirMode = REDIR_READ;
241         if (*Tok == _T('<'))
242             goto fail;
243     }
244     else
245     {
246         RedirMode = REDIR_WRITE;
247         if (*Tok == _T('>'))
248         {
249             RedirMode = REDIR_APPEND;
250             Tok++;
251         }
252     }
253 
254     if (!*Tok)
255     {
256         /* The file name was not part of this token, so it'll be the next one */
257         if (ParseToken(0, STANDARD_SEPS) != TOK_NORMAL)
258             goto fail;
259         Tok = CurrentToken;
260     }
261 
262     /* If a redirection for this handle number already exists, delete it */
263     while ((Redir = *List))
264     {
265         if (Redir->Number == Number)
266         {
267             *List = Redir->Next;
268             cmd_free(Redir);
269             continue;
270         }
271         List = &Redir->Next;
272     }
273 
274     Redir = cmd_alloc(FIELD_OFFSET(REDIRECTION, Filename[_tcslen(Tok) + 1]));
275     Redir->Next = NULL;
276     Redir->OldHandle = INVALID_HANDLE_VALUE;
277     Redir->Number = Number;
278     Redir->Mode = RedirMode;
279     _tcscpy(Redir->Filename, Tok);
280     *List = Redir;
281     return TRUE;
282 
283 fail:
284     ParseError();
285     FreeRedirection(*List);
286     *List = NULL;
287     return FALSE;
288 }
289 
290 static PARSED_COMMAND *ParseCommandOp(int OpType);
291 
292 /* Parse a parenthesized block */
293 static PARSED_COMMAND *ParseBlock(REDIRECTION *RedirList)
294 {
295     PARSED_COMMAND *Cmd, *Sub, **NextPtr;
296     Cmd = cmd_alloc(sizeof(PARSED_COMMAND));
297     Cmd->Type = C_BLOCK;
298     Cmd->Next = NULL;
299     Cmd->Subcommands = NULL;
300     Cmd->Redirections = RedirList;
301 
302     /* Read the block contents */
303     NextPtr = &Cmd->Subcommands;
304     InsideBlock++;
305     while (1)
306     {
307         Sub = ParseCommandOp(C_OP_LOWEST);
308         if (Sub)
309         {
310             *NextPtr = Sub;
311             NextPtr = &Sub->Next;
312         }
313         else if (bParseError)
314         {
315             InsideBlock--;
316             FreeCommand(Cmd);
317             return NULL;
318         }
319 
320         if (CurrentTokenType == TOK_END_BLOCK)
321             break;
322 
323         /* Skip past the \n */
324         ParseChar();
325     }
326     InsideBlock--;
327 
328     /* Process any trailing redirections */
329     while (ParseToken(0, STANDARD_SEPS) == TOK_REDIRECTION)
330     {
331         if (!ParseRedirection(&Cmd->Redirections))
332         {
333             FreeCommand(Cmd);
334             return NULL;
335         }
336     }
337     return Cmd;
338 }
339 
340 /* Parse an IF statement */
341 static PARSED_COMMAND *ParseIf(void)
342 {
343     PARSED_COMMAND *Cmd = cmd_alloc(sizeof(PARSED_COMMAND));
344     int Type;
345     memset(Cmd, 0, sizeof(PARSED_COMMAND));
346     Cmd->Type = C_IF;
347 
348     Type = CurrentTokenType;
349     if (_tcsicmp(CurrentToken, _T("/I")) == 0)
350     {
351         Cmd->If.Flags |= IFFLAG_IGNORECASE;
352         Type = ParseToken(0, STANDARD_SEPS);
353     }
354     if (_tcsicmp(CurrentToken, _T("not")) == 0)
355     {
356         Cmd->If.Flags |= IFFLAG_NEGATE;
357         Type = ParseToken(0, STANDARD_SEPS);
358     }
359 
360     if (Type != TOK_NORMAL)
361     {
362         FreeCommand(Cmd);
363         ParseError();
364         return NULL;
365     }
366 
367     /* Check for unary operators */
368     for (; Cmd->If.Operator <= IF_MAX_UNARY; Cmd->If.Operator++)
369     {
370         if (_tcsicmp(CurrentToken, IfOperatorString[Cmd->If.Operator]) == 0)
371         {
372             if (ParseToken(0, STANDARD_SEPS) != TOK_NORMAL)
373             {
374                 FreeCommand(Cmd);
375                 ParseError();
376                 return NULL;
377             }
378             Cmd->If.RightArg = cmd_dup(CurrentToken);
379             goto condition_done;
380         }
381     }
382 
383     /* It must be a two-argument (comparison) operator. It could be ==, so
384      * the equals sign can't be treated as whitespace here. */
385     Cmd->If.LeftArg = cmd_dup(CurrentToken);
386     ParseToken(0, _T(",;"));
387 
388     /* The right argument can come immediately after == */
389     if (_tcsnicmp(CurrentToken, _T("=="), 2) == 0 && CurrentToken[2])
390     {
391         Cmd->If.RightArg = cmd_dup(&CurrentToken[2]);
392         goto condition_done;
393     }
394 
395     for (; Cmd->If.Operator <= IF_MAX_COMPARISON; Cmd->If.Operator++)
396     {
397         if (_tcsicmp(CurrentToken, IfOperatorString[Cmd->If.Operator]) == 0)
398         {
399             if (ParseToken(0, STANDARD_SEPS) != TOK_NORMAL)
400                 break;
401             Cmd->If.RightArg = cmd_dup(CurrentToken);
402             goto condition_done;
403         }
404     }
405     FreeCommand(Cmd);
406     ParseError();
407     return NULL;
408 
409 condition_done:
410     Cmd->Subcommands = ParseCommandOp(C_OP_LOWEST);
411     if (Cmd->Subcommands == NULL)
412     {
413         FreeCommand(Cmd);
414         return NULL;
415     }
416     if (_tcsicmp(CurrentToken, _T("else")) == 0)
417     {
418         Cmd->Subcommands->Next = ParseCommandOp(C_OP_LOWEST);
419         if (Cmd->Subcommands->Next == NULL)
420         {
421             FreeCommand(Cmd);
422             return NULL;
423         }
424     }
425 
426     return Cmd;
427 }
428 
429 /*
430  * Parse a FOR command.
431  * Syntax is: FOR [options] %var IN (list) DO command
432  */
433 static PARSED_COMMAND *ParseFor(void)
434 {
435     PARSED_COMMAND *Cmd = cmd_alloc(sizeof(PARSED_COMMAND));
436     TCHAR List[CMDLINE_LENGTH];
437     TCHAR *Pos = List;
438 
439     memset(Cmd, 0, sizeof(PARSED_COMMAND));
440     Cmd->Type = C_FOR;
441 
442     while (1)
443     {
444         if (_tcsicmp(CurrentToken, _T("/D")) == 0)
445             Cmd->For.Switches |= FOR_DIRS;
446         else if (_tcsicmp(CurrentToken, _T("/F")) == 0)
447         {
448             Cmd->For.Switches |= FOR_F;
449             if (!Cmd->For.Params)
450             {
451                 ParseToken(0, STANDARD_SEPS);
452                 if (CurrentToken[0] == _T('/') || CurrentToken[0] == _T('%'))
453                     break;
454                 Cmd->For.Params = cmd_dup(CurrentToken);
455             }
456         }
457         else if (_tcsicmp(CurrentToken, _T("/L")) == 0)
458             Cmd->For.Switches |= FOR_LOOP;
459         else if (_tcsicmp(CurrentToken, _T("/R")) == 0)
460         {
461             Cmd->For.Switches |= FOR_RECURSIVE;
462             if (!Cmd->For.Params)
463             {
464                 ParseToken(0, STANDARD_SEPS);
465                 if (CurrentToken[0] == _T('/') || CurrentToken[0] == _T('%'))
466                     break;
467                 StripQuotes(CurrentToken);
468                 Cmd->For.Params = cmd_dup(CurrentToken);
469             }
470         }
471         else
472             break;
473         ParseToken(0, STANDARD_SEPS);
474     }
475 
476     /* Make sure there aren't two different switches specified
477      * at the same time, unless they're /D and /R */
478     if ((Cmd->For.Switches & (Cmd->For.Switches - 1)) != 0
479         && Cmd->For.Switches != (FOR_DIRS | FOR_RECURSIVE))
480     {
481         goto error;
482     }
483 
484     /* Variable name should be % and just one other character */
485     if (CurrentToken[0] != _T('%') || _tcslen(CurrentToken) != 2)
486         goto error;
487     Cmd->For.Variable = CurrentToken[1];
488 
489     ParseToken(0, STANDARD_SEPS);
490     if (_tcsicmp(CurrentToken, _T("in")) != 0)
491         goto error;
492 
493     if (ParseToken(_T('('), STANDARD_SEPS) != TOK_BEGIN_BLOCK)
494         goto error;
495 
496     while (1)
497     {
498         int Type;
499 
500         /* Pretend we're inside a block so the tokenizer will stop on ')' */
501         InsideBlock++;
502         Type = ParseToken(0, STANDARD_SEPS);
503         InsideBlock--;
504 
505         if (Type == TOK_END_BLOCK)
506             break;
507 
508         if (Type == TOK_END)
509         {
510             /* Skip past the \n */
511             ParseChar();
512             continue;
513         }
514 
515         if (Type != TOK_NORMAL)
516             goto error;
517 
518         if (Pos != List)
519             *Pos++ = _T(' ');
520 
521         if (Pos + _tcslen(CurrentToken) >= &List[CMDLINE_LENGTH])
522             goto error;
523         Pos = _stpcpy(Pos, CurrentToken);
524     }
525     *Pos = _T('\0');
526     Cmd->For.List = cmd_dup(List);
527 
528     ParseToken(0, STANDARD_SEPS);
529     if (_tcsicmp(CurrentToken, _T("do")) != 0)
530         goto error;
531 
532     Cmd->Subcommands = ParseCommandOp(C_OP_LOWEST);
533     if (Cmd->Subcommands == NULL)
534     {
535         FreeCommand(Cmd);
536         return NULL;
537     }
538 
539     return Cmd;
540 
541 error:
542     FreeCommand(Cmd);
543     ParseError();
544     return NULL;
545 }
546 
547 /* Parse a REM command */
548 static PARSED_COMMAND *ParseRem(void)
549 {
550     /* Just ignore the rest of the line */
551     while (CurChar && CurChar != _T('\n'))
552         ParseChar();
553     return NULL;
554 }
555 
556 static DECLSPEC_NOINLINE PARSED_COMMAND *ParseCommandPart(REDIRECTION *RedirList)
557 {
558     TCHAR ParsedLine[CMDLINE_LENGTH];
559     PARSED_COMMAND *Cmd;
560     PARSED_COMMAND *(*Func)(void);
561 
562     TCHAR *Pos = _stpcpy(ParsedLine, CurrentToken) + 1;
563     DWORD_PTR TailOffset = Pos - ParsedLine;
564 
565     /* Check for special forms */
566     if ((Func = ParseFor, _tcsicmp(ParsedLine, _T("for")) == 0) ||
567         (Func = ParseIf,  _tcsicmp(ParsedLine, _T("if")) == 0)  ||
568         (Func = ParseRem, _tcsicmp(ParsedLine, _T("rem")) == 0))
569     {
570         ParseToken(0, STANDARD_SEPS);
571         /* Do special parsing only if it's not followed by /? */
572         if (_tcscmp(CurrentToken, _T("/?")) != 0)
573         {
574             if (RedirList)
575             {
576                 ParseError();
577                 FreeRedirection(RedirList);
578                 return NULL;
579             }
580             return Func();
581         }
582         Pos = _stpcpy(Pos, _T(" /?"));
583     }
584 
585     /* Now get the tail */
586     while (1)
587     {
588         int Type = ParseToken(0, NULL);
589         if (Type == TOK_NORMAL)
590         {
591             if (Pos + _tcslen(CurrentToken) >= &ParsedLine[CMDLINE_LENGTH])
592             {
593                 ParseError();
594                 FreeRedirection(RedirList);
595                 return NULL;
596             }
597             Pos = _stpcpy(Pos, CurrentToken);
598         }
599         else if (Type == TOK_REDIRECTION)
600         {
601             if (!ParseRedirection(&RedirList))
602                 return NULL;
603         }
604         else
605         {
606             break;
607         }
608     }
609     *Pos++ = _T('\0');
610 
611     Cmd = cmd_alloc(FIELD_OFFSET(PARSED_COMMAND, Command.First[Pos - ParsedLine]));
612     Cmd->Type = C_COMMAND;
613     Cmd->Next = NULL;
614     Cmd->Subcommands = NULL;
615     Cmd->Redirections = RedirList;
616     memcpy(Cmd->Command.First, ParsedLine, (Pos - ParsedLine) * sizeof(TCHAR));
617     Cmd->Command.Rest = Cmd->Command.First + TailOffset;
618     return Cmd;
619 }
620 
621 static PARSED_COMMAND *ParsePrimary(void)
622 {
623     REDIRECTION *RedirList = NULL;
624     int Type;
625 
626     while (IsSeparator(CurChar))
627     {
628         if (CurChar == _T('\n'))
629             return NULL;
630         ParseChar();
631     }
632 
633     if (!CurChar)
634         return NULL;
635 
636     if (CurChar == _T(':'))
637     {
638         /* "Ignore" the rest of the line.
639          * (Line continuations will still be parsed, though.) */
640         while (ParseToken(0, NULL) != TOK_END)
641             ;
642         return NULL;
643     }
644 
645     if (CurChar == _T('@'))
646     {
647         PARSED_COMMAND *Cmd;
648         ParseChar();
649         Cmd = cmd_alloc(sizeof(PARSED_COMMAND));
650         Cmd->Type = C_QUIET;
651         Cmd->Next = NULL;
652         /* @ acts like a unary operator with low precedence,
653          * so call the top-level parser */
654         Cmd->Subcommands = ParseCommandOp(C_OP_LOWEST);
655         Cmd->Redirections = NULL;
656         return Cmd;
657     }
658 
659     /* Process leading redirections and get the head of the command */
660     while ((Type = ParseToken(_T('('), STANDARD_SEPS)) == TOK_REDIRECTION)
661     {
662         if (!ParseRedirection(&RedirList))
663             return NULL;
664     }
665 
666     if (Type == TOK_NORMAL)
667         return ParseCommandPart(RedirList);
668     else if (Type == TOK_BEGIN_BLOCK)
669         return ParseBlock(RedirList);
670     else if (Type == TOK_END_BLOCK && !RedirList)
671         return NULL;
672 
673     ParseError();
674     FreeRedirection(RedirList);
675     return NULL;
676 }
677 
678 static PARSED_COMMAND *ParseCommandOp(int OpType)
679 {
680     PARSED_COMMAND *Cmd, *Left, *Right;
681 
682     if (OpType == C_OP_HIGHEST)
683         Cmd = ParsePrimary();
684     else
685         Cmd = ParseCommandOp(OpType + 1);
686 
687     if (Cmd && !_tcscmp(CurrentToken, OpString[OpType - C_OP_LOWEST]))
688     {
689         Left = Cmd;
690         Right = ParseCommandOp(OpType);
691         if (!Right)
692         {
693             if (!bParseError)
694             {
695                 /* & is allowed to have an empty RHS */
696                 if (OpType == C_MULTI)
697                     return Left;
698                 ParseError();
699             }
700             FreeCommand(Left);
701             return NULL;
702         }
703 
704         Cmd = cmd_alloc(sizeof(PARSED_COMMAND));
705         Cmd->Type = OpType;
706         Cmd->Next = NULL;
707         Cmd->Redirections = NULL;
708         Cmd->Subcommands = Left;
709         Left->Next = Right;
710         Right->Next = NULL;
711     }
712 
713     return Cmd;
714 }
715 
716 PARSED_COMMAND *
717 ParseCommand(LPTSTR Line)
718 {
719     PARSED_COMMAND *Cmd;
720 
721     if (Line)
722     {
723         if (!SubstituteVars(Line, ParseLine, _T('%')))
724             return NULL;
725         bLineContinuations = FALSE;
726     }
727     else
728     {
729         if (!ReadLine(ParseLine, FALSE))
730             return NULL;
731         bLineContinuations = TRUE;
732     }
733     bParseError = FALSE;
734     ParsePos = ParseLine;
735     CurChar = _T(' ');
736 
737     Cmd = ParseCommandOp(C_OP_LOWEST);
738     if (Cmd)
739     {
740         if (CurrentTokenType != TOK_END)
741             ParseError();
742         if (bParseError)
743         {
744             FreeCommand(Cmd);
745             Cmd = NULL;
746         }
747         bIgnoreEcho = FALSE;
748     }
749     else
750     {
751         bIgnoreEcho = TRUE;
752     }
753     return Cmd;
754 }
755 
756 
757 /*
758  * Reconstruct a parse tree into text form; used for echoing
759  * batch file commands and FOR instances.
760  */
761 VOID
762 EchoCommand(PARSED_COMMAND *Cmd)
763 {
764     TCHAR Buf[CMDLINE_LENGTH];
765     PARSED_COMMAND *Sub;
766     REDIRECTION *Redir;
767 
768     switch (Cmd->Type)
769     {
770     case C_COMMAND:
771         if (SubstituteForVars(Cmd->Command.First, Buf))
772             ConOutPrintf(_T("%s"), Buf);
773         if (SubstituteForVars(Cmd->Command.Rest, Buf))
774             ConOutPrintf(_T("%s"), Buf);
775         break;
776     case C_QUIET:
777         return;
778     case C_BLOCK:
779         ConOutChar(_T('('));
780         Sub = Cmd->Subcommands;
781         if (Sub && !Sub->Next)
782         {
783             /* Single-command block: display all on one line */
784             EchoCommand(Sub);
785         }
786         else if (Sub)
787         {
788             /* Multi-command block: display parenthesis on separate lines */
789             ConOutChar(_T('\n'));
790             do
791             {
792                 EchoCommand(Sub);
793                 ConOutChar(_T('\n'));
794                 Sub = Sub->Next;
795             } while (Sub);
796         }
797         ConOutChar(_T(')'));
798         break;
799     case C_MULTI:
800     case C_IFFAILURE:
801     case C_IFSUCCESS:
802     case C_PIPE:
803         Sub = Cmd->Subcommands;
804         EchoCommand(Sub);
805         ConOutPrintf(_T(" %s "), OpString[Cmd->Type - C_OP_LOWEST]);
806         EchoCommand(Sub->Next);
807         break;
808     case C_IF:
809         ConOutPrintf(_T("if"));
810         if (Cmd->If.Flags & IFFLAG_IGNORECASE)
811             ConOutPrintf(_T(" /I"));
812         if (Cmd->If.Flags & IFFLAG_NEGATE)
813             ConOutPrintf(_T(" not"));
814         if (Cmd->If.LeftArg && SubstituteForVars(Cmd->If.LeftArg, Buf))
815             ConOutPrintf(_T(" %s"), Buf);
816         ConOutPrintf(_T(" %s"), IfOperatorString[Cmd->If.Operator]);
817         if (SubstituteForVars(Cmd->If.RightArg, Buf))
818             ConOutPrintf(_T(" %s "), Buf);
819         Sub = Cmd->Subcommands;
820         EchoCommand(Sub);
821         if (Sub->Next)
822         {
823             ConOutPrintf(_T(" else "));
824             EchoCommand(Sub->Next);
825         }
826         break;
827     case C_FOR:
828         ConOutPrintf(_T("for"));
829         if (Cmd->For.Switches & FOR_DIRS)      ConOutPrintf(_T(" /D"));
830         if (Cmd->For.Switches & FOR_F)         ConOutPrintf(_T(" /F"));
831         if (Cmd->For.Switches & FOR_LOOP)      ConOutPrintf(_T(" /L"));
832         if (Cmd->For.Switches & FOR_RECURSIVE) ConOutPrintf(_T(" /R"));
833         if (Cmd->For.Params)
834             ConOutPrintf(_T(" %s"), Cmd->For.Params);
835         ConOutPrintf(_T(" %%%c in (%s) do "), Cmd->For.Variable, Cmd->For.List);
836         EchoCommand(Cmd->Subcommands);
837         break;
838     }
839 
840     for (Redir = Cmd->Redirections; Redir; Redir = Redir->Next)
841     {
842         if (SubstituteForVars(Redir->Filename, Buf))
843             ConOutPrintf(_T(" %c%s%s"), _T('0') + Redir->Number,
844                 RedirString[Redir->Mode], Buf);
845     }
846 }
847 
848 /*
849  * "Unparse" a command into a text form suitable for passing to CMD /C.
850  * Used for pipes. This is basically the same thing as EchoCommand, but
851  * writing into a string instead of to standard output.
852  */
853 TCHAR *
854 Unparse(PARSED_COMMAND *Cmd, TCHAR *Out, TCHAR *OutEnd)
855 {
856     TCHAR Buf[CMDLINE_LENGTH];
857     PARSED_COMMAND *Sub;
858     REDIRECTION *Redir;
859 
860 /*
861  * Since this function has the annoying requirement that it must avoid
862  * overflowing the supplied buffer, define some helper macros to make
863  * this less painful.
864  */
865 #define CHAR(Char) { \
866     if (Out == OutEnd) return NULL; \
867     *Out++ = Char; }
868 #define STRING(String) { \
869     if (Out + _tcslen(String) > OutEnd) return NULL; \
870     Out = _stpcpy(Out, String); }
871 #define PRINTF(Format, ...) { \
872     UINT Len = _sntprintf(Out, OutEnd - Out, Format, __VA_ARGS__); \
873     if (Len > (UINT)(OutEnd - Out)) return NULL; \
874     Out += Len; }
875 #define RECURSE(Subcommand) { \
876     Out = Unparse(Subcommand, Out, OutEnd); \
877     if (!Out) return NULL; }
878 
879     switch (Cmd->Type)
880     {
881     case C_COMMAND:
882         /* This is fragile since there could be special characters, but
883          * Windows doesn't bother escaping them, so for compatibility
884          * we probably shouldn't do it either */
885         if (!SubstituteForVars(Cmd->Command.First, Buf)) return NULL;
886         STRING(Buf)
887         if (!SubstituteForVars(Cmd->Command.Rest, Buf)) return NULL;
888         STRING(Buf)
889         break;
890     case C_QUIET:
891         CHAR(_T('@'))
892         RECURSE(Cmd->Subcommands)
893         break;
894     case C_BLOCK:
895         CHAR(_T('('))
896         for (Sub = Cmd->Subcommands; Sub; Sub = Sub->Next)
897         {
898             RECURSE(Sub)
899             if (Sub->Next)
900                 CHAR(_T('&'))
901         }
902         CHAR(_T(')'))
903         break;
904     case C_MULTI:
905     case C_IFFAILURE:
906     case C_IFSUCCESS:
907     case C_PIPE:
908         Sub = Cmd->Subcommands;
909         RECURSE(Sub)
910         PRINTF(_T(" %s "), OpString[Cmd->Type - C_OP_LOWEST])
911         RECURSE(Sub->Next)
912         break;
913     case C_IF:
914         STRING(_T("if"))
915         if (Cmd->If.Flags & IFFLAG_IGNORECASE)
916             STRING(_T(" /I"))
917         if (Cmd->If.Flags & IFFLAG_NEGATE)
918             STRING(_T(" not"))
919         if (Cmd->If.LeftArg && SubstituteForVars(Cmd->If.LeftArg, Buf))
920             PRINTF(_T(" %s"), Buf)
921         PRINTF(_T(" %s"), IfOperatorString[Cmd->If.Operator]);
922         if (!SubstituteForVars(Cmd->If.RightArg, Buf)) return NULL;
923         PRINTF(_T(" %s "), Buf)
924         Sub = Cmd->Subcommands;
925         RECURSE(Sub)
926         if (Sub->Next)
927         {
928             STRING(_T(" else "))
929             RECURSE(Sub->Next)
930         }
931         break;
932     case C_FOR:
933         STRING(_T("for"))
934         if (Cmd->For.Switches & FOR_DIRS)      STRING(_T(" /D"))
935         if (Cmd->For.Switches & FOR_F)         STRING(_T(" /F"))
936         if (Cmd->For.Switches & FOR_LOOP)      STRING(_T(" /L"))
937         if (Cmd->For.Switches & FOR_RECURSIVE) STRING(_T(" /R"))
938         if (Cmd->For.Params)
939             PRINTF(_T(" %s"), Cmd->For.Params)
940         PRINTF(_T(" %%%c in (%s) do "), Cmd->For.Variable, Cmd->For.List)
941         RECURSE(Cmd->Subcommands)
942         break;
943     }
944 
945     for (Redir = Cmd->Redirections; Redir; Redir = Redir->Next)
946     {
947         if (!SubstituteForVars(Redir->Filename, Buf)) return NULL;
948         PRINTF(_T(" %c%s%s"), _T('0') + Redir->Number,
949             RedirString[Redir->Mode], Buf)
950     }
951     return Out;
952 }
953 
954 VOID
955 FreeCommand(PARSED_COMMAND *Cmd)
956 {
957     if (Cmd->Subcommands)
958         FreeCommand(Cmd->Subcommands);
959     if (Cmd->Next)
960         FreeCommand(Cmd->Next);
961     FreeRedirection(Cmd->Redirections);
962     if (Cmd->Type == C_IF)
963     {
964         cmd_free(Cmd->If.LeftArg);
965         cmd_free(Cmd->If.RightArg);
966     }
967     else if (Cmd->Type == C_FOR)
968     {
969         cmd_free(Cmd->For.Params);
970         cmd_free(Cmd->For.List);
971     }
972     cmd_free(Cmd);
973 }
974