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]));
277 WARN("Cannot allocate memory for Redir!\n");
281 Redir
->OldHandle
= INVALID_HANDLE_VALUE
;
282 Redir
->Number
= Number
;
283 Redir
->Mode
= RedirMode
;
284 _tcscpy(Redir
->Filename
, Tok
);
290 FreeRedirection(*List
);
295 static PARSED_COMMAND
*ParseCommandOp(int OpType
);
297 /* Parse a parenthesized block */
298 static PARSED_COMMAND
*ParseBlock(REDIRECTION
*RedirList
)
300 PARSED_COMMAND
*Cmd
, *Sub
, **NextPtr
;
302 Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
305 WARN("Cannot allocate memory for Cmd!\n");
307 FreeRedirection(RedirList
);
312 Cmd
->Subcommands
= NULL
;
313 Cmd
->Redirections
= RedirList
;
315 /* Read the block contents */
316 NextPtr
= &Cmd
->Subcommands
;
320 Sub
= ParseCommandOp(C_OP_LOWEST
);
324 NextPtr
= &Sub
->Next
;
326 else if (bParseError
)
333 if (CurrentTokenType
== TOK_END_BLOCK
)
336 /* Skip past the \n */
341 /* Process any trailing redirections */
342 while (ParseToken(0, STANDARD_SEPS
) == TOK_REDIRECTION
)
344 if (!ParseRedirection(&Cmd
->Redirections
))
353 /* Parse an IF statement */
354 static PARSED_COMMAND
*ParseIf(void)
359 Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
362 WARN("Cannot allocate memory for Cmd!\n");
366 memset(Cmd
, 0, sizeof(PARSED_COMMAND
));
369 Type
= CurrentTokenType
;
370 if (_tcsicmp(CurrentToken
, _T("/I")) == 0)
372 Cmd
->If
.Flags
|= IFFLAG_IGNORECASE
;
373 Type
= ParseToken(0, STANDARD_SEPS
);
375 if (_tcsicmp(CurrentToken
, _T("not")) == 0)
377 Cmd
->If
.Flags
|= IFFLAG_NEGATE
;
378 Type
= ParseToken(0, STANDARD_SEPS
);
381 if (Type
!= TOK_NORMAL
)
388 /* Check for unary operators */
389 for (; Cmd
->If
.Operator
<= IF_MAX_UNARY
; Cmd
->If
.Operator
++)
391 if (_tcsicmp(CurrentToken
, IfOperatorString
[Cmd
->If
.Operator
]) == 0)
393 if (ParseToken(0, STANDARD_SEPS
) != TOK_NORMAL
)
399 Cmd
->If
.RightArg
= cmd_dup(CurrentToken
);
404 /* It must be a two-argument (comparison) operator. It could be ==, so
405 * the equals sign can't be treated as whitespace here. */
406 Cmd
->If
.LeftArg
= cmd_dup(CurrentToken
);
407 ParseToken(0, _T(",;"));
409 /* The right argument can come immediately after == */
410 if (_tcsnicmp(CurrentToken
, _T("=="), 2) == 0 && CurrentToken
[2])
412 Cmd
->If
.RightArg
= cmd_dup(&CurrentToken
[2]);
416 for (; Cmd
->If
.Operator
<= IF_MAX_COMPARISON
; Cmd
->If
.Operator
++)
418 if (_tcsicmp(CurrentToken
, IfOperatorString
[Cmd
->If
.Operator
]) == 0)
420 if (ParseToken(0, STANDARD_SEPS
) != TOK_NORMAL
)
422 Cmd
->If
.RightArg
= cmd_dup(CurrentToken
);
431 Cmd
->Subcommands
= ParseCommandOp(C_OP_LOWEST
);
432 if (Cmd
->Subcommands
== NULL
)
437 if (_tcsicmp(CurrentToken
, _T("else")) == 0)
439 Cmd
->Subcommands
->Next
= ParseCommandOp(C_OP_LOWEST
);
440 if (Cmd
->Subcommands
->Next
== NULL
)
451 * Parse a FOR command.
452 * Syntax is: FOR [options] %var IN (list) DO command
454 static PARSED_COMMAND
*ParseFor(void)
457 TCHAR List
[CMDLINE_LENGTH
];
460 Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
463 WARN("Cannot allocate memory for Cmd!\n");
467 memset(Cmd
, 0, sizeof(PARSED_COMMAND
));
472 if (_tcsicmp(CurrentToken
, _T("/D")) == 0)
473 Cmd
->For
.Switches
|= FOR_DIRS
;
474 else if (_tcsicmp(CurrentToken
, _T("/F")) == 0)
476 Cmd
->For
.Switches
|= FOR_F
;
477 if (!Cmd
->For
.Params
)
479 ParseToken(0, STANDARD_SEPS
);
480 if (CurrentToken
[0] == _T('/') || CurrentToken
[0] == _T('%'))
482 Cmd
->For
.Params
= cmd_dup(CurrentToken
);
485 else if (_tcsicmp(CurrentToken
, _T("/L")) == 0)
486 Cmd
->For
.Switches
|= FOR_LOOP
;
487 else if (_tcsicmp(CurrentToken
, _T("/R")) == 0)
489 Cmd
->For
.Switches
|= FOR_RECURSIVE
;
490 if (!Cmd
->For
.Params
)
492 ParseToken(0, STANDARD_SEPS
);
493 if (CurrentToken
[0] == _T('/') || CurrentToken
[0] == _T('%'))
495 StripQuotes(CurrentToken
);
496 Cmd
->For
.Params
= cmd_dup(CurrentToken
);
501 ParseToken(0, STANDARD_SEPS
);
504 /* Make sure there aren't two different switches specified
505 * at the same time, unless they're /D and /R */
506 if ((Cmd
->For
.Switches
& (Cmd
->For
.Switches
- 1)) != 0
507 && Cmd
->For
.Switches
!= (FOR_DIRS
| FOR_RECURSIVE
))
512 /* Variable name should be % and just one other character */
513 if (CurrentToken
[0] != _T('%') || _tcslen(CurrentToken
) != 2)
515 Cmd
->For
.Variable
= CurrentToken
[1];
517 ParseToken(0, STANDARD_SEPS
);
518 if (_tcsicmp(CurrentToken
, _T("in")) != 0)
521 if (ParseToken(_T('('), STANDARD_SEPS
) != TOK_BEGIN_BLOCK
)
528 /* Pretend we're inside a block so the tokenizer will stop on ')' */
530 Type
= ParseToken(0, STANDARD_SEPS
);
533 if (Type
== TOK_END_BLOCK
)
538 /* Skip past the \n */
543 if (Type
!= TOK_NORMAL
)
549 if (Pos
+ _tcslen(CurrentToken
) >= &List
[CMDLINE_LENGTH
])
551 Pos
= _stpcpy(Pos
, CurrentToken
);
554 Cmd
->For
.List
= cmd_dup(List
);
556 ParseToken(0, STANDARD_SEPS
);
557 if (_tcsicmp(CurrentToken
, _T("do")) != 0)
560 Cmd
->Subcommands
= ParseCommandOp(C_OP_LOWEST
);
561 if (Cmd
->Subcommands
== NULL
)
575 /* Parse a REM command */
576 static PARSED_COMMAND
*ParseRem(void)
578 /* Just ignore the rest of the line */
579 while (CurChar
&& CurChar
!= _T('\n'))
584 static DECLSPEC_NOINLINE PARSED_COMMAND
*ParseCommandPart(REDIRECTION
*RedirList
)
586 TCHAR ParsedLine
[CMDLINE_LENGTH
];
588 PARSED_COMMAND
*(*Func
)(void);
590 TCHAR
*Pos
= _stpcpy(ParsedLine
, CurrentToken
) + 1;
591 DWORD_PTR TailOffset
= Pos
- ParsedLine
;
593 /* Check for special forms */
594 if ((Func
= ParseFor
, _tcsicmp(ParsedLine
, _T("for")) == 0) ||
595 (Func
= ParseIf
, _tcsicmp(ParsedLine
, _T("if")) == 0) ||
596 (Func
= ParseRem
, _tcsicmp(ParsedLine
, _T("rem")) == 0))
598 ParseToken(0, STANDARD_SEPS
);
599 /* Do special parsing only if it's not followed by /? */
600 if (_tcscmp(CurrentToken
, _T("/?")) != 0)
605 FreeRedirection(RedirList
);
610 Pos
= _stpcpy(Pos
, _T(" /?"));
613 /* Now get the tail */
616 int Type
= ParseToken(0, NULL
);
617 if (Type
== TOK_NORMAL
)
619 if (Pos
+ _tcslen(CurrentToken
) >= &ParsedLine
[CMDLINE_LENGTH
])
622 FreeRedirection(RedirList
);
625 Pos
= _stpcpy(Pos
, CurrentToken
);
627 else if (Type
== TOK_REDIRECTION
)
629 if (!ParseRedirection(&RedirList
))
639 Cmd
= cmd_alloc(FIELD_OFFSET(PARSED_COMMAND
, Command
.First
[Pos
- ParsedLine
]));
642 WARN("Cannot allocate memory for Cmd!\n");
644 FreeRedirection(RedirList
);
647 Cmd
->Type
= C_COMMAND
;
649 Cmd
->Subcommands
= NULL
;
650 Cmd
->Redirections
= RedirList
;
651 memcpy(Cmd
->Command
.First
, ParsedLine
, (Pos
- ParsedLine
) * sizeof(TCHAR
));
652 Cmd
->Command
.Rest
= Cmd
->Command
.First
+ TailOffset
;
656 static PARSED_COMMAND
*ParsePrimary(void)
658 REDIRECTION
*RedirList
= NULL
;
661 while (IsSeparator(CurChar
))
663 if (CurChar
== _T('\n'))
671 if (CurChar
== _T(':'))
673 /* "Ignore" the rest of the line.
674 * (Line continuations will still be parsed, though.) */
675 while (ParseToken(0, NULL
) != TOK_END
)
680 if (CurChar
== _T('@'))
684 Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
687 WARN("Cannot allocate memory for Cmd!\n");
693 /* @ acts like a unary operator with low precedence,
694 * so call the top-level parser */
695 Cmd
->Subcommands
= ParseCommandOp(C_OP_LOWEST
);
696 Cmd
->Redirections
= NULL
;
700 /* Process leading redirections and get the head of the command */
701 while ((Type
= ParseToken(_T('('), STANDARD_SEPS
)) == TOK_REDIRECTION
)
703 if (!ParseRedirection(&RedirList
))
707 if (Type
== TOK_NORMAL
)
708 return ParseCommandPart(RedirList
);
709 else if (Type
== TOK_BEGIN_BLOCK
)
710 return ParseBlock(RedirList
);
711 else if (Type
== TOK_END_BLOCK
&& !RedirList
)
715 FreeRedirection(RedirList
);
719 static PARSED_COMMAND
*ParseCommandOp(int OpType
)
721 PARSED_COMMAND
*Cmd
, *Left
, *Right
;
723 if (OpType
== C_OP_HIGHEST
)
724 Cmd
= ParsePrimary();
726 Cmd
= ParseCommandOp(OpType
+ 1);
728 if (Cmd
&& !_tcscmp(CurrentToken
, OpString
[OpType
- C_OP_LOWEST
]))
731 Right
= ParseCommandOp(OpType
);
736 /* & is allowed to have an empty RHS */
737 if (OpType
== C_MULTI
)
745 Cmd
= cmd_alloc(sizeof(PARSED_COMMAND
));
748 WARN("Cannot allocate memory for Cmd!\n");
756 Cmd
->Redirections
= NULL
;
757 Cmd
->Subcommands
= Left
;
766 ParseCommand(LPTSTR Line
)
772 if (!SubstituteVars(Line
, ParseLine
, _T('%')))
774 bLineContinuations
= FALSE
;
778 if (!ReadLine(ParseLine
, FALSE
))
780 bLineContinuations
= TRUE
;
783 ParsePos
= ParseLine
;
786 Cmd
= ParseCommandOp(C_OP_LOWEST
);
789 if (CurrentTokenType
!= TOK_END
)
807 * Reconstruct a parse tree into text form; used for echoing
808 * batch file commands and FOR instances.
811 EchoCommand(PARSED_COMMAND
*Cmd
)
813 TCHAR Buf
[CMDLINE_LENGTH
];
820 if (SubstituteForVars(Cmd
->Command
.First
, Buf
))
821 ConOutPrintf(_T("%s"), Buf
);
822 if (SubstituteForVars(Cmd
->Command
.Rest
, Buf
))
823 ConOutPrintf(_T("%s"), Buf
);
829 Sub
= Cmd
->Subcommands
;
830 if (Sub
&& !Sub
->Next
)
832 /* Single-command block: display all on one line */
837 /* Multi-command block: display parenthesis on separate lines */
838 ConOutChar(_T('\n'));
842 ConOutChar(_T('\n'));
852 Sub
= Cmd
->Subcommands
;
854 ConOutPrintf(_T(" %s "), OpString
[Cmd
->Type
- C_OP_LOWEST
]);
855 EchoCommand(Sub
->Next
);
858 ConOutPrintf(_T("if"));
859 if (Cmd
->If
.Flags
& IFFLAG_IGNORECASE
)
860 ConOutPrintf(_T(" /I"));
861 if (Cmd
->If
.Flags
& IFFLAG_NEGATE
)
862 ConOutPrintf(_T(" not"));
863 if (Cmd
->If
.LeftArg
&& SubstituteForVars(Cmd
->If
.LeftArg
, Buf
))
864 ConOutPrintf(_T(" %s"), Buf
);
865 ConOutPrintf(_T(" %s"), IfOperatorString
[Cmd
->If
.Operator
]);
866 if (SubstituteForVars(Cmd
->If
.RightArg
, Buf
))
867 ConOutPrintf(_T(" %s "), Buf
);
868 Sub
= Cmd
->Subcommands
;
872 ConOutPrintf(_T(" else "));
873 EchoCommand(Sub
->Next
);
877 ConOutPrintf(_T("for"));
878 if (Cmd
->For
.Switches
& FOR_DIRS
) ConOutPrintf(_T(" /D"));
879 if (Cmd
->For
.Switches
& FOR_F
) ConOutPrintf(_T(" /F"));
880 if (Cmd
->For
.Switches
& FOR_LOOP
) ConOutPrintf(_T(" /L"));
881 if (Cmd
->For
.Switches
& FOR_RECURSIVE
) ConOutPrintf(_T(" /R"));
883 ConOutPrintf(_T(" %s"), Cmd
->For
.Params
);
884 ConOutPrintf(_T(" %%%c in (%s) do "), Cmd
->For
.Variable
, Cmd
->For
.List
);
885 EchoCommand(Cmd
->Subcommands
);
889 for (Redir
= Cmd
->Redirections
; Redir
; Redir
= Redir
->Next
)
891 if (SubstituteForVars(Redir
->Filename
, Buf
))
893 ConOutPrintf(_T(" %c%s%s"), _T('0') + Redir
->Number
,
894 RedirString
[Redir
->Mode
], Buf
);
900 * "Unparse" a command into a text form suitable for passing to CMD /C.
901 * Used for pipes. This is basically the same thing as EchoCommand, but
902 * writing into a string instead of to standard output.
905 Unparse(PARSED_COMMAND
*Cmd
, TCHAR
*Out
, TCHAR
*OutEnd
)
907 TCHAR Buf
[CMDLINE_LENGTH
];
912 * Since this function has the annoying requirement that it must avoid
913 * overflowing the supplied buffer, define some helper macros to make
918 if (Out == OutEnd) return NULL; \
921 #define STRING(String) \
923 if (Out + _tcslen(String) > OutEnd) return NULL; \
924 Out = _stpcpy(Out, String); \
926 #define PRINTF(Format, ...) \
928 UINT Len = _sntprintf(Out, OutEnd - Out, Format, __VA_ARGS__); \
929 if (Len > (UINT)(OutEnd - Out)) return NULL; \
932 #define RECURSE(Subcommand) \
934 Out = Unparse(Subcommand, Out, OutEnd); \
935 if (!Out) return NULL; \
941 /* This is fragile since there could be special characters, but
942 * Windows doesn't bother escaping them, so for compatibility
943 * we probably shouldn't do it either */
944 if (!SubstituteForVars(Cmd
->Command
.First
, Buf
)) return NULL
;
946 if (!SubstituteForVars(Cmd
->Command
.Rest
, Buf
)) return NULL
;
951 RECURSE(Cmd
->Subcommands
);
955 for (Sub
= Cmd
->Subcommands
; Sub
; Sub
= Sub
->Next
)
967 Sub
= Cmd
->Subcommands
;
969 PRINTF(_T(" %s "), OpString
[Cmd
->Type
- C_OP_LOWEST
]);
974 if (Cmd
->If
.Flags
& IFFLAG_IGNORECASE
)
976 if (Cmd
->If
.Flags
& IFFLAG_NEGATE
)
978 if (Cmd
->If
.LeftArg
&& SubstituteForVars(Cmd
->If
.LeftArg
, Buf
))
979 PRINTF(_T(" %s"), Buf
);
980 PRINTF(_T(" %s"), IfOperatorString
[Cmd
->If
.Operator
]);
981 if (!SubstituteForVars(Cmd
->If
.RightArg
, Buf
)) return NULL
;
982 PRINTF(_T(" %s "), Buf
);
983 Sub
= Cmd
->Subcommands
;
987 STRING(_T(" else "));
993 if (Cmd
->For
.Switches
& FOR_DIRS
) STRING(_T(" /D"));
994 if (Cmd
->For
.Switches
& FOR_F
) STRING(_T(" /F"));
995 if (Cmd
->For
.Switches
& FOR_LOOP
) STRING(_T(" /L"));
996 if (Cmd
->For
.Switches
& FOR_RECURSIVE
) STRING(_T(" /R"));
998 PRINTF(_T(" %s"), Cmd
->For
.Params
);
999 PRINTF(_T(" %%%c in (%s) do "), Cmd
->For
.Variable
, Cmd
->For
.List
);
1000 RECURSE(Cmd
->Subcommands
);
1004 for (Redir
= Cmd
->Redirections
; Redir
; Redir
= Redir
->Next
)
1006 if (!SubstituteForVars(Redir
->Filename
, Buf
))
1008 PRINTF(_T(" %c%s%s"), _T('0') + Redir
->Number
,
1009 RedirString
[Redir
->Mode
], Buf
);
1015 FreeCommand(PARSED_COMMAND
*Cmd
)
1017 if (Cmd
->Subcommands
)
1018 FreeCommand(Cmd
->Subcommands
);
1020 FreeCommand(Cmd
->Next
);
1021 FreeRedirection(Cmd
->Redirections
);
1022 if (Cmd
->Type
== C_IF
)
1024 cmd_free(Cmd
->If
.LeftArg
);
1025 cmd_free(Cmd
->If
.RightArg
);
1027 else if (Cmd
->Type
== C_FOR
)
1029 cmd_free(Cmd
->For
.Params
);
1030 cmd_free(Cmd
->For
.List
);