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(void)
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.
75 while (Char
== _T('\r'));
80 if (bLineContinuations
)
82 if (!ReadLine(ParseLine
, TRUE
))
84 /* ^C pressed, or line was too long */
87 else if (*(ParsePos
= ParseLine
))
93 return CurChar
= Char
;
96 static void ParseError(void)
98 error_syntax(CurrentTokenType
!= TOK_END
? CurrentToken
: NULL
);
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.
106 static int ParseToken(TCHAR ExtraEnd
, TCHAR
*Separators
)
108 TCHAR
*Out
= CurrentToken
;
111 BOOL bInQuote
= FALSE
;
113 for (Char
= CurChar
; Char
&& Char
!= _T('\n'); Char
= ParseChar())
115 bInQuote
^= (Char
== _T('"'));
118 if (Separators
!= NULL
)
120 if (_istspace(Char
) || _tcschr(Separators
, Char
))
122 /* Skip leading separators */
123 if (Out
== CurrentToken
)
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('>'))))
137 if (Char
== ExtraEnd
)
139 if (InsideBlock
&& Char
== _T(')'))
141 if (_tcschr(_T("&|<>"), Char
))
147 /* Eat up a \n, allowing line continuation */
148 if (Char
== _T('\n'))
150 /* Next character is a forced literal */
153 if (Out
== &CurrentToken
[CMDLINE_LENGTH
- 1])
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
)
164 else if (Char
== _T('('))
166 Type
= TOK_BEGIN_BLOCK
;
170 else if (Char
== _T(')'))
172 Type
= TOK_END_BLOCK
;
176 else if (Char
== _T('&') || Char
== _T('|'))
181 /* check for && or || */
188 else if ((Char
>= _T('0') && Char
<= _T('9'))
189 || (Char
== _T('<') || Char
== _T('>')))
191 Type
= TOK_REDIRECTION
;
192 if (Char
>= _T('0') && Char
<= _T('9'))
201 /* Strangely, the tokenizer allows << as well as >>... (it
202 * will cause an error when trying to parse it though) */
209 while (IsSeparator(Char
= ParseChar()))
211 if (Char
>= _T('0') && Char
<= _T('9'))
223 return CurrentTokenType
= Type
;
226 static BOOL
ParseRedirection(REDIRECTION
**List
)
228 TCHAR
*Tok
= CurrentToken
;
230 REDIR_MODE RedirMode
;
233 if (*Tok
>= _T('0') && *Tok
<= _T('9'))
234 Number
= *Tok
++ - _T('0');
236 Number
= *Tok
== _T('<') ? 0 : 1;
238 if (*Tok
++ == _T('<'))
240 RedirMode
= REDIR_READ
;
246 RedirMode
= REDIR_WRITE
;
249 RedirMode
= REDIR_APPEND
;
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
)
262 /* If a redirection for this handle number already exists, delete it */
263 while ((Redir
= *List
))
265 if (Redir
->Number
== Number
)
274 Redir
= cmd_alloc(FIELD_OFFSET(REDIRECTION
, Filename
[_tcslen(Tok
) + 1]));
276 Redir
->OldHandle
= INVALID_HANDLE_VALUE
;
277 Redir
->Number
= Number
;
278 Redir
->Mode
= RedirMode
;
279 _tcscpy(Redir
->Filename
, Tok
);
285 FreeRedirection(*List
);
290 static PARSED_COMMAND
*ParseCommandOp(int OpType
);
292 /* Parse a parenthesized block */
293 static PARSED_COMMAND
*ParseBlock(REDIRECTION
*RedirList
)
295 PARSED_COMMAND
*Cmd
, *Sub
, **NextPtr
;
296 Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
299 Cmd
->Subcommands
= NULL
;
300 Cmd
->Redirections
= RedirList
;
302 /* Read the block contents */
303 NextPtr
= &Cmd
->Subcommands
;
307 Sub
= ParseCommandOp(C_OP_LOWEST
);
311 NextPtr
= &Sub
->Next
;
313 else if (bParseError
)
320 if (CurrentTokenType
== TOK_END_BLOCK
)
323 /* Skip past the \n */
328 /* Process any trailing redirections */
329 while (ParseToken(0, STANDARD_SEPS
) == TOK_REDIRECTION
)
331 if (!ParseRedirection(&Cmd
->Redirections
))
340 /* Parse an IF statement */
341 static PARSED_COMMAND
*ParseIf(void)
343 PARSED_COMMAND
*Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
345 memset(Cmd
, 0, sizeof(PARSED_COMMAND
));
348 Type
= CurrentTokenType
;
349 if (_tcsicmp(CurrentToken
, _T("/I")) == 0)
351 Cmd
->If
.Flags
|= IFFLAG_IGNORECASE
;
352 Type
= ParseToken(0, STANDARD_SEPS
);
354 if (_tcsicmp(CurrentToken
, _T("not")) == 0)
356 Cmd
->If
.Flags
|= IFFLAG_NEGATE
;
357 Type
= ParseToken(0, STANDARD_SEPS
);
360 if (Type
!= TOK_NORMAL
)
367 /* Check for unary operators */
368 for (; Cmd
->If
.Operator
<= IF_MAX_UNARY
; Cmd
->If
.Operator
++)
370 if (_tcsicmp(CurrentToken
, IfOperatorString
[Cmd
->If
.Operator
]) == 0)
372 if (ParseToken(0, STANDARD_SEPS
) != TOK_NORMAL
)
378 Cmd
->If
.RightArg
= cmd_dup(CurrentToken
);
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(",;"));
388 /* The right argument can come immediately after == */
389 if (_tcsnicmp(CurrentToken
, _T("=="), 2) == 0 && CurrentToken
[2])
391 Cmd
->If
.RightArg
= cmd_dup(&CurrentToken
[2]);
395 for (; Cmd
->If
.Operator
<= IF_MAX_COMPARISON
; Cmd
->If
.Operator
++)
397 if (_tcsicmp(CurrentToken
, IfOperatorString
[Cmd
->If
.Operator
]) == 0)
399 if (ParseToken(0, STANDARD_SEPS
) != TOK_NORMAL
)
401 Cmd
->If
.RightArg
= cmd_dup(CurrentToken
);
410 Cmd
->Subcommands
= ParseCommandOp(C_OP_LOWEST
);
411 if (Cmd
->Subcommands
== NULL
)
416 if (_tcsicmp(CurrentToken
, _T("else")) == 0)
418 Cmd
->Subcommands
->Next
= ParseCommandOp(C_OP_LOWEST
);
419 if (Cmd
->Subcommands
->Next
== NULL
)
430 * Parse a FOR command.
431 * Syntax is: FOR [options] %var IN (list) DO command
433 static PARSED_COMMAND
*ParseFor(void)
435 PARSED_COMMAND
*Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
436 TCHAR List
[CMDLINE_LENGTH
];
439 memset(Cmd
, 0, sizeof(PARSED_COMMAND
));
444 if (_tcsicmp(CurrentToken
, _T("/D")) == 0)
445 Cmd
->For
.Switches
|= FOR_DIRS
;
446 else if (_tcsicmp(CurrentToken
, _T("/F")) == 0)
448 Cmd
->For
.Switches
|= FOR_F
;
449 if (!Cmd
->For
.Params
)
451 ParseToken(0, STANDARD_SEPS
);
452 if (CurrentToken
[0] == _T('/') || CurrentToken
[0] == _T('%'))
454 Cmd
->For
.Params
= cmd_dup(CurrentToken
);
457 else if (_tcsicmp(CurrentToken
, _T("/L")) == 0)
458 Cmd
->For
.Switches
|= FOR_LOOP
;
459 else if (_tcsicmp(CurrentToken
, _T("/R")) == 0)
461 Cmd
->For
.Switches
|= FOR_RECURSIVE
;
462 if (!Cmd
->For
.Params
)
464 ParseToken(0, STANDARD_SEPS
);
465 if (CurrentToken
[0] == _T('/') || CurrentToken
[0] == _T('%'))
467 StripQuotes(CurrentToken
);
468 Cmd
->For
.Params
= cmd_dup(CurrentToken
);
473 ParseToken(0, STANDARD_SEPS
);
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
))
484 /* Variable name should be % and just one other character */
485 if (CurrentToken
[0] != _T('%') || _tcslen(CurrentToken
) != 2)
487 Cmd
->For
.Variable
= CurrentToken
[1];
489 ParseToken(0, STANDARD_SEPS
);
490 if (_tcsicmp(CurrentToken
, _T("in")) != 0)
493 if (ParseToken(_T('('), STANDARD_SEPS
) != TOK_BEGIN_BLOCK
)
500 /* Pretend we're inside a block so the tokenizer will stop on ')' */
502 Type
= ParseToken(0, STANDARD_SEPS
);
505 if (Type
== TOK_END_BLOCK
)
510 /* Skip past the \n */
515 if (Type
!= TOK_NORMAL
)
521 if (Pos
+ _tcslen(CurrentToken
) >= &List
[CMDLINE_LENGTH
])
523 Pos
= _stpcpy(Pos
, CurrentToken
);
526 Cmd
->For
.List
= cmd_dup(List
);
528 ParseToken(0, STANDARD_SEPS
);
529 if (_tcsicmp(CurrentToken
, _T("do")) != 0)
532 Cmd
->Subcommands
= ParseCommandOp(C_OP_LOWEST
);
533 if (Cmd
->Subcommands
== NULL
)
547 /* Parse a REM command */
548 static PARSED_COMMAND
*ParseRem(void)
550 /* Just ignore the rest of the line */
551 while (CurChar
&& CurChar
!= _T('\n'))
556 static DECLSPEC_NOINLINE PARSED_COMMAND
*ParseCommandPart(REDIRECTION
*RedirList
)
558 TCHAR ParsedLine
[CMDLINE_LENGTH
];
560 PARSED_COMMAND
*(*Func
)(void);
562 TCHAR
*Pos
= _stpcpy(ParsedLine
, CurrentToken
) + 1;
563 DWORD_PTR TailOffset
= Pos
- ParsedLine
;
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))
570 ParseToken(0, STANDARD_SEPS
);
571 /* Do special parsing only if it's not followed by /? */
572 if (_tcscmp(CurrentToken
, _T("/?")) != 0)
577 FreeRedirection(RedirList
);
582 Pos
= _stpcpy(Pos
, _T(" /?"));
585 /* Now get the tail */
588 int Type
= ParseToken(0, NULL
);
589 if (Type
== TOK_NORMAL
)
591 if (Pos
+ _tcslen(CurrentToken
) >= &ParsedLine
[CMDLINE_LENGTH
])
594 FreeRedirection(RedirList
);
597 Pos
= _stpcpy(Pos
, CurrentToken
);
599 else if (Type
== TOK_REDIRECTION
)
601 if (!ParseRedirection(&RedirList
))
611 Cmd
= cmd_alloc(FIELD_OFFSET(PARSED_COMMAND
, Command
.First
[Pos
- ParsedLine
]));
612 Cmd
->Type
= C_COMMAND
;
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
;
621 static PARSED_COMMAND
*ParsePrimary(void)
623 REDIRECTION
*RedirList
= NULL
;
626 while (IsSeparator(CurChar
))
628 if (CurChar
== _T('\n'))
636 if (CurChar
== _T(':'))
638 /* "Ignore" the rest of the line.
639 * (Line continuations will still be parsed, though.) */
640 while (ParseToken(0, NULL
) != TOK_END
)
645 if (CurChar
== _T('@'))
649 Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
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
;
659 /* Process leading redirections and get the head of the command */
660 while ((Type
= ParseToken(_T('('), STANDARD_SEPS
)) == TOK_REDIRECTION
)
662 if (!ParseRedirection(&RedirList
))
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
)
674 FreeRedirection(RedirList
);
678 static PARSED_COMMAND
*ParseCommandOp(int OpType
)
680 PARSED_COMMAND
*Cmd
, *Left
, *Right
;
682 if (OpType
== C_OP_HIGHEST
)
683 Cmd
= ParsePrimary();
685 Cmd
= ParseCommandOp(OpType
+ 1);
687 if (Cmd
&& !_tcscmp(CurrentToken
, OpString
[OpType
- C_OP_LOWEST
]))
690 Right
= ParseCommandOp(OpType
);
695 /* & is allowed to have an empty RHS */
696 if (OpType
== C_MULTI
)
704 Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
707 Cmd
->Redirections
= NULL
;
708 Cmd
->Subcommands
= Left
;
717 ParseCommand(LPTSTR Line
)
723 if (!SubstituteVars(Line
, ParseLine
, _T('%')))
725 bLineContinuations
= FALSE
;
729 if (!ReadLine(ParseLine
, FALSE
))
731 bLineContinuations
= TRUE
;
734 ParsePos
= ParseLine
;
737 Cmd
= ParseCommandOp(C_OP_LOWEST
);
740 if (CurrentTokenType
!= TOK_END
)
758 * Reconstruct a parse tree into text form; used for echoing
759 * batch file commands and FOR instances.
762 EchoCommand(PARSED_COMMAND
*Cmd
)
764 TCHAR Buf
[CMDLINE_LENGTH
];
771 if (SubstituteForVars(Cmd
->Command
.First
, Buf
))
772 ConOutPrintf(_T("%s"), Buf
);
773 if (SubstituteForVars(Cmd
->Command
.Rest
, Buf
))
774 ConOutPrintf(_T("%s"), Buf
);
780 Sub
= Cmd
->Subcommands
;
781 if (Sub
&& !Sub
->Next
)
783 /* Single-command block: display all on one line */
788 /* Multi-command block: display parenthesis on separate lines */
789 ConOutChar(_T('\n'));
793 ConOutChar(_T('\n'));
803 Sub
= Cmd
->Subcommands
;
805 ConOutPrintf(_T(" %s "), OpString
[Cmd
->Type
- C_OP_LOWEST
]);
806 EchoCommand(Sub
->Next
);
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
;
823 ConOutPrintf(_T(" else "));
824 EchoCommand(Sub
->Next
);
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"));
834 ConOutPrintf(_T(" %s"), Cmd
->For
.Params
);
835 ConOutPrintf(_T(" %%%c in (%s) do "), Cmd
->For
.Variable
, Cmd
->For
.List
);
836 EchoCommand(Cmd
->Subcommands
);
840 for (Redir
= Cmd
->Redirections
; Redir
; Redir
= Redir
->Next
)
842 if (SubstituteForVars(Redir
->Filename
, Buf
))
843 ConOutPrintf(_T(" %c%s%s"), _T('0') + Redir
->Number
,
844 RedirString
[Redir
->Mode
], Buf
);
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.
854 Unparse(PARSED_COMMAND
*Cmd
, TCHAR
*Out
, TCHAR
*OutEnd
)
856 TCHAR Buf
[CMDLINE_LENGTH
];
861 * Since this function has the annoying requirement that it must avoid
862 * overflowing the supplied buffer, define some helper macros to make
865 #define CHAR(Char) { \
866 if (Out == OutEnd) return NULL; \
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; \
875 #define RECURSE(Subcommand) { \
876 Out = Unparse(Subcommand, Out, OutEnd); \
877 if (!Out) return NULL; }
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
;
887 if (!SubstituteForVars(Cmd
->Command
.Rest
, Buf
)) return NULL
;
892 RECURSE(Cmd
->Subcommands
)
896 for (Sub
= Cmd
->Subcommands
; Sub
; Sub
= Sub
->Next
)
908 Sub
= Cmd
->Subcommands
;
910 PRINTF(_T(" %s "), OpString
[Cmd
->Type
- C_OP_LOWEST
])
915 if (Cmd
->If
.Flags
& IFFLAG_IGNORECASE
)
917 if (Cmd
->If
.Flags
& IFFLAG_NEGATE
)
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
;
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"))
939 PRINTF(_T(" %s"), Cmd
->For
.Params
)
940 PRINTF(_T(" %%%c in (%s) do "), Cmd
->For
.Variable
, Cmd
->For
.List
)
941 RECURSE(Cmd
->Subcommands
)
945 for (Redir
= Cmd
->Redirections
; Redir
; Redir
= Redir
->Next
)
947 if (!SubstituteForVars(Redir
->Filename
, Buf
)) return NULL
;
948 PRINTF(_T(" %c%s%s"), _T('0') + Redir
->Number
,
949 RedirString
[Redir
->Mode
], Buf
)
955 FreeCommand(PARSED_COMMAND
*Cmd
)
957 if (Cmd
->Subcommands
)
958 FreeCommand(Cmd
->Subcommands
);
960 FreeCommand(Cmd
->Next
);
961 FreeRedirection(Cmd
->Redirections
);
962 if (Cmd
->Type
== C_IF
)
964 cmd_free(Cmd
->If
.LeftArg
);
965 cmd_free(Cmd
->If
.RightArg
);
967 else if (Cmd
->Type
== C_FOR
)
969 cmd_free(Cmd
->For
.Params
);
970 cmd_free(Cmd
->For
.List
);