2 * PARSER.C - command parsing.
7 #define C_OP_LOWEST C_MULTI
8 #define C_OP_HIGHEST C_PIPE
9 static const TCHAR OpString
[][3] = { _T("&"), _T("||"), _T("&&"), _T("|") };
11 static const TCHAR RedirString
[][3] = { _T("<"), _T(">"), _T(">>") };
13 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
));
48 static BOOL bParseError
;
49 static BOOL bLineContinuations
;
50 static TCHAR ParseLine
[CMDLINE_LENGTH
];
51 static TCHAR
*ParsePos
;
54 static TCHAR CurrentToken
[CMDLINE_LENGTH
];
55 static int CurrentTokenType
;
56 static int InsideBlock
;
58 static TCHAR
ParseChar()
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.
73 while (Char
== _T('\r'));
78 if (bLineContinuations
)
80 if (!ReadLine(ParseLine
, TRUE
))
82 /* ^C pressed, or line was too long */
85 else if (*(ParsePos
= ParseLine
))
91 return CurChar
= Char
;
94 static void ParseError()
96 error_syntax(CurrentTokenType
!= TOK_END
? CurrentToken
: NULL
);
101 * Yes, cmd has a Lexical Analyzer. Whenever the parser gives an "xxx was
102 * unexpected at this time." message, it shows what the last token read was.
104 static int ParseToken(TCHAR ExtraEnd
, TCHAR
*Separators
)
106 TCHAR
*Out
= CurrentToken
;
109 BOOL bInQuote
= FALSE
;
111 for (Char
= CurChar
; Char
&& Char
!= _T('\n'); Char
= ParseChar())
113 bInQuote
^= (Char
== _T('"'));
116 if (Separators
!= NULL
)
118 if (_istspace(Char
) || _tcschr(Separators
, Char
))
120 /* Skip leading separators */
121 if (Out
== CurrentToken
)
127 /* Check for numbered redirection */
128 if ((Char
>= _T('0') && Char
<= _T('9') &&
129 (ParsePos
== &ParseLine
[1] || IsSeparator(ParsePos
[-2]))
130 && (*ParsePos
== _T('<') || *ParsePos
== _T('>'))))
135 if (Char
== ExtraEnd
)
137 if (InsideBlock
&& Char
== _T(')'))
139 if (_tcschr(_T("&|<>"), Char
))
145 /* Eat up a \n, allowing line continuation */
146 if (Char
== _T('\n'))
148 /* Next character is a forced literal */
151 if (Out
== &CurrentToken
[CMDLINE_LENGTH
- 1])
156 /* Check if we got at least one character before reaching a special one.
157 * If so, return them and leave the special for the next call. */
158 if (Out
!= CurrentToken
)
162 else if (Char
== _T('('))
164 Type
= TOK_BEGIN_BLOCK
;
168 else if (Char
== _T(')'))
170 Type
= TOK_END_BLOCK
;
174 else if (Char
== _T('&') || Char
== _T('|'))
179 /* check for && or || */
186 else if ((Char
>= _T('0') && Char
<= _T('9'))
187 || (Char
== _T('<') || Char
== _T('>')))
189 Type
= TOK_REDIRECTION
;
190 if (Char
>= _T('0') && Char
<= _T('9'))
199 /* Strangely, the tokenizer allows << as well as >>... (it
200 * will cause an error when trying to parse it though) */
207 while (IsSeparator(Char
= ParseChar()))
209 if (Char
>= _T('0') && Char
<= _T('9'))
221 return CurrentTokenType
= Type
;
224 static BOOL
ParseRedirection(REDIRECTION
**List
)
226 TCHAR
*Tok
= CurrentToken
;
228 REDIR_MODE RedirMode
;
231 if (*Tok
>= _T('0') && *Tok
<= _T('9'))
232 Number
= *Tok
++ - _T('0');
234 Number
= *Tok
== _T('<') ? 0 : 1;
236 if (*Tok
++ == _T('<'))
238 RedirMode
= REDIR_READ
;
244 RedirMode
= REDIR_WRITE
;
247 RedirMode
= REDIR_APPEND
;
254 /* The file name was not part of this token, so it'll be the next one */
255 if (ParseToken(0, STANDARD_SEPS
) != TOK_NORMAL
)
260 /* If a redirection for this handle number already exists, delete it */
261 while ((Redir
= *List
))
263 if (Redir
->Number
== Number
)
272 Redir
= cmd_alloc(FIELD_OFFSET(REDIRECTION
, Filename
[_tcslen(Tok
) + 1]));
274 Redir
->OldHandle
= INVALID_HANDLE_VALUE
;
275 Redir
->Number
= Number
;
276 Redir
->Mode
= RedirMode
;
277 _tcscpy(Redir
->Filename
, Tok
);
283 FreeRedirection(*List
);
288 static PARSED_COMMAND
*ParseCommandOp(int OpType
);
290 /* Parse a parenthesized block */
291 static PARSED_COMMAND
*ParseBlock(REDIRECTION
*RedirList
)
293 PARSED_COMMAND
*Cmd
, *Sub
, **NextPtr
;
294 Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
297 Cmd
->Subcommands
= NULL
;
298 Cmd
->Redirections
= RedirList
;
300 /* Read the block contents */
301 NextPtr
= &Cmd
->Subcommands
;
305 Sub
= ParseCommandOp(C_OP_LOWEST
);
309 NextPtr
= &Sub
->Next
;
311 else if (bParseError
)
318 if (CurrentTokenType
== TOK_END_BLOCK
)
320 /* Skip past the \n */
325 /* Process any trailing redirections */
326 while (ParseToken(0, STANDARD_SEPS
) == TOK_REDIRECTION
)
328 if (!ParseRedirection(&Cmd
->Redirections
))
337 /* Parse an IF statement */
338 static PARSED_COMMAND
*ParseIf(void)
340 PARSED_COMMAND
*Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
342 memset(Cmd
, 0, sizeof(PARSED_COMMAND
));
345 Type
= CurrentTokenType
;
346 if (_tcsicmp(CurrentToken
, _T("/I")) == 0)
348 Cmd
->If
.Flags
|= IFFLAG_IGNORECASE
;
349 Type
= ParseToken(0, STANDARD_SEPS
);
351 if (_tcsicmp(CurrentToken
, _T("not")) == 0)
353 Cmd
->If
.Flags
|= IFFLAG_NEGATE
;
354 Type
= ParseToken(0, STANDARD_SEPS
);
357 if (Type
!= TOK_NORMAL
)
364 /* Check for unary operators */
365 for (; Cmd
->If
.Operator
<= IF_MAX_UNARY
; Cmd
->If
.Operator
++)
367 if (_tcsicmp(CurrentToken
, IfOperatorString
[Cmd
->If
.Operator
]) == 0)
369 if (ParseToken(0, STANDARD_SEPS
) != TOK_NORMAL
)
375 Cmd
->If
.RightArg
= cmd_dup(CurrentToken
);
380 /* It must be a two-argument (comparison) operator. It could be ==, so
381 * the equals sign can't be treated as whitespace here. */
382 Cmd
->If
.LeftArg
= cmd_dup(CurrentToken
);
383 ParseToken(0, _T(",;"));
385 /* The right argument can come immediately after == */
386 if (_tcsnicmp(CurrentToken
, _T("=="), 2) == 0 && CurrentToken
[2])
388 Cmd
->If
.RightArg
= cmd_dup(&CurrentToken
[2]);
392 for (; Cmd
->If
.Operator
<= IF_MAX_COMPARISON
; Cmd
->If
.Operator
++)
394 if (_tcsicmp(CurrentToken
, IfOperatorString
[Cmd
->If
.Operator
]) == 0)
396 if (ParseToken(0, STANDARD_SEPS
) != TOK_NORMAL
)
398 Cmd
->If
.RightArg
= cmd_dup(CurrentToken
);
407 Cmd
->Subcommands
= ParseCommandOp(C_OP_LOWEST
);
408 if (Cmd
->Subcommands
== NULL
)
413 if (_tcsicmp(CurrentToken
, _T("else")) == 0)
415 Cmd
->Subcommands
->Next
= ParseCommandOp(C_OP_LOWEST
);
416 if (Cmd
->Subcommands
->Next
== NULL
)
427 * Parse a FOR command.
428 * Syntax is: FOR [options] %var IN (list) DO command
430 static PARSED_COMMAND
*ParseFor(void)
432 PARSED_COMMAND
*Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
433 TCHAR List
[CMDLINE_LENGTH
];
436 memset(Cmd
, 0, sizeof(PARSED_COMMAND
));
441 if (_tcsicmp(CurrentToken
, _T("/D")) == 0)
442 Cmd
->For
.Switches
|= FOR_DIRS
;
443 else if (_tcsicmp(CurrentToken
, _T("/F")) == 0)
445 Cmd
->For
.Switches
|= FOR_F
;
446 if (!Cmd
->For
.Params
)
448 ParseToken(0, STANDARD_SEPS
);
449 if (CurrentToken
[0] == _T('/') || CurrentToken
[0] == _T('%'))
451 Cmd
->For
.Params
= cmd_dup(CurrentToken
);
454 else if (_tcsicmp(CurrentToken
, _T("/L")) == 0)
455 Cmd
->For
.Switches
|= FOR_LOOP
;
456 else if (_tcsicmp(CurrentToken
, _T("/R")) == 0)
458 Cmd
->For
.Switches
|= FOR_RECURSIVE
;
459 if (!Cmd
->For
.Params
)
461 ParseToken(0, STANDARD_SEPS
);
462 if (CurrentToken
[0] == _T('/') || CurrentToken
[0] == _T('%'))
464 StripQuotes(CurrentToken
);
465 Cmd
->For
.Params
= cmd_dup(CurrentToken
);
470 ParseToken(0, STANDARD_SEPS
);
473 /* Make sure there aren't two different switches specified
474 * at the same time, unless they're /D and /R */
475 if ((Cmd
->For
.Switches
& (Cmd
->For
.Switches
- 1)) != 0
476 && Cmd
->For
.Switches
!= (FOR_DIRS
| FOR_RECURSIVE
))
481 /* Variable name should be % and just one other character */
482 if (CurrentToken
[0] != _T('%') || _tcslen(CurrentToken
) != 2)
484 Cmd
->For
.Variable
= CurrentToken
[1];
486 ParseToken(0, STANDARD_SEPS
);
487 if (_tcsicmp(CurrentToken
, _T("in")) != 0)
490 if (ParseToken(_T('('), STANDARD_SEPS
) != TOK_BEGIN_BLOCK
)
497 /* Pretend we're inside a block so the tokenizer will stop on ')' */
499 Type
= ParseToken(0, STANDARD_SEPS
);
502 if (Type
== TOK_END_BLOCK
)
505 if (Type
!= TOK_NORMAL
)
511 if (Pos
+ _tcslen(CurrentToken
) >= &List
[CMDLINE_LENGTH
])
513 Pos
= _stpcpy(Pos
, CurrentToken
);
516 Cmd
->For
.List
= cmd_dup(List
);
518 ParseToken(0, STANDARD_SEPS
);
519 if (_tcsicmp(CurrentToken
, _T("do")) != 0)
522 Cmd
->Subcommands
= ParseCommandOp(C_OP_LOWEST
);
523 if (Cmd
->Subcommands
== NULL
)
537 /* Parse a REM command */
538 static PARSED_COMMAND
*ParseRem(void)
540 /* Just ignore the rest of the line */
541 while (CurChar
&& CurChar
!= _T('\n'))
546 static DECLSPEC_NOINLINE PARSED_COMMAND
*ParseCommandPart(REDIRECTION
*RedirList
)
548 TCHAR ParsedLine
[CMDLINE_LENGTH
];
550 PARSED_COMMAND
*(*Func
)(void);
552 TCHAR
*Pos
= _stpcpy(ParsedLine
, CurrentToken
) + 1;
553 DWORD_PTR TailOffset
= Pos
- ParsedLine
;
555 /* Check for special forms */
556 if ((Func
= ParseFor
, _tcsicmp(ParsedLine
, _T("for")) == 0) ||
557 (Func
= ParseIf
, _tcsicmp(ParsedLine
, _T("if")) == 0) ||
558 (Func
= ParseRem
, _tcsicmp(ParsedLine
, _T("rem")) == 0))
560 ParseToken(0, STANDARD_SEPS
);
561 /* Do special parsing only if it's not followed by /? */
562 if (_tcscmp(CurrentToken
, _T("/?")) != 0)
567 FreeRedirection(RedirList
);
572 Pos
= _stpcpy(Pos
, _T(" /?"));
575 /* Now get the tail */
578 int Type
= ParseToken(0, NULL
);
579 if (Type
== TOK_NORMAL
)
581 if (Pos
+ _tcslen(CurrentToken
) >= &ParsedLine
[CMDLINE_LENGTH
])
584 FreeRedirection(RedirList
);
587 Pos
= _stpcpy(Pos
, CurrentToken
);
589 else if (Type
== TOK_REDIRECTION
)
591 if (!ParseRedirection(&RedirList
))
601 Cmd
= cmd_alloc(FIELD_OFFSET(PARSED_COMMAND
, Command
.First
[Pos
- ParsedLine
]));
602 Cmd
->Type
= C_COMMAND
;
604 Cmd
->Subcommands
= NULL
;
605 Cmd
->Redirections
= RedirList
;
606 memcpy(Cmd
->Command
.First
, ParsedLine
, (Pos
- ParsedLine
) * sizeof(TCHAR
));
607 Cmd
->Command
.Rest
= Cmd
->Command
.First
+ TailOffset
;
611 static PARSED_COMMAND
*ParsePrimary(void)
613 REDIRECTION
*RedirList
= NULL
;
616 while (IsSeparator(CurChar
))
618 if (CurChar
== _T('\n'))
626 if (CurChar
== _T(':'))
628 /* "Ignore" the rest of the line.
629 * (Line continuations will still be parsed, though.) */
630 while (ParseToken(0, NULL
) != TOK_END
)
635 if (CurChar
== _T('@'))
639 Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
642 /* @ acts like a unary operator with low precedence,
643 * so call the top-level parser */
644 Cmd
->Subcommands
= ParseCommandOp(C_OP_LOWEST
);
645 Cmd
->Redirections
= NULL
;
649 /* Process leading redirections and get the head of the command */
650 while ((Type
= ParseToken(_T('('), STANDARD_SEPS
)) == TOK_REDIRECTION
)
652 if (!ParseRedirection(&RedirList
))
656 if (Type
== TOK_NORMAL
)
657 return ParseCommandPart(RedirList
);
658 else if (Type
== TOK_BEGIN_BLOCK
)
659 return ParseBlock(RedirList
);
660 else if (Type
== TOK_END_BLOCK
&& !RedirList
)
664 FreeRedirection(RedirList
);
668 static PARSED_COMMAND
*ParseCommandOp(int OpType
)
670 PARSED_COMMAND
*Cmd
, *Left
, *Right
;
672 if (OpType
== C_OP_HIGHEST
)
673 Cmd
= ParsePrimary();
675 Cmd
= ParseCommandOp(OpType
+ 1);
677 if (Cmd
&& !_tcscmp(CurrentToken
, OpString
[OpType
- C_OP_LOWEST
]))
680 Right
= ParseCommandOp(OpType
);
685 /* & is allowed to have an empty RHS */
686 if (OpType
== C_MULTI
)
694 Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
697 Cmd
->Redirections
= NULL
;
698 Cmd
->Subcommands
= Left
;
707 ParseCommand(LPTSTR Line
)
713 if (!SubstituteVars(Line
, ParseLine
, _T('%')))
715 bLineContinuations
= FALSE
;
719 if (!ReadLine(ParseLine
, FALSE
))
721 bLineContinuations
= TRUE
;
724 ParsePos
= ParseLine
;
727 Cmd
= ParseCommandOp(C_OP_LOWEST
);
730 if (CurrentTokenType
!= TOK_END
)
748 * Reconstruct a parse tree into text form; used for echoing
749 * batch file commands and FOR instances.
752 EchoCommand(PARSED_COMMAND
*Cmd
)
754 TCHAR Buf
[CMDLINE_LENGTH
];
761 if (SubstituteForVars(Cmd
->Command
.First
, Buf
))
762 ConOutPrintf(_T("%s"), Buf
);
763 if (SubstituteForVars(Cmd
->Command
.Rest
, Buf
))
764 ConOutPrintf(_T("%s"), Buf
);
770 Sub
= Cmd
->Subcommands
;
771 if (Sub
&& !Sub
->Next
)
773 /* Single-command block: display all on one line */
778 /* Multi-command block: display parenthesis on separate lines */
779 ConOutChar(_T('\n'));
783 ConOutChar(_T('\n'));
793 Sub
= Cmd
->Subcommands
;
795 ConOutPrintf(_T(" %s "), OpString
[Cmd
->Type
- C_OP_LOWEST
]);
796 EchoCommand(Sub
->Next
);
799 ConOutPrintf(_T("if"));
800 if (Cmd
->If
.Flags
& IFFLAG_IGNORECASE
)
801 ConOutPrintf(_T(" /I"));
802 if (Cmd
->If
.Flags
& IFFLAG_NEGATE
)
803 ConOutPrintf(_T(" not"));
804 if (Cmd
->If
.LeftArg
&& SubstituteForVars(Cmd
->If
.LeftArg
, Buf
))
805 ConOutPrintf(_T(" %s"), Buf
);
806 ConOutPrintf(_T(" %s"), IfOperatorString
[Cmd
->If
.Operator
]);
807 if (SubstituteForVars(Cmd
->If
.RightArg
, Buf
))
808 ConOutPrintf(_T(" %s "), Buf
);
809 Sub
= Cmd
->Subcommands
;
813 ConOutPrintf(_T(" else "));
814 EchoCommand(Sub
->Next
);
818 ConOutPrintf(_T("for"));
819 if (Cmd
->For
.Switches
& FOR_DIRS
) ConOutPrintf(_T(" /D"));
820 if (Cmd
->For
.Switches
& FOR_F
) ConOutPrintf(_T(" /F"));
821 if (Cmd
->For
.Switches
& FOR_LOOP
) ConOutPrintf(_T(" /L"));
822 if (Cmd
->For
.Switches
& FOR_RECURSIVE
) ConOutPrintf(_T(" /R"));
824 ConOutPrintf(_T(" %s"), Cmd
->For
.Params
);
825 ConOutPrintf(_T(" %%%c in (%s) do "), Cmd
->For
.Variable
, Cmd
->For
.List
);
826 EchoCommand(Cmd
->Subcommands
);
830 for (Redir
= Cmd
->Redirections
; Redir
; Redir
= Redir
->Next
)
832 if (SubstituteForVars(Redir
->Filename
, Buf
))
833 ConOutPrintf(_T(" %c%s%s"), _T('0') + Redir
->Number
,
834 RedirString
[Redir
->Mode
], Buf
);
839 * "Unparse" a command into a text form suitable for passing to CMD /C.
840 * Used for pipes. This is basically the same thing as EchoCommand, but
841 * writing into a string instead of to standard output.
844 Unparse(PARSED_COMMAND
*Cmd
, TCHAR
*Out
, TCHAR
*OutEnd
)
846 TCHAR Buf
[CMDLINE_LENGTH
];
851 * Since this function has the annoying requirement that it must avoid
852 * overflowing the supplied buffer, define some helper macros to make
855 #define CHAR(Char) { \
856 if (Out == OutEnd) return NULL; \
858 #define STRING(String) { \
859 if (Out + _tcslen(String) > OutEnd) return NULL; \
860 Out = _stpcpy(Out, String); }
861 #define PRINTF(Format, ...) { \
862 UINT Len = _sntprintf(Out, OutEnd - Out, Format, __VA_ARGS__); \
863 if (Len > (UINT)(OutEnd - Out)) return NULL; \
865 #define RECURSE(Subcommand) { \
866 Out = Unparse(Subcommand, Out, OutEnd); \
867 if (!Out) return NULL; }
872 /* This is fragile since there could be special characters, but
873 * Windows doesn't bother escaping them, so for compatibility
874 * we probably shouldn't do it either */
875 if (!SubstituteForVars(Cmd
->Command
.First
, Buf
)) return NULL
;
877 if (!SubstituteForVars(Cmd
->Command
.Rest
, Buf
)) return NULL
;
882 RECURSE(Cmd
->Subcommands
)
886 for (Sub
= Cmd
->Subcommands
; Sub
; Sub
= Sub
->Next
)
898 Sub
= Cmd
->Subcommands
;
900 PRINTF(_T(" %s "), OpString
[Cmd
->Type
- C_OP_LOWEST
])
905 if (Cmd
->If
.Flags
& IFFLAG_IGNORECASE
)
907 if (Cmd
->If
.Flags
& IFFLAG_NEGATE
)
909 if (Cmd
->If
.LeftArg
&& SubstituteForVars(Cmd
->If
.LeftArg
, Buf
))
910 PRINTF(_T(" %s"), Buf
)
911 PRINTF(_T(" %s"), IfOperatorString
[Cmd
->If
.Operator
]);
912 if (!SubstituteForVars(Cmd
->If
.RightArg
, Buf
)) return NULL
;
913 PRINTF(_T(" %s "), Buf
)
914 Sub
= Cmd
->Subcommands
;
924 if (Cmd
->For
.Switches
& FOR_DIRS
) STRING(_T(" /D"))
925 if (Cmd
->For
.Switches
& FOR_F
) STRING(_T(" /F"))
926 if (Cmd
->For
.Switches
& FOR_LOOP
) STRING(_T(" /L"))
927 if (Cmd
->For
.Switches
& FOR_RECURSIVE
) STRING(_T(" /R"))
929 PRINTF(_T(" %s"), Cmd
->For
.Params
)
930 PRINTF(_T(" %%%c in (%s) do "), Cmd
->For
.Variable
, Cmd
->For
.List
)
931 RECURSE(Cmd
->Subcommands
)
935 for (Redir
= Cmd
->Redirections
; Redir
; Redir
= Redir
->Next
)
937 if (!SubstituteForVars(Redir
->Filename
, Buf
)) return NULL
;
938 PRINTF(_T(" %c%s%s"), _T('0') + Redir
->Number
,
939 RedirString
[Redir
->Mode
], Buf
)
945 FreeCommand(PARSED_COMMAND
*Cmd
)
947 if (Cmd
->Subcommands
)
948 FreeCommand(Cmd
->Subcommands
);
950 FreeCommand(Cmd
->Next
);
951 FreeRedirection(Cmd
->Redirections
);
952 if (Cmd
->Type
== C_IF
)
954 cmd_free(Cmd
->If
.LeftArg
);
955 cmd_free(Cmd
->If
.RightArg
);
957 else if (Cmd
->Type
== C_FOR
)
959 cmd_free(Cmd
->For
.Params
);
960 cmd_free(Cmd
->For
.List
);