2 * PARSER.C - command parsing.
8 #define C_OP_LOWEST C_MULTI
9 #define C_OP_HIGHEST C_PIPE
10 static const TCHAR OpString
[][3] = { _T("&"), _T("||"), _T("&&"), _T("|") };
12 static const TCHAR RedirString
[][3] = { _T("<"), _T(">"), _T(">>") };
14 static const TCHAR
*const IfOperatorString
[] = {
19 #define IF_MAX_UNARY IF_EXIST
27 #define IF_MAX_COMPARISON IF_NEQ
30 /* These three characters act like spaces to the parser in most contexts */
31 #define STANDARD_SEPS _T(",;=")
33 static BOOL
IsSeparator(TCHAR Char
)
35 return _istspace(Char
) || (Char
&& _tcschr(STANDARD_SEPS
, Char
));
38 enum { TOK_END
, TOK_NORMAL
, TOK_OPERATOR
, TOK_REDIRECTION
,
39 TOK_BEGIN_BLOCK
, TOK_END_BLOCK
};
41 static BOOL bParseError
;
42 static BOOL bLineContinuations
;
43 static TCHAR ParseLine
[CMDLINE_LENGTH
];
44 static TCHAR
*ParsePos
;
47 static TCHAR CurrentToken
[CMDLINE_LENGTH
];
48 static int CurrentTokenType
;
49 static int InsideBlock
;
51 static TCHAR
ParseChar()
59 /* Although CRs can be injected into a line via an environment
60 * variable substitution, the parser ignores them - they won't
61 * even separate tokens. */
64 while (Char
== _T('\r'));
69 if (bLineContinuations
)
71 if (!ReadLine(ParseLine
, TRUE
))
73 /* ^C pressed, or line was too long */
76 else if (*(ParsePos
= ParseLine
))
82 return CurChar
= Char
;
85 static void ParseError()
87 error_syntax(CurrentTokenType
!= TOK_END
? CurrentToken
: NULL
);
91 /* Yes, cmd has a Lexical Analyzer. Whenever the parser gives an "xxx was
92 * unexpected at this time." message, it shows what the last token read was */
93 static int ParseToken(TCHAR ExtraEnd
, TCHAR
*Separators
)
95 TCHAR
*Out
= CurrentToken
;
98 BOOL bInQuote
= FALSE
;
100 for (Char
= CurChar
; Char
&& Char
!= _T('\n'); Char
= ParseChar())
102 bInQuote
^= (Char
== _T('"'));
105 if (Separators
!= NULL
)
107 if (_istspace(Char
) || _tcschr(Separators
, Char
))
109 /* Skip leading separators */
110 if (Out
== CurrentToken
)
116 /* Check for numbered redirection */
117 if ((Char
>= _T('0') && Char
<= _T('9') &&
118 (ParsePos
== &ParseLine
[1] || IsSeparator(ParsePos
[-2]))
119 && (*ParsePos
== _T('<') || *ParsePos
== _T('>'))))
124 if (Char
== ExtraEnd
)
126 if (InsideBlock
&& Char
== _T(')'))
128 if (_tcschr(_T("&|<>"), Char
))
134 /* Eat up a \n, allowing line continuation */
135 if (Char
== _T('\n'))
137 /* Next character is a forced literal */
140 if (Out
== &CurrentToken
[CMDLINE_LENGTH
- 1])
145 /* Check if we got at least one character before reaching a special one.
146 * If so, return them and leave the special for the next call. */
147 if (Out
!= CurrentToken
)
151 else if (Char
== _T('('))
153 Type
= TOK_BEGIN_BLOCK
;
157 else if (Char
== _T(')'))
159 Type
= TOK_END_BLOCK
;
163 else if (Char
== _T('&') || Char
== _T('|'))
168 /* check for && or || */
175 else if ((Char
>= _T('0') && Char
<= _T('9'))
176 || (Char
== _T('<') || Char
== _T('>')))
178 Type
= TOK_REDIRECTION
;
179 if (Char
>= _T('0') && Char
<= _T('9'))
188 /* Strangely, the tokenizer allows << as well as >>... (it
189 * will cause an error when trying to parse it though) */
196 while (IsSeparator(Char
= ParseChar()))
198 if (Char
>= _T('0') && Char
<= _T('9'))
210 return CurrentTokenType
= Type
;
213 static BOOL
ParseRedirection(REDIRECTION
**List
)
215 TCHAR
*Tok
= CurrentToken
;
217 REDIR_MODE RedirMode
;
220 if (*Tok
>= _T('0') && *Tok
<= _T('9'))
221 Number
= *Tok
++ - _T('0');
223 Number
= *Tok
== _T('<') ? 0 : 1;
225 if (*Tok
++ == _T('<'))
227 RedirMode
= REDIR_READ
;
233 RedirMode
= REDIR_WRITE
;
236 RedirMode
= REDIR_APPEND
;
243 /* The file name was not part of this token, so it'll be the next one */
244 if (ParseToken(0, STANDARD_SEPS
) != TOK_NORMAL
)
249 /* If a redirection for this handle number already exists, delete it */
250 while ((Redir
= *List
))
252 if (Redir
->Number
== Number
)
261 Redir
= cmd_alloc(FIELD_OFFSET(REDIRECTION
, Filename
[_tcslen(Tok
) + 1]));
263 Redir
->OldHandle
= INVALID_HANDLE_VALUE
;
264 Redir
->Number
= Number
;
265 Redir
->Mode
= RedirMode
;
266 _tcscpy(Redir
->Filename
, Tok
);
271 FreeRedirection(*List
);
276 static PARSED_COMMAND
*ParseCommandOp(int OpType
);
278 /* Parse a parenthesized block */
279 static PARSED_COMMAND
*ParseBlock(REDIRECTION
*RedirList
)
281 PARSED_COMMAND
*Cmd
, *Sub
, **NextPtr
;
282 Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
285 Cmd
->Subcommands
= NULL
;
286 Cmd
->Redirections
= RedirList
;
288 /* Read the block contents */
289 NextPtr
= &Cmd
->Subcommands
;
293 Sub
= ParseCommandOp(C_OP_LOWEST
);
297 NextPtr
= &Sub
->Next
;
299 else if (bParseError
)
306 if (CurrentTokenType
== TOK_END_BLOCK
)
308 /* Skip past the \n */
313 /* Process any trailing redirections */
314 while (ParseToken(0, STANDARD_SEPS
) == TOK_REDIRECTION
)
316 if (!ParseRedirection(&Cmd
->Redirections
))
325 /* Parse an IF statement */
326 static PARSED_COMMAND
*ParseIf(void)
328 PARSED_COMMAND
*Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
330 memset(Cmd
, 0, sizeof(PARSED_COMMAND
));
333 Type
= CurrentTokenType
;
334 if (_tcsicmp(CurrentToken
, _T("/I")) == 0)
336 Cmd
->If
.Flags
|= IFFLAG_IGNORECASE
;
337 Type
= ParseToken(0, STANDARD_SEPS
);
339 if (_tcsicmp(CurrentToken
, _T("not")) == 0)
341 Cmd
->If
.Flags
|= IFFLAG_NEGATE
;
342 Type
= ParseToken(0, STANDARD_SEPS
);
345 if (Type
!= TOK_NORMAL
)
352 /* Check for unary operators */
353 for (; Cmd
->If
.Operator
<= IF_MAX_UNARY
; Cmd
->If
.Operator
++)
355 if (_tcsicmp(CurrentToken
, IfOperatorString
[Cmd
->If
.Operator
]) == 0)
357 if (ParseToken(0, STANDARD_SEPS
) != TOK_NORMAL
)
363 Cmd
->If
.RightArg
= cmd_dup(CurrentToken
);
368 /* It must be a two-argument (comparison) operator. It could be ==, so
369 * the equals sign can't be treated as whitespace here. */
370 Cmd
->If
.LeftArg
= cmd_dup(CurrentToken
);
371 ParseToken(0, _T(",;"));
373 /* The right argument can come immediately after == */
374 if (_tcsnicmp(CurrentToken
, _T("=="), 2) == 0 && CurrentToken
[2])
376 Cmd
->If
.RightArg
= cmd_dup(&CurrentToken
[2]);
380 for (; Cmd
->If
.Operator
<= IF_MAX_COMPARISON
; Cmd
->If
.Operator
++)
382 if (_tcsicmp(CurrentToken
, IfOperatorString
[Cmd
->If
.Operator
]) == 0)
384 if (ParseToken(0, STANDARD_SEPS
) != TOK_NORMAL
)
386 Cmd
->If
.RightArg
= cmd_dup(CurrentToken
);
395 Cmd
->Subcommands
= ParseCommandOp(C_OP_LOWEST
);
396 if (Cmd
->Subcommands
== NULL
)
401 if (_tcsicmp(CurrentToken
, _T("else")) == 0)
403 Cmd
->Subcommands
->Next
= ParseCommandOp(C_OP_LOWEST
);
404 if (Cmd
->Subcommands
->Next
== NULL
)
414 /* Parse a FOR command.
415 * Syntax is: FOR [options] %var IN (list) DO command */
416 static PARSED_COMMAND
*ParseFor(void)
418 PARSED_COMMAND
*Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
419 TCHAR List
[CMDLINE_LENGTH
];
422 memset(Cmd
, 0, sizeof(PARSED_COMMAND
));
427 if (_tcsicmp(CurrentToken
, _T("/D")) == 0)
428 Cmd
->For
.Switches
|= FOR_DIRS
;
429 else if (_tcsicmp(CurrentToken
, _T("/F")) == 0)
431 Cmd
->For
.Switches
|= FOR_F
;
432 if (!Cmd
->For
.Params
)
434 ParseToken(0, STANDARD_SEPS
);
435 if (CurrentToken
[0] == _T('/') || CurrentToken
[0] == _T('%'))
437 Cmd
->For
.Params
= cmd_dup(CurrentToken
);
440 else if (_tcsicmp(CurrentToken
, _T("/L")) == 0)
441 Cmd
->For
.Switches
|= FOR_LOOP
;
442 else if (_tcsicmp(CurrentToken
, _T("/R")) == 0)
444 Cmd
->For
.Switches
|= FOR_RECURSIVE
;
445 if (!Cmd
->For
.Params
)
447 ParseToken(0, STANDARD_SEPS
);
448 if (CurrentToken
[0] == _T('/') || CurrentToken
[0] == _T('%'))
450 StripQuotes(CurrentToken
);
451 Cmd
->For
.Params
= cmd_dup(CurrentToken
);
456 ParseToken(0, STANDARD_SEPS
);
459 /* Make sure there aren't two different switches specified
460 * at the same time, unless they're /D and /R */
461 if ((Cmd
->For
.Switches
& (Cmd
->For
.Switches
- 1)) != 0
462 && Cmd
->For
.Switches
!= (FOR_DIRS
| FOR_RECURSIVE
))
467 /* Variable name should be % and just one other character */
468 if (CurrentToken
[0] != _T('%') || _tcslen(CurrentToken
) != 2)
470 Cmd
->For
.Variable
= CurrentToken
[1];
472 ParseToken(0, STANDARD_SEPS
);
473 if (_tcsicmp(CurrentToken
, _T("in")) != 0)
476 if (ParseToken(_T('('), STANDARD_SEPS
) != TOK_BEGIN_BLOCK
)
483 /* Pretend we're inside a block so the tokenizer will stop on ')' */
485 Type
= ParseToken(0, STANDARD_SEPS
);
488 if (Type
== TOK_END_BLOCK
)
491 if (Type
!= TOK_NORMAL
)
497 if (Pos
+ _tcslen(CurrentToken
) >= &List
[CMDLINE_LENGTH
])
499 Pos
= _stpcpy(Pos
, CurrentToken
);
502 Cmd
->For
.List
= cmd_dup(List
);
504 ParseToken(0, STANDARD_SEPS
);
505 if (_tcsicmp(CurrentToken
, _T("do")) != 0)
508 Cmd
->Subcommands
= ParseCommandOp(C_OP_LOWEST
);
509 if (Cmd
->Subcommands
== NULL
)
523 /* Parse a REM command */
524 static PARSED_COMMAND
*ParseRem(void)
526 /* Just ignore the rest of the line */
527 while (CurChar
&& CurChar
!= _T('\n'))
532 static DECLSPEC_NOINLINE PARSED_COMMAND
*ParseCommandPart(REDIRECTION
*RedirList
)
534 TCHAR ParsedLine
[CMDLINE_LENGTH
];
536 PARSED_COMMAND
*(*Func
)(void);
538 TCHAR
*Pos
= _stpcpy(ParsedLine
, CurrentToken
) + 1;
539 DWORD_PTR TailOffset
= Pos
- ParsedLine
;
541 /* Check for special forms */
542 if ((Func
= ParseFor
, _tcsicmp(ParsedLine
, _T("for")) == 0) ||
543 (Func
= ParseIf
, _tcsicmp(ParsedLine
, _T("if")) == 0) ||
544 (Func
= ParseRem
, _tcsicmp(ParsedLine
, _T("rem")) == 0))
546 ParseToken(0, STANDARD_SEPS
);
547 /* Do special parsing only if it's not followed by /? */
548 if (_tcscmp(CurrentToken
, _T("/?")) != 0)
553 FreeRedirection(RedirList
);
558 Pos
= _stpcpy(Pos
, _T(" /?"));
561 /* Now get the tail */
564 int Type
= ParseToken(0, NULL
);
565 if (Type
== TOK_NORMAL
)
567 if (Pos
+ _tcslen(CurrentToken
) >= &ParsedLine
[CMDLINE_LENGTH
])
570 FreeRedirection(RedirList
);
573 Pos
= _stpcpy(Pos
, CurrentToken
);
575 else if (Type
== TOK_REDIRECTION
)
577 if (!ParseRedirection(&RedirList
))
587 Cmd
= cmd_alloc(FIELD_OFFSET(PARSED_COMMAND
, Command
.First
[Pos
- ParsedLine
]));
588 Cmd
->Type
= C_COMMAND
;
590 Cmd
->Subcommands
= NULL
;
591 Cmd
->Redirections
= RedirList
;
592 memcpy(Cmd
->Command
.First
, ParsedLine
, (Pos
- ParsedLine
) * sizeof(TCHAR
));
593 Cmd
->Command
.Rest
= Cmd
->Command
.First
+ TailOffset
;
597 static PARSED_COMMAND
*ParsePrimary(void)
599 REDIRECTION
*RedirList
= NULL
;
602 while (IsSeparator(CurChar
))
604 if (CurChar
== _T('\n'))
612 if (CurChar
== _T(':'))
614 /* "Ignore" the rest of the line.
615 * (Line continuations will still be parsed, though.) */
616 while (ParseToken(0, NULL
) != TOK_END
)
621 if (CurChar
== _T('@'))
625 Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
628 /* @ acts like a unary operator with low precedence,
629 * so call the top-level parser */
630 Cmd
->Subcommands
= ParseCommandOp(C_OP_LOWEST
);
631 Cmd
->Redirections
= NULL
;
635 /* Process leading redirections and get the head of the command */
636 while ((Type
= ParseToken(_T('('), STANDARD_SEPS
)) == TOK_REDIRECTION
)
638 if (!ParseRedirection(&RedirList
))
642 if (Type
== TOK_NORMAL
)
643 return ParseCommandPart(RedirList
);
644 else if (Type
== TOK_BEGIN_BLOCK
)
645 return ParseBlock(RedirList
);
646 else if (Type
== TOK_END_BLOCK
&& !RedirList
)
650 FreeRedirection(RedirList
);
654 static PARSED_COMMAND
*ParseCommandOp(int OpType
)
656 PARSED_COMMAND
*Cmd
, *Left
, *Right
;
658 if (OpType
== C_OP_HIGHEST
)
659 Cmd
= ParsePrimary();
661 Cmd
= ParseCommandOp(OpType
+ 1);
663 if (Cmd
&& !_tcscmp(CurrentToken
, OpString
[OpType
- C_OP_LOWEST
]))
666 Right
= ParseCommandOp(OpType
);
671 /* & is allowed to have an empty RHS */
672 if (OpType
== C_MULTI
)
680 Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
683 Cmd
->Redirections
= NULL
;
684 Cmd
->Subcommands
= Left
;
693 ParseCommand(LPTSTR Line
)
699 if (!SubstituteVars(Line
, ParseLine
, _T('%')))
701 bLineContinuations
= FALSE
;
705 if (!ReadLine(ParseLine
, FALSE
))
707 bLineContinuations
= TRUE
;
710 ParsePos
= ParseLine
;
713 Cmd
= ParseCommandOp(C_OP_LOWEST
);
716 if (CurrentTokenType
!= TOK_END
)
733 /* Reconstruct a parse tree into text form; used for echoing
734 * batch file commands and FOR instances. */
736 EchoCommand(PARSED_COMMAND
*Cmd
)
738 TCHAR Buf
[CMDLINE_LENGTH
];
745 if (SubstituteForVars(Cmd
->Command
.First
, Buf
))
746 ConOutPrintf(_T("%s"), Buf
);
747 if (SubstituteForVars(Cmd
->Command
.Rest
, Buf
))
748 ConOutPrintf(_T("%s"), Buf
);
754 Sub
= Cmd
->Subcommands
;
755 if (Sub
&& !Sub
->Next
)
757 /* Single-command block: display all on one line */
762 /* Multi-command block: display parenthesis on separate lines */
763 ConOutChar(_T('\n'));
767 ConOutChar(_T('\n'));
777 Sub
= Cmd
->Subcommands
;
779 ConOutPrintf(_T(" %s "), OpString
[Cmd
->Type
- C_OP_LOWEST
]);
780 EchoCommand(Sub
->Next
);
783 ConOutPrintf(_T("if"));
784 if (Cmd
->If
.Flags
& IFFLAG_IGNORECASE
)
785 ConOutPrintf(_T(" /I"));
786 if (Cmd
->If
.Flags
& IFFLAG_NEGATE
)
787 ConOutPrintf(_T(" not"));
788 if (Cmd
->If
.LeftArg
&& SubstituteForVars(Cmd
->If
.LeftArg
, Buf
))
789 ConOutPrintf(_T(" %s"), Buf
);
790 ConOutPrintf(_T(" %s"), IfOperatorString
[Cmd
->If
.Operator
]);
791 if (SubstituteForVars(Cmd
->If
.RightArg
, Buf
))
792 ConOutPrintf(_T(" %s "), Buf
);
793 Sub
= Cmd
->Subcommands
;
797 ConOutPrintf(_T(" else "));
798 EchoCommand(Sub
->Next
);
802 ConOutPrintf(_T("for"));
803 if (Cmd
->For
.Switches
& FOR_DIRS
) ConOutPrintf(_T(" /D"));
804 if (Cmd
->For
.Switches
& FOR_F
) ConOutPrintf(_T(" /F"));
805 if (Cmd
->For
.Switches
& FOR_LOOP
) ConOutPrintf(_T(" /L"));
806 if (Cmd
->For
.Switches
& FOR_RECURSIVE
) ConOutPrintf(_T(" /R"));
808 ConOutPrintf(_T(" %s"), Cmd
->For
.Params
);
809 ConOutPrintf(_T(" %%%c in (%s) do "), Cmd
->For
.Variable
, Cmd
->For
.List
);
810 EchoCommand(Cmd
->Subcommands
);
814 for (Redir
= Cmd
->Redirections
; Redir
; Redir
= Redir
->Next
)
816 if (SubstituteForVars(Redir
->Filename
, Buf
))
817 ConOutPrintf(_T(" %c%s%s"), _T('0') + Redir
->Number
,
818 RedirString
[Redir
->Mode
], Buf
);
822 /* "Unparse" a command into a text form suitable for passing to CMD /C.
823 * Used for pipes. This is basically the same thing as EchoCommand, but
824 * writing into a string instead of to standard output. */
826 Unparse(PARSED_COMMAND
*Cmd
, TCHAR
*Out
, TCHAR
*OutEnd
)
828 TCHAR Buf
[CMDLINE_LENGTH
];
832 /* Since this function has the annoying requirement that it must avoid
833 * overflowing the supplied buffer, define some helper macros to make
834 * this less painful */
835 #define CHAR(Char) { \
836 if (Out == OutEnd) return NULL; \
838 #define STRING(String) { \
839 if (Out + _tcslen(String) > OutEnd) return NULL; \
840 Out = _stpcpy(Out, String); }
841 #define PRINTF(Format, ...) { \
842 UINT Len = _sntprintf(Out, OutEnd - Out, Format, __VA_ARGS__); \
843 if (Len > (UINT)(OutEnd - Out)) return NULL; \
845 #define RECURSE(Subcommand) { \
846 Out = Unparse(Subcommand, Out, OutEnd); \
847 if (!Out) return NULL; }
852 /* This is fragile since there could be special characters, but
853 * Windows doesn't bother escaping them, so for compatibility
854 * we probably shouldn't do it either */
855 if (!SubstituteForVars(Cmd
->Command
.First
, Buf
)) return NULL
;
857 if (!SubstituteForVars(Cmd
->Command
.Rest
, Buf
)) return NULL
;
862 RECURSE(Cmd
->Subcommands
)
866 for (Sub
= Cmd
->Subcommands
; Sub
; Sub
= Sub
->Next
)
878 Sub
= Cmd
->Subcommands
;
880 PRINTF(_T(" %s "), OpString
[Cmd
->Type
- C_OP_LOWEST
])
885 if (Cmd
->If
.Flags
& IFFLAG_IGNORECASE
)
887 if (Cmd
->If
.Flags
& IFFLAG_NEGATE
)
889 if (Cmd
->If
.LeftArg
&& SubstituteForVars(Cmd
->If
.LeftArg
, Buf
))
890 PRINTF(_T(" %s"), Buf
)
891 PRINTF(_T(" %s"), IfOperatorString
[Cmd
->If
.Operator
]);
892 if (!SubstituteForVars(Cmd
->If
.RightArg
, Buf
)) return NULL
;
893 PRINTF(_T(" %s "), Buf
)
894 Sub
= Cmd
->Subcommands
;
904 if (Cmd
->For
.Switches
& FOR_DIRS
) STRING(_T(" /D"))
905 if (Cmd
->For
.Switches
& FOR_F
) STRING(_T(" /F"))
906 if (Cmd
->For
.Switches
& FOR_LOOP
) STRING(_T(" /L"))
907 if (Cmd
->For
.Switches
& FOR_RECURSIVE
) STRING(_T(" /R"))
909 PRINTF(_T(" %s"), Cmd
->For
.Params
)
910 PRINTF(_T(" %%%c in (%s) do "), Cmd
->For
.Variable
, Cmd
->For
.List
)
911 RECURSE(Cmd
->Subcommands
)
915 for (Redir
= Cmd
->Redirections
; Redir
; Redir
= Redir
->Next
)
917 if (!SubstituteForVars(Redir
->Filename
, Buf
)) return NULL
;
918 PRINTF(_T(" %c%s%s"), _T('0') + Redir
->Number
,
919 RedirString
[Redir
->Mode
], Buf
)
925 FreeCommand(PARSED_COMMAND
*Cmd
)
927 if (Cmd
->Subcommands
)
928 FreeCommand(Cmd
->Subcommands
);
930 FreeCommand(Cmd
->Next
);
931 FreeRedirection(Cmd
->Redirections
);
932 if (Cmd
->Type
== C_IF
)
934 cmd_free(Cmd
->If
.LeftArg
);
935 cmd_free(Cmd
->If
.RightArg
);
937 else if (Cmd
->Type
== C_FOR
)
939 cmd_free(Cmd
->For
.Params
);
940 cmd_free(Cmd
->For
.List
);