reactos/base/shell/cmd/parser.c
Hermès Bélusca-Maïto 37bda06eed
[CMD] CALL: Fix the implementation of the CALL command, make it more compatible with Windows' CMD.
- Fail if no parameter is provided.

- The "CALL :label args..." syntax is available only when command extensions
  are enabled. Fail if this syntax is used outside of a batch context.

- Reparse the CALL command parameter with the command parser, in order
  to accurately parse and interpret it as a possible command (including
  escape carets, etc...) and not duplicate the logic.
  ** CURRENT Windows' CMD-compatibility LIMITATION ** (may be lifted in
  a "ROS-specific" running mode of CMD): only allow standard commands to
  be specified as parameter of the CALL command.

  This reparsing behaviour can be observed in Windows' CMD, by dumping
  the interpreted commands after enabling the cmd!fDumpParse flag from
  a debugger (using public symbols).

- When reparsing, we should tell the parser to NOT ignore lines that
  start with a colon, because in this situation these are to be
  considered as valid "commands" (for parsing "CALL :label").

  * For Windows' CMD-compatibility, the remaining escape carets need to
    be doubled again so that, after the new parser step, they are escaped
    back to their original form. But then we also need to do it the "buggy"
    way à la Windows, where carets in quotes are doubled either! However
    when being re-parsed, since they are in quotes they remain doubled!!
    (see "Phase 6" in https://stackoverflow.com/a/4095133/13530036 ).

  * A MSCMD_CALL_QUIRKS define allows to disable this buggy behaviour,
    and instead tell the parser to not not interpret the escape carets.

- When initializing a new batch context when the "CALL :label" syntax is
  used, ensure that we reuse the same batch file position pointer as its
  parent, so as to have correct call label ordering behaviour.

  That is,

  :label
  ECHO hi
  CALL :label
  :label
  ECHO bye

  should display:

  hi
  bye
  bye

  i.e., the CALL calls the second label instead of the first one (and
  thus entering into an infinite loop).

  Finally, the "CALL :label" syntax strips the first ':' away, so, as a
  side-effect, the command "CALL :EOF" fails (otherwise it would perform
  a "GOTO :EOF" and succeeds), while "CALL ::EOF" succeeds.

Fixes some cmd_winetests.
2020-09-27 19:05:23 +02:00

2107 lines
58 KiB
C

/*
* PARSER.C - Command-line Lexical Analyzer/Tokenizer and Parser.
*/
#include "precomp.h"
/*
* Defines for enabling different Windows' CMD compatibility behaviours.
*/
/* Enable this define for command echoer compatibility */
#define MSCMD_ECHO_COMMAND_COMPAT
/* Enable this define for parser quirks (see UnParseToken() for more details) */
#define MSCMD_PARSER_BUGS
/* Enable this define for parenthesized blocks parsing quirks */
// #define MSCMD_PARENS_PARSE_BUGS
/* Enable this define for redirection parsing quirks */
#define MSCMD_REDIR_PARSE_BUGS
/* Enable this define for allowing '&' commands with an empty RHS.
* The default behaviour is to just return the LHS instead.
* See ParseCommandBinaryOp() for details. */
// #define MSCMD_MULTI_EMPTY_RHS
/*
* Parser debugging support. These flags are global so that their values can be
* modified at runtime from a debugger. They correspond to the public Windows'
* cmd!fDumpTokens and cmd!fDumpParse booleans.
* (Same names are used for compatibility as they are documented online.)
*/
BOOLEAN fDumpTokens = FALSE;
BOOLEAN fDumpParse = FALSE;
#define C_OP_LOWEST C_MULTI
#define C_OP_HIGHEST C_PIPE
static const TCHAR OpString[][3] = { _T("&"), _T("||"), _T("&&"), _T("|") };
static const TCHAR RedirString[][3] = { _T("<"), _T(">"), _T(">>") };
static const TCHAR* const IfOperatorString[] =
{
/** Unary operators **/
/* Standard */
_T("errorlevel"),
_T("exist"),
/* Extended */
_T("cmdextversion"),
_T("defined"),
#define IF_MAX_UNARY IF_DEFINED
/** Binary operators **/
/* Standard */
_T("=="),
/* Extended */
_T("equ"),
_T("neq"),
_T("lss"),
_T("leq"),
_T("gtr"),
_T("geq"),
#define IF_MAX_COMPARISON IF_GEQ
};
static __inline BOOL IsSeparator(TCHAR Char)
{
return _istspace(Char) || (Char && !!_tcschr(STANDARD_SEPS, Char));
}
typedef enum _TOK_TYPE
{
TOK_END,
TOK_NORMAL,
TOK_OPERATOR,
TOK_REDIRECTION,
TOK_BEGIN_BLOCK,
TOK_END_BLOCK
} TOK_TYPE;
/* Scratch buffer for temporary command substitutions / expansions */
static TCHAR TempBuf[CMDLINE_LENGTH];
/*static*/ BOOL bParseError;
static BOOL bLineContinuations;
/*static*/ TCHAR ParseLine[CMDLINE_LENGTH];
static PTCHAR ParsePos;
static PTCHAR OldParsePos;
BOOL bIgnoreParserComments = TRUE;
BOOL bHandleContinuations = TRUE;
static TCHAR CurrentToken[CMDLINE_LENGTH];
static TOK_TYPE CurrentTokenType = TOK_END;
#ifndef MSCMD_PARSER_BUGS
static BOOL bReparseToken = FALSE;
static PTCHAR LastCurTokPos;
#endif
static INT InsideBlock = 0;
static VOID ResetParser(IN PTCHAR Pos)
{
bParseError = FALSE;
ParsePos = Pos;
OldParsePos = ParsePos;
}
/*
* This function "refetches" the last parsed token back into the stream
* for later reparsing -- since the way of lexing it is context-dependent.
* This "feature" is at the root of many obscure CMD parsing quirks,
* due to the fact this feature is in opposition with line-continuation.
* Indeed, when a stream of characters has a line-continuation, the lexer-
* parser will parse the stream up to the end of the line, then will
* reset the parser state and position back to the beginning of the line
* before accepting the rest of the character stream and continuing
* parsing them. This means that all the non-parsed characters before the
* line-continuation have been lost. Of course, their parsed form is now
* within the current parsed token. However, suppose now we need to
* unparse this token for reparsing it a different way later on. If we
* somehow pushed the already-parsed current token back into the beginning
* of the character stream, besides the complications of moving up the
* characters in the stream buffer, we would basically have "new" data
* that has been already parsed one way, to be now parsed another way.
* If instead we had saved somehow the unparsed form of the token, and
* we push back that form into the stream buffer for reparsing, we would
* encounter again the line-continuation, that, depending on which
* context the token is reparsed, would cause problems:
* e.g. in the case of REM command parsing, the parser would stop at the
* first line-continuation.
*
* When MSCMD_PARSER_BUGS is undefined, the UnParseToken() / ParseToken()
* cycle keeps the current token in its buffer, but also saves the start
* position corresponding to the batch of characters that have been parsed
* during the last line-continuation. The next ParseToken() would then
* reparse these latest charcters and the result replaces the last part
* in the current token.
*
* For example, a first parsing of
* foo^\n
* bar^\n
* baz
* would result in the current token "foobarbaz", where the start position
* corresponding to the batch of characters parsed during the last line-continuation
* being pointing at "baz". The stream buffer only contains "baz" (and following data).
* Then UnParseToken() saves this info so that at the next ParseToken(), the "baz"
* part of the stream buffer gets reparsed (possibly differently) and the result
* would replace the "baz" part in the current token.
*
* If MSCMD_PARSER_BUGS is defined however, then the behaviour of the Windows' CMD
* parser applies: in the example above, the last ParseToken() call would completely
* replace the current token "foobarbaz" with the new result of the parsing of "baz".
*/
static VOID UnParseToken(VOID)
{
ParsePos = OldParsePos;
/* Debugging support */
if (fDumpTokens)
ConOutPrintf(_T("Ungetting: '%s'\n"), ParsePos);
#ifndef MSCMD_PARSER_BUGS
bReparseToken = TRUE;
#endif
}
static VOID InitParser(VOID)
{
*CurrentToken = 0;
CurrentTokenType = TOK_END;
InsideBlock = 0;
#ifndef MSCMD_PARSER_BUGS
bReparseToken = FALSE;
LastCurTokPos = NULL;
#endif
ResetParser(ParseLine);
}
static TCHAR ParseChar(VOID)
{
TCHAR Char;
if (bParseError)
return 0;
restart:
/*
* Although CRs can be injected into a line via an environment
* variable substitution, the parser ignores them - they won't
* even separate tokens.
*/
do
{
Char = *ParsePos++;
}
while (Char == _T('\r'));
if (!Char) --ParsePos;
if (!Char && bLineContinuations)
{
if (!ReadLine(ParseLine, TRUE))
{
/* ^C pressed, or line was too long */
//
// FIXME: Distinguish with respect to BATCH end of file !!
//
bParseError = TRUE;
}
else
{
ResetParser(ParseLine);
if (*ParsePos)
goto restart;
}
}
return Char;
}
VOID ParseErrorEx(IN PCTSTR s)
{
/* Only display the first error we encounter */
if (!bParseError)
error_syntax(s);
bParseError = TRUE;
}
static __inline VOID ParseError(VOID)
{
ParseErrorEx(CurrentTokenType != TOK_END ? CurrentToken : NULL);
}
static TOK_TYPE
ParseTokenEx(
IN TCHAR PrefixOperator OPTIONAL,
IN TCHAR ExtraEnd OPTIONAL,
IN PCTSTR Separators OPTIONAL,
IN BOOL bHandleContinuations)
{
TOK_TYPE Type;
PTCHAR CurrentTokStart = CurrentToken;
PTCHAR Out = CurrentTokStart;
TCHAR Char;
BOOL bInQuote = FALSE;
#ifndef MSCMD_PARSER_BUGS
if (bReparseToken)
{
bReparseToken = FALSE;
/*
* We will append the part to be reparsed to the old one
* (still present in CurrentToken).
*/
CurrentTokStart = LastCurTokPos;
Out = CurrentTokStart;
}
else
{
LastCurTokPos = CurrentToken;
}
#endif
/* Start with what we have at current ParsePos */
OldParsePos = ParsePos;
for (Char = ParseChar(); Char && Char != _T('\n'); Char = ParseChar())
{
bInQuote ^= (Char == _T('"'));
if (!bInQuote)
{
if (Separators != NULL)
{
if (_istspace(Char) || !!_tcschr(Separators, Char))
{
/* Skip leading separators */
if (Out == CurrentTokStart)
continue;
break;
}
}
/* Check for prefix operator */
if ((Out == CurrentTokStart) && (Char == PrefixOperator))
break;
/*
* Check for numbered redirection.
*
* For this purpose, we check whether this is a number, that is
* in first position in the current parsing buffer (remember that
* ParsePos points to the next character) or is preceded by a
* whitespace-like separator, including standard command operators
* (excepting '@' !) and double-quotes.
*/
if ( _istdigit(Char) &&
(ParsePos == &OldParsePos[1] ||
IsSeparator(ParsePos[-2]) ||
!!_tcschr(_T("()&|\""), ParsePos[-2])) &&
(*ParsePos == _T('<') || *ParsePos == _T('>')) )
{
break;
}
/* Check for other delimiters / operators */
if (Char == ExtraEnd)
break;
if (InsideBlock && Char == _T(')'))
break;
if (_tcschr(_T("&|<>"), Char))
break;
if (bHandleContinuations && (Char == _T('^')))
{
Char = ParseChar();
/* Eat up a \n, allowing line continuation */
if (Char == _T('\n'))
{
#ifndef MSCMD_PARSER_BUGS
LastCurTokPos = Out;
#endif
Char = ParseChar();
}
/* Next character is a forced literal */
if (Out == CurrentTokStart)
{
/* Ignore any prefix operator if we don't start a new command block */
if (CurrentTokenType != TOK_BEGIN_BLOCK)
PrefixOperator = 0;
}
}
}
if (Out == &CurrentToken[CMDLINE_LENGTH - 1])
break;
*Out++ = Char;
// PrefixOperator = 0;
}
/*
* We exited the parsing loop. If the current character is the first one
* (Out == CurrentTokStart), interpret it as an operator. Otherwise,
* terminate the current token (type TOK_NORMAL) and keep the current
* character so that it can be refetched as an operator at the next call.
*/
if (Out != CurrentTokStart)
{
Type = TOK_NORMAL;
}
/*
* Else we have an operator.
*/
else if (Char == _T('@'))
{
Type = TOK_OPERATOR; // TOK_QUIET / TOK_PREFIX_OPERATOR
*Out++ = Char;
Char = ParseChar();
}
else if (Char == _T('('))
{
Type = TOK_BEGIN_BLOCK;
*Out++ = Char;
Char = ParseChar();
}
else if (Char == _T(')'))
{
Type = TOK_END_BLOCK;
*Out++ = Char;
Char = ParseChar();
}
else if (Char == _T('&') || Char == _T('|'))
{
Type = TOK_OPERATOR;
*Out++ = Char;
Char = ParseChar();
/* Check for '&&' or '||' */
if (Char == Out[-1])
{
*Out++ = Char;
Char = ParseChar();
}
}
else if ( _istdigit(Char) ||
(Char == _T('<') || Char == _T('>')) )
{
Type = TOK_REDIRECTION;
if (_istdigit(Char))
{
*Out++ = Char;
Char = ParseChar();
}
/* By construction (see the while-loop above),
* the next character must be a redirection. */
ASSERT(Char == _T('<') || Char == _T('>'));
*Out++ = Char;
Char = ParseChar();
if (Char == Out[-1])
{
/* Strangely, the tokenizer allows << as well as >>... (it
* will cause an error when trying to parse it though) */
*Out++ = Char;
Char = ParseChar();
}
if (Char == _T('&'))
{
*Out++ = Char;
while (IsSeparator(Char = ParseChar()))
;
if (_istdigit(Char))
{
*Out++ = Char;
Char = ParseChar();
}
}
}
else
{
Type = TOK_END;
*Out++ = Char;
}
*Out = _T('\0');
/*
* Rewind the parsing position, so that the current character can be
* refetched later on. However do this only if it is not NULL and if
* this is not TOK_END, since we do not want to reparse later the line
* termination (we could enter into infinite loops, or, in case of line
* continuation, get unwanted "More?" prompts).
*/
if (Char != 0 && Type != TOK_END)
--ParsePos;
/* Debugging support */
if (fDumpTokens)
ConOutPrintf(_T("ParseToken: (%d) '%s'\n"), Type, CurrentToken);
return (CurrentTokenType = Type);
}
static __inline INT
ParseToken(
IN TCHAR ExtraEnd OPTIONAL,
IN PCTSTR Separators OPTIONAL)
{
return ParseTokenEx(0, ExtraEnd, Separators, bHandleContinuations);
}
static PARSED_COMMAND*
AllocCommand(
IN COMMAND_TYPE Type,
IN PCTSTR CmdHead OPTIONAL,
IN PCTSTR CmdTail OPTIONAL)
{
PARSED_COMMAND* Cmd;
switch (Type)
{
case C_COMMAND:
case C_REM:
{
SIZE_T CmdHeadLen = _tcslen(CmdHead) + 1;
SIZE_T CmdTailLen = _tcslen(CmdTail) + 1;
Cmd = cmd_alloc(FIELD_OFFSET(PARSED_COMMAND,
Command.First[CmdHeadLen + CmdTailLen]));
if (!Cmd)
return NULL;
Cmd->Type = Type;
Cmd->Next = NULL;
Cmd->Subcommands = NULL;
Cmd->Redirections = NULL; /* Is assigned by the calling function */
memcpy(Cmd->Command.First, CmdHead, CmdHeadLen * sizeof(TCHAR));
Cmd->Command.Rest = Cmd->Command.First + CmdHeadLen;
memcpy(Cmd->Command.Rest, CmdTail, CmdTailLen * sizeof(TCHAR));
return Cmd;
}
case C_QUIET:
case C_BLOCK:
case C_MULTI:
case C_OR:
case C_AND:
case C_PIPE:
{
Cmd = cmd_alloc(sizeof(PARSED_COMMAND));
if (!Cmd)
return NULL;
Cmd->Type = Type;
Cmd->Next = NULL;
Cmd->Subcommands = NULL;
Cmd->Redirections = NULL; /* For C_BLOCK only: is assigned by the calling function */
return Cmd;
}
case C_FOR:
case C_IF:
{
Cmd = cmd_alloc(sizeof(PARSED_COMMAND));
if (!Cmd)
return NULL;
memset(Cmd, 0, sizeof(PARSED_COMMAND));
Cmd->Type = Type;
return Cmd;
}
default:
ERR("Unknown command type 0x%x\n", Type);
ASSERT(FALSE);
return NULL;
}
}
VOID
FreeCommand(
IN OUT PARSED_COMMAND* Cmd)
{
if (Cmd->Subcommands)
FreeCommand(Cmd->Subcommands);
if (Cmd->Next)
FreeCommand(Cmd->Next);
FreeRedirection(Cmd->Redirections);
if (Cmd->Type == C_FOR)
{
cmd_free(Cmd->For.Params);
cmd_free(Cmd->For.List);
}
else if (Cmd->Type == C_IF)
{
cmd_free(Cmd->If.LeftArg);
cmd_free(Cmd->If.RightArg);
}
cmd_free(Cmd);
}
/* Parse redirections and append them to the list */
static BOOL
ParseRedirection(
IN OUT REDIRECTION** List)
{
PTSTR Tok = CurrentToken;
REDIRECTION* Redir;
REDIR_MODE RedirMode;
BYTE Number;
if ( !(*Tok == _T('<') || *Tok == _T('>')) &&
!(_istdigit(*Tok) &&
(Tok[1] == _T('<') || Tok[1] == _T('>')) ) )
{
ASSERT(CurrentTokenType != TOK_REDIRECTION);
return FALSE;
}
ASSERT((CurrentTokenType == TOK_REDIRECTION) ||
(CurrentTokenType == TOK_NORMAL));
if (_istdigit(*Tok))
Number = *Tok++ - _T('0');
else
Number = *Tok == _T('<') ? 0 : 1;
if (*Tok++ == _T('<'))
{
RedirMode = REDIR_READ;
/* Forbid '<<' */
if (*Tok == _T('<'))
goto fail;
}
else
{
RedirMode = REDIR_WRITE;
if (*Tok == _T('>'))
{
RedirMode = REDIR_APPEND;
Tok++;
}
}
if (*Tok == _T('&'))
{
/* This is a handle redirection: the next character must be one single digit */
if (!(_istdigit(Tok[1]) && !Tok[2]))
goto fail;
}
else
#ifndef MSCMD_REDIR_PARSE_BUGS
if (!*Tok)
/* The file name was not part of this token, so it will be the next one */
#else
/* Get rid of what possibly remains in the token, and retrieve the next one */
#endif
{
if (ParseToken(0, STANDARD_SEPS) != TOK_NORMAL)
goto fail;
Tok = CurrentToken;
}
/* If a redirection for this handle number already exists, delete it */
while ((Redir = *List))
{
if (Redir->Number == Number)
{
*List = Redir->Next;
cmd_free(Redir);
continue;
}
List = &Redir->Next;
}
Redir = cmd_alloc(FIELD_OFFSET(REDIRECTION, Filename[_tcslen(Tok) + 1]));
if (!Redir)
{
WARN("Cannot allocate memory for Redir!\n");
goto fail;
}
Redir->Next = NULL;
Redir->OldHandle = INVALID_HANDLE_VALUE;
Redir->Number = Number;
Redir->Mode = RedirMode;
_tcscpy(Redir->Filename, Tok);
*List = Redir;
return TRUE;
fail:
ParseError();
FreeRedirection(*List);
*List = NULL;
return FALSE;
}
static __inline PARSED_COMMAND*
ParseCommandOp(
IN COMMAND_TYPE OpType);
/* Parse a parenthesized block */
static PARSED_COMMAND*
ParseBlock(
IN OUT REDIRECTION** RedirList)
{
PARSED_COMMAND *Cmd, *Sub, **NextPtr;
Cmd = AllocCommand(C_BLOCK, NULL, NULL);
if (!Cmd)
{
WARN("Cannot allocate memory for Cmd!\n");
ParseError();
return NULL;
}
/* Read the block contents */
NextPtr = &Cmd->Subcommands;
++InsideBlock;
while (TRUE)
{
/*
* Windows' CMD compatibility: Strip leading newlines in the block.
*
* Note that this behaviour is buggy, especially when MSCMD_PARSER_BUGS is defined!
* For example:
* (foo^\n
* bar)
* would be parsed ultimately as: '(', 'bar', ')' because the "foo^"
* part would be discarded due to the UnParseToken() call, since this
* function doesn't work across line continuations.
*/
while (ParseToken(0, STANDARD_SEPS) == TOK_END && *CurrentToken == _T('\n'))
;
if (*CurrentToken && *CurrentToken != _T('\n'))
UnParseToken();
/* Break early if we have nothing else to read. We will also fail
* due to the fact we haven't encountered any closing parenthesis. */
if (!*CurrentToken /* || *CurrentToken == _T('\n') */)
{
ASSERT(CurrentTokenType == TOK_END);
break;
}
/*
* NOTE: Windows' CMD uses a "CRLF" operator when dealing with
* newlines in parenthesized blocks, as an alternative to the
* '&' command-separation operator.
*/
Sub = ParseCommandOp(C_OP_LOWEST);
if (Sub)
{
*NextPtr = Sub;
NextPtr = &Sub->Next;
}
else if (bParseError)
{
--InsideBlock;
FreeCommand(Cmd);
return NULL;
}
if (CurrentTokenType == TOK_END_BLOCK)
break;
/* Skip past the \n */
}
--InsideBlock;
/* Fail if the block was not terminated, or if we have
* an empty block, i.e. "( )", considered invalid. */
if ((CurrentTokenType != TOK_END_BLOCK) || (Cmd->Subcommands == NULL))
{
ParseError();
FreeCommand(Cmd);
return NULL;
}
/* Process any trailing redirections and append them to the list */
#ifndef MSCMD_REDIR_PARSE_BUGS
while (ParseToken(0, STANDARD_SEPS) == TOK_REDIRECTION)
{
if (!ParseRedirection(RedirList))
{
FreeCommand(Cmd);
return NULL;
}
}
#else
while (ParseToken(0, STANDARD_SEPS) != TOK_END)
{
if (!ParseRedirection(RedirList))
{
/* If an actual error happened in ParseRedirection(), bail out */
if (bParseError)
{
FreeCommand(Cmd);
return NULL;
}
/* Otherwise it just returned FALSE because the current token
* is not a redirection. Unparse the token and refetch it. */
break;
}
}
#endif
if (CurrentTokenType != TOK_END)
{
/*
* Windows' CMD compatibility: Unparse the current token.
*
* Note that this behaviour is buggy, especially when MSCMD_PARSER_BUGS is defined!
* For example:
* (foo^\n
* bar)
* would be parsed ultimately as: '(', 'bar', ')' because the "foo^"
* part would be discarded due to the UnParseToken() call, since this
* function doesn't work across line continuations.
*/
UnParseToken();
/*
* Since it is expected that when ParseBlock() returns, the next
* token is already fetched, call ParseToken() again to compensate.
*/
ParseToken(0, STANDARD_SEPS);
}
return Cmd;
}
/* Parse an IF statement */
static PARSED_COMMAND*
ParseIf(VOID)
{
PARSED_COMMAND* Cmd;
Cmd = AllocCommand(C_IF, NULL, NULL);
if (!Cmd)
{
WARN("Cannot allocate memory for Cmd!\n");
ParseError();
return NULL;
}
if (bEnableExtensions && (_tcsicmp(CurrentToken, _T("/I")) == 0))
{
Cmd->If.Flags |= IFFLAG_IGNORECASE;
ParseToken(0, STANDARD_SEPS);
}
if (_tcsicmp(CurrentToken, _T("not")) == 0)
{
Cmd->If.Flags |= IFFLAG_NEGATE;
ParseToken(0, STANDARD_SEPS);
}
if (CurrentTokenType != TOK_NORMAL)
goto error;
/* Check for unary operators */
for (; Cmd->If.Operator <= IF_MAX_UNARY; Cmd->If.Operator++)
{
/* Skip the extended operators if the extensions are disabled */
if (!bEnableExtensions && (Cmd->If.Operator >= IF_CMDEXTVERSION))
continue;
if (_tcsicmp(CurrentToken, IfOperatorString[Cmd->If.Operator]) == 0)
{
if (ParseToken(0, STANDARD_SEPS) != TOK_NORMAL)
goto error;
Cmd->If.RightArg = cmd_dup(CurrentToken);
goto condition_done;
}
}
/* It must be a two-argument (comparison) operator. It could be ==, so
* the equals sign can't be treated as whitespace here. */
Cmd->If.LeftArg = cmd_dup(CurrentToken);
ParseToken(0, _T(",;"));
/* The right argument can come immediately after == */
if (_tcsnicmp(CurrentToken, _T("=="), 2) == 0 && CurrentToken[2])
{
Cmd->If.RightArg = cmd_dup(&CurrentToken[2]);
goto condition_done;
}
// Cmd->If.Operator == IF_MAX_UNARY + 1;
for (; Cmd->If.Operator <= IF_MAX_COMPARISON; Cmd->If.Operator++)
{
/* Skip the extended operators if the extensions are disabled */
if (!bEnableExtensions && (Cmd->If.Operator >= IF_EQU)) // (Cmd->If.Operator > IF_STRINGEQ)
continue;
if (_tcsicmp(CurrentToken, IfOperatorString[Cmd->If.Operator]) == 0)
{
if (ParseToken(0, STANDARD_SEPS) != TOK_NORMAL)
goto error;
Cmd->If.RightArg = cmd_dup(CurrentToken);
goto condition_done;
}
}
goto error;
condition_done:
Cmd->Subcommands = ParseCommandOp(C_OP_LOWEST);
if (Cmd->Subcommands == NULL)
goto error;
if (_tcsicmp(CurrentToken, _T("else")) == 0)
{
Cmd->Subcommands->Next = ParseCommandOp(C_OP_LOWEST);
if (Cmd->Subcommands->Next == NULL)
goto error;
}
return Cmd;
error:
FreeCommand(Cmd);
ParseError();
return NULL;
}
/*
* Parse a FOR command.
* Syntax is: FOR [options] %var IN (list) DO command
*/
static PARSED_COMMAND*
ParseFor(VOID)
{
PARSED_COMMAND* Cmd;
/* Use the scratch buffer */
PTSTR List = TempBuf;
PTCHAR Pos = List;
Cmd = AllocCommand(C_FOR, NULL, NULL);
if (!Cmd)
{
WARN("Cannot allocate memory for Cmd!\n");
ParseError();
return NULL;
}
/* Skip the extended FOR syntax if extensions are disabled */
if (!bEnableExtensions)
goto parseForBody;
while (TRUE)
{
if (_tcsicmp(CurrentToken, _T("/D")) == 0)
{
Cmd->For.Switches |= FOR_DIRS;
}
else if (_tcsicmp(CurrentToken, _T("/F")) == 0)
{
Cmd->For.Switches |= FOR_F;
if (!Cmd->For.Params)
{
ParseToken(0, STANDARD_SEPS);
if (CurrentToken[0] == _T('/') || CurrentToken[0] == _T('%'))
break;
Cmd->For.Params = cmd_dup(CurrentToken);
}
}
else if (_tcsicmp(CurrentToken, _T("/L")) == 0)
{
Cmd->For.Switches |= FOR_LOOP;
}
else if (_tcsicmp(CurrentToken, _T("/R")) == 0)
{
Cmd->For.Switches |= FOR_RECURSIVE;
if (!Cmd->For.Params)
{
ParseToken(0, STANDARD_SEPS);
if (CurrentToken[0] == _T('/') || CurrentToken[0] == _T('%'))
break;
StripQuotes(CurrentToken);
Cmd->For.Params = cmd_dup(CurrentToken);
}
}
else
{
break;
}
ParseToken(0, STANDARD_SEPS);
}
/* Make sure there aren't two different switches specified
* at the same time, unless they're /D and /R */
if ((Cmd->For.Switches & (Cmd->For.Switches - 1)) != 0
&& Cmd->For.Switches != (FOR_DIRS | FOR_RECURSIVE))
{
goto error;
}
parseForBody:
/* Variable name should be % and just one other character */
if (CurrentToken[0] != _T('%') || _tcslen(CurrentToken) != 2)
goto error;
Cmd->For.Variable = CurrentToken[1];
ParseToken(0, STANDARD_SEPS);
if (_tcsicmp(CurrentToken, _T("in")) != 0)
goto error;
if (ParseToken(_T('('), STANDARD_SEPS) != TOK_BEGIN_BLOCK)
goto error;
while (TRUE)
{
/* Pretend we're inside a block so the tokenizer will stop on ')' */
++InsideBlock;
ParseToken(0, STANDARD_SEPS);
--InsideBlock;
if (CurrentTokenType == TOK_END_BLOCK)
break;
/* Skip past the \n */
if ((CurrentTokenType == TOK_END) && *CurrentToken == _T('\n'))
continue;
if (CurrentTokenType != TOK_NORMAL)
goto error;
if (Pos != List)
*Pos++ = _T(' ');
if (Pos + _tcslen(CurrentToken) >= &List[CMDLINE_LENGTH])
goto error;
Pos = _stpcpy(Pos, CurrentToken);
}
*Pos = _T('\0');
Cmd->For.List = cmd_dup(List);
ParseToken(0, STANDARD_SEPS);
if (_tcsicmp(CurrentToken, _T("do")) != 0)
goto error;
Cmd->Subcommands = ParseCommandOp(C_OP_LOWEST);
if (Cmd->Subcommands == NULL)
goto error;
return Cmd;
error:
FreeCommand(Cmd);
ParseError();
return NULL;
}
/* Parse a REM command */
static PARSED_COMMAND*
ParseRem(VOID)
{
PARSED_COMMAND* Cmd;
/* The common scratch buffer already contains the name of the command */
PTSTR ParsedLine = TempBuf;
PTCHAR Pos = ParsedLine + _tcslen(ParsedLine) + 1;
SIZE_T TailOffset = Pos - ParsedLine;
/* Build a minimal command for REM, so that it can still get through the batch echo unparsing */
/* Unparse the current token, so as to emulate the REM command parsing
* behaviour of Windows' CMD, that discards everything before the last
* line continuation. */
UnParseToken();
/*
* Ignore the rest of the line, without any line continuation (but eat the caret).
* We cannot simply set bLineContinuations to TRUE or FALSE, because we want (only
* for the REM command), even when bLineContinuations == FALSE, to get the caret,
* otherwise it would be ignored.
*/
while (ParseTokenEx(0, 0, NULL, FALSE) != TOK_END)
{
if (Pos + _tcslen(CurrentToken) >= &ParsedLine[CMDLINE_LENGTH])
{
ParseError();
return NULL;
}
Pos = _stpcpy(Pos, CurrentToken);
}
*Pos = _T('\0');
Cmd = AllocCommand(C_REM,
ParsedLine,
ParsedLine + TailOffset);
if (!Cmd)
{
WARN("Cannot allocate memory for Cmd!\n");
ParseError();
return NULL;
}
return Cmd;
}
/* Parse a command */
static PARSED_COMMAND*
ParseCommandPart(
IN OUT REDIRECTION** RedirList)
{
PARSED_COMMAND* Cmd;
PARSED_COMMAND* (*Func)(VOID);
/* Use the scratch buffer */
PTSTR ParsedLine = TempBuf;
/* We need to copy the current token because it's going to be changed below by the ParseToken() calls */
PTCHAR Pos = _stpcpy(ParsedLine, CurrentToken) + 1;
SIZE_T TailOffset = Pos - ParsedLine;
/* Check for special forms */
if ((Func = ParseFor, _tcsicmp(ParsedLine, _T("FOR")) == 0) ||
(Func = ParseIf, _tcsicmp(ParsedLine, _T("IF")) == 0) ||
(Func = ParseRem, _tcsicmp(ParsedLine, _T("REM")) == 0))
{
PTCHAR pHelp;
ParseToken(0, STANDARD_SEPS);
if ((pHelp = _tcsstr(CurrentToken, _T("/?"))) &&
(Func == ParseIf ? (pHelp[2] == _T('/') || pHelp[2] == 0) : TRUE))
{
/* /? was found within the first token */
ParseToken(0, STANDARD_SEPS);
}
else
{
pHelp = NULL;
}
if (pHelp && (CurrentTokenType == TOK_NORMAL))
{
/* We encountered /? first, but is followed
* by another token: that's an error. */
ParseError();
return NULL;
}
/* Do actual parsing only if no help is present */
if (!pHelp)
{
/* FOR and IF commands cannot have leading redirection, but REM can */
if (*RedirList && ((Func == ParseFor) || (Func == ParseIf)))
{
/* Display the culprit command and fail */
ParseErrorEx(ParsedLine);
return NULL;
}
return Func();
}
/* Otherwise, run FOR,IF,REM as regular commands only for help support */
if (Pos + _tcslen(_T("/?")) >= &ParsedLine[CMDLINE_LENGTH])
{
ParseError();
return NULL;
}
Pos = _stpcpy(Pos, _T("/?"));
}
else
{
ParseToken(0, NULL);
}
/* Now get the tail */
while (CurrentTokenType != TOK_END)
{
if (CurrentTokenType == TOK_NORMAL)
{
if (Pos + _tcslen(CurrentToken) >= &ParsedLine[CMDLINE_LENGTH])
{
ParseError();
return NULL;
}
Pos = _stpcpy(Pos, CurrentToken);
}
#ifndef MSCMD_REDIR_PARSE_BUGS
else if (CurrentTokenType == TOK_REDIRECTION)
{
/* Process any trailing redirections and append them to the list */
while (CurrentTokenType == TOK_REDIRECTION)
{
if (!ParseRedirection(RedirList))
return NULL;
ParseToken(0, STANDARD_SEPS);
}
if (CurrentTokenType == TOK_END)
break;
/* Unparse the current token, and reparse it below with no separators */
UnParseToken();
}
else
{
/* There is no need to do a UnParseToken() / ParseToken() cycle */
break;
}
#else
else
{
/* Process any trailing redirections and append them to the list */
BOOL bSuccess = FALSE;
ASSERT(CurrentTokenType != TOK_END);
while (CurrentTokenType != TOK_END)
{
if (!ParseRedirection(RedirList))
{
/* If an actual error happened in ParseRedirection(), bail out */
if (bParseError)
return NULL;
/* Otherwise it just returned FALSE because the current token
* is not a redirection. Unparse the token and refetch it. */
break;
}
bSuccess = TRUE;
ParseToken(0, STANDARD_SEPS);
}
if (CurrentTokenType == TOK_END)
break;
/* Unparse the current token, and reparse it below with no separators */
UnParseToken();
/* If bSuccess == FALSE, we know that it's still the old fetched token, but
* it has been unparsed, so we need to refetch it before quitting the loop. */
if (!bSuccess)
{
ParseToken(0, NULL);
break;
}
}
#endif
ParseToken(0, NULL);
}
*Pos = _T('\0');
Cmd = AllocCommand(C_COMMAND,
ParsedLine,
ParsedLine + TailOffset);
if (!Cmd)
{
WARN("Cannot allocate memory for Cmd!\n");
ParseError();
return NULL;
}
return Cmd;
}
static PARSED_COMMAND*
ParsePrimary(VOID)
{
PARSED_COMMAND* Cmd = NULL;
REDIRECTION* RedirList = NULL;
/* In this context, '@' is considered as a separate token */
if ((*CurrentToken == _T('@')) && (CurrentTokenType == TOK_OPERATOR))
{
Cmd = AllocCommand(C_QUIET, NULL, NULL);
if (!Cmd)
{
WARN("Cannot allocate memory for Cmd!\n");
ParseError();
return NULL;
}
/* @ acts like a unary operator with low precedence,
* so call the top-level parser */
Cmd->Subcommands = ParseCommandOp(C_OP_LOWEST);
return Cmd;
}
/* Process leading redirections and get the head of the command */
#ifndef MSCMD_REDIR_PARSE_BUGS
while (CurrentTokenType == TOK_REDIRECTION)
{
if (!ParseRedirection(&RedirList))
return NULL;
ParseToken(_T('('), STANDARD_SEPS);
}
#else
{
BOOL bSuccess = FALSE;
while (CurrentTokenType != TOK_END)
{
if (!ParseRedirection(&RedirList))
{
/* If an actual error happened in ParseRedirection(), bail out */
if (bParseError)
return NULL;
/* Otherwise it just returned FALSE because
* the current token is not a redirection. */
break;
}
bSuccess = TRUE;
ParseToken(0, STANDARD_SEPS);
}
if (bSuccess)
{
/* Unparse the current token, and reparse it with support for parenthesis */
if (CurrentTokenType != TOK_END)
UnParseToken();
ParseToken(_T('('), STANDARD_SEPS);
}
}
#endif
if (CurrentTokenType == TOK_NORMAL)
Cmd = ParseCommandPart(&RedirList);
else if (CurrentTokenType == TOK_BEGIN_BLOCK)
Cmd = ParseBlock(&RedirList);
else if (CurrentTokenType == TOK_END_BLOCK && !RedirList)
return NULL;
if (Cmd)
{
/* FOR and IF commands cannot have leading redirection
* (checked by ParseCommandPart(), errors out if so). */
ASSERT(!RedirList || (Cmd->Type != C_FOR && Cmd->Type != C_IF));
/* Save the redirection list in the command */
Cmd->Redirections = RedirList;
/* Return the new command */
return Cmd;
}
ParseError();
FreeRedirection(RedirList);
return NULL;
}
static PARSED_COMMAND*
ParseCommandBinaryOp(
IN COMMAND_TYPE OpType)
{
PARSED_COMMAND* Cmd;
if (OpType == C_OP_LOWEST) // i.e. CP_MULTI
{
/* Ignore any parser-level comments */
if (bIgnoreParserComments && (*CurrentToken == _T(':')))
{
/* Ignore the rest of the line, including line continuations */
while (ParseToken(0, NULL) != TOK_END)
;
#ifdef MSCMD_PARENS_PARSE_BUGS
/*
* Return NULL in case we are NOT inside a parenthesized block,
* otherwise continue. The effects can be observed as follows:
* within a parenthesized block, every second ':'-prefixed command
* is not ignored, while the first of each "pair" is ignored.
* This first command **MUST NOT** be followed by an empty line,
* otherwise a syntax error is raised.
*/
if (InsideBlock == 0)
{
#endif
return NULL;
#ifdef MSCMD_PARENS_PARSE_BUGS
}
/* Get the next token */
ParseToken(0, NULL);
#endif
}
/*
* Ignore single closing parenthesis outside of command blocks,
* thus interpreted as a command. This very specific situation
* can happen e.g. while running in batch mode, when jumping to
* a label present inside a command block.
*
* NOTE: If necessary, this condition can be restricted to only
* when a batch context 'bc' is active.
*
* NOTE 2: For further security, Windows checks that we are NOT
* currently inside a parenthesized block, and also, ignores
* explicitly everything (ParseToken() loop) on the same line
* (including line continuations) after this closing parenthesis.
*
* Why doing so? Consider the following batch:
*
* IF 1==1 (
* :label
* echo A
* ) ^
* ELSE (
* echo B
* exit /b
* )
* GOTO :label
*
* First the IF block is executed. Since the condition is trivially
* true, only the first block "echo A" is executed, then execution
* goes after the IF block, that is, at the GOTO. Here, the GOTO
* jumps within the first IF-block, however, the running context now
* is NOT an IF. So parsing and execution will go through each command,
* starting with 'echo A'. But then one gets the ') ^\n ELSE (' part !!
* If we want to make sense of this without bailing out due to
* parsing error, we should ignore this line, **including** the line
* continuation. Hence we need to loop over all the tokens following
* the closing parenthesis, instead of just returning NULL straight ahead.
* Then execution continues with the other commands, 'echo B' and
* 'exit /b' (here to stop the code loop). Execution would also
* continue (if 'exit' was replaced by something else) and encounter
* the lone closing parenthesis ')', that should again be ignored.
*
* Note that this feature has been introduced in Win2k+.
*/
if (/** bc && **/ (_tcscmp(CurrentToken, _T(")")) == 0) &&
(CurrentTokenType != TOK_END_BLOCK))
{
ASSERT(InsideBlock == 0);
/* Ignore the rest of the line, including line continuations */
while (ParseToken(0, NULL) != TOK_END)
;
return NULL;
}
#ifdef MSCMD_PARENS_PARSE_BUGS
/* Check whether we have an empty line only if we are not inside
* a parenthesized block, and return NULL if so, otherwise do not
* do anything; a syntax error will be raised later. */
if (InsideBlock == 0)
#endif
if (!*CurrentToken || *CurrentToken == _T('\n'))
{
ASSERT(CurrentTokenType == TOK_END);
return NULL;
}
}
if (OpType == C_OP_HIGHEST)
Cmd = ParsePrimary();
else
Cmd = ParseCommandBinaryOp(OpType + 1);
if (Cmd && !_tcscmp(CurrentToken, OpString[OpType - C_OP_LOWEST]))
{
PARSED_COMMAND* Left = Cmd;
PARSED_COMMAND* Right;
Right = ParseCommandOp(OpType);
if (!Right)
{
/*
* The '&' operator is allowed to have an empty RHS.
* In this case, we directly return the LHS only.
* Note that Windows' CMD prefers building a '&'
* command with an empty RHS.
*/
if (!bParseError && (OpType != C_MULTI))
ParseError();
if (bParseError)
{
FreeCommand(Left);
return NULL;
}
#ifndef MSCMD_MULTI_EMPTY_RHS
return Left;
#endif
}
Cmd = AllocCommand(OpType, NULL, NULL);
if (!Cmd)
{
WARN("Cannot allocate memory for Cmd!\n");
ParseError();
FreeCommand(Left);
FreeCommand(Right);
return NULL;
}
Cmd->Subcommands = Left;
Left->Next = Right;
#ifdef MSCMD_MULTI_EMPTY_RHS
if (Right)
#endif
Right->Next = NULL;
}
return Cmd;
}
static __inline PARSED_COMMAND*
ParseCommandOp(
IN COMMAND_TYPE OpType)
{
/* Start parsing: initialize the first token */
/* Parse the prefix "quiet" operator '@' as a separate command.
* Thus, @@foo@bar is parsed as: '@', '@', 'foo@bar'. */
ParseTokenEx(_T('@'), _T('('), STANDARD_SEPS, bHandleContinuations);
return ParseCommandBinaryOp(OpType);
}
PARSED_COMMAND*
ParseCommand(
IN PCTSTR Line)
{
PARSED_COMMAND* Cmd;
if (Line)
{
if (!SubstituteVars(Line, ParseLine, _T('%')))
return NULL;
bLineContinuations = FALSE;
}
else
{
if (!ReadLine(ParseLine, FALSE))
return NULL;
bLineContinuations = TRUE;
}
InitParser();
Cmd = ParseCommandOp(C_OP_LOWEST);
if (Cmd)
{
bIgnoreEcho = FALSE;
if ((CurrentTokenType != TOK_END) &&
(_tcscmp(CurrentToken, _T("\n")) != 0))
{
ParseError();
}
if (bParseError)
{
FreeCommand(Cmd);
return NULL;
}
/* Debugging support */
if (fDumpParse)
DumpCommand(Cmd, 0);
}
else
{
bIgnoreEcho = TRUE;
}
return Cmd;
}
/*
* This function is similar to EchoCommand(), but is used
* for dumping the command tree for debugging purposes.
*/
static VOID
DumpRedir(
IN REDIRECTION* Redirections)
{
REDIRECTION* Redir;
if (Redirections)
#ifndef MSCMD_ECHO_COMMAND_COMPAT
ConOutPuts(_T(" Redir: "));
#else
ConOutPuts(_T("Redir: "));
#endif
for (Redir = Redirections; Redir; Redir = Redir->Next)
{
ConOutPrintf(_T(" %x %s%s"), Redir->Number,
RedirString[Redir->Mode], Redir->Filename);
}
}
VOID
DumpCommand(
IN PARSED_COMMAND* Cmd,
IN ULONG SpacePad)
{
/*
* This macro is like DumpCommand(Cmd, Pad);
* but avoids an extra recursive level.
* Note that it can be used ONLY for terminating commands!
*/
#define DUMP(Command, Pad) \
do { \
Cmd = (Command); \
SpacePad = (Pad); \
goto dump; \
} while (0)
PARSED_COMMAND* Sub;
dump:
if (!Cmd)
return;
/* Space padding */
ConOutPrintf(_T("%*s"), SpacePad, _T(""));
switch (Cmd->Type)
{
case C_COMMAND:
case C_REM:
{
/* Generic command name, and Type */
#ifndef MSCMD_ECHO_COMMAND_COMPAT
ConOutPrintf(_T("Cmd: %s Type: %x"),
Cmd->Command.First, Cmd->Type);
#else
ConOutPrintf(_T("Cmd: %s Type: %x "),
Cmd->Command.First, Cmd->Type);
#endif
/* Arguments */
if (Cmd->Command.Rest && *(Cmd->Command.Rest))
#ifndef MSCMD_ECHO_COMMAND_COMPAT
ConOutPrintf(_T(" Args: `%s'"), Cmd->Command.Rest);
#else
ConOutPrintf(_T("Args: `%s' "), Cmd->Command.Rest);
#endif
/* Redirections */
DumpRedir(Cmd->Redirections);
ConOutChar(_T('\n'));
return;
}
case C_QUIET:
{
#ifndef MSCMD_ECHO_COMMAND_COMPAT
ConOutChar(_T('@'));
#else
ConOutPuts(_T("@ "));
#endif
DumpRedir(Cmd->Redirections); // FIXME: Can we have leading redirections??
ConOutChar(_T('\n'));
/*DumpCommand*/DUMP(Cmd->Subcommands, SpacePad + 2);
return;
}
case C_BLOCK:
{
#ifndef MSCMD_ECHO_COMMAND_COMPAT
ConOutChar(_T('('));
#else
ConOutPuts(_T("( "));
#endif
DumpRedir(Cmd->Redirections);
ConOutChar(_T('\n'));
SpacePad += 2;
for (Sub = Cmd->Subcommands; Sub; Sub = Sub->Next)
{
#if defined(MSCMD_ECHO_COMMAND_COMPAT) && defined(MSCMD_PARSER_BUGS)
/*
* We will emulate Windows' CMD handling of "CRLF" and "&" multi-command
* enumeration within parenthesized command blocks.
*/
if (!Sub->Next)
{
DumpCommand(Sub, SpacePad);
continue;
}
if (Sub->Type != C_MULTI)
{
ConOutPrintf(_T("%*s"), SpacePad, _T(""));
ConOutPuts(_T("CRLF \n"));
DumpCommand(Sub, SpacePad);
continue;
}
/* Now, Sub->Type == C_MULTI */
Cmd = Sub;
ConOutPrintf(_T("%*s"), SpacePad, _T(""));
ConOutPrintf(_T("%s \n"), OpString[Cmd->Type - C_OP_LOWEST]);
// FIXME: Can we have redirections on these operator-type commands?
SpacePad += 2;
Cmd = Cmd->Subcommands;
DumpCommand(Cmd, SpacePad);
ConOutPrintf(_T("%*s"), SpacePad, _T(""));
ConOutPuts(_T("CRLF \n"));
DumpCommand(Cmd->Next, SpacePad);
// NOTE: Next commands will remain indented.
#else
/*
* If this command is followed by another one, first display "CRLF".
* This also emulates the CRLF placement "bug" of Windows' CMD
* for the last two commands.
*/
if (Sub->Next)
{
ConOutPrintf(_T("%*s"), SpacePad, _T(""));
#ifndef MSCMD_ECHO_COMMAND_COMPAT
ConOutPuts(_T("CRLF\n"));
#else
ConOutPuts(_T("CRLF \n"));
#endif
}
DumpCommand(Sub, SpacePad);
#endif // defined(MSCMD_ECHO_COMMAND_COMPAT) && defined(MSCMD_PARSER_BUGS)
}
return;
}
case C_MULTI:
case C_OR:
case C_AND:
case C_PIPE:
{
#ifndef MSCMD_ECHO_COMMAND_COMPAT
ConOutPrintf(_T("%s\n"), OpString[Cmd->Type - C_OP_LOWEST]);
#else
ConOutPrintf(_T("%s \n"), OpString[Cmd->Type - C_OP_LOWEST]);
#endif
// FIXME: Can we have redirections on these operator-type commands?
SpacePad += 2;
Sub = Cmd->Subcommands;
DumpCommand(Sub, SpacePad);
/*DumpCommand*/DUMP(Sub->Next, SpacePad);
return;
}
case C_FOR:
{
ConOutPuts(_T("for"));
/* NOTE: FOR cannot have leading redirections */
if (Cmd->For.Switches & FOR_DIRS) ConOutPuts(_T(" /D"));
if (Cmd->For.Switches & FOR_F) ConOutPuts(_T(" /F"));
if (Cmd->For.Switches & FOR_LOOP) ConOutPuts(_T(" /L"));
if (Cmd->For.Switches & FOR_RECURSIVE) ConOutPuts(_T(" /R"));
if (Cmd->For.Params)
ConOutPrintf(_T(" %s"), Cmd->For.Params);
ConOutPrintf(_T(" %%%c in (%s) do\n"), Cmd->For.Variable, Cmd->For.List);
/*DumpCommand*/DUMP(Cmd->Subcommands, SpacePad + 2);
return;
}
case C_IF:
{
ConOutPuts(_T("if"));
/* NOTE: IF cannot have leading redirections */
if (Cmd->If.Flags & IFFLAG_IGNORECASE)
ConOutPuts(_T(" /I"));
ConOutChar(_T('\n'));
SpacePad += 2;
/*
* Show the IF command condition as a command.
* If it is negated, indent the command more.
*/
if (Cmd->If.Flags & IFFLAG_NEGATE)
{
ConOutPrintf(_T("%*s"), SpacePad, _T(""));
ConOutPuts(_T("not\n"));
SpacePad += 2;
}
ConOutPrintf(_T("%*s"), SpacePad, _T(""));
/*
* Command name:
* - Unary operator: its name is the command name, and its argument is the command argument.
* - Binary operator: its LHS is the command name, its RHS is the command argument.
*
* Type:
* Windows' CMD (Win2k3 / Win7-10) values are as follows:
* CMDEXTVERSION Type: 0x32 / 0x34
* ERRORLEVEL Type: 0x33 / 0x35
* DEFINED Type: 0x34 / 0x36
* EXIST Type: 0x35 / 0x37
* == Type: 0x37 / 0x39 (String Comparison)
*
* For the following command:
* NOT Type: 0x36 / 0x38
* Windows only prints it without any type / redirection.
*
* For the following command:
* EQU, NEQ, etc. Type: 0x38 / 0x3a (Generic Comparison)
* Windows displays it as command of unknown type.
*/
#ifndef MSCMD_ECHO_COMMAND_COMPAT
ConOutPrintf(_T("Cmd: %s Type: %x"),
(Cmd->If.Operator <= IF_MAX_UNARY) ?
IfOperatorString[Cmd->If.Operator] :
Cmd->If.LeftArg,
Cmd->If.Operator);
#else
ConOutPrintf(_T("Cmd: %s Type: %x "),
(Cmd->If.Operator <= IF_MAX_UNARY) ?
IfOperatorString[Cmd->If.Operator] :
Cmd->If.LeftArg,
Cmd->If.Operator);
#endif
/* Arguments */
#ifndef MSCMD_ECHO_COMMAND_COMPAT
ConOutPrintf(_T(" Args: `%s'"), Cmd->If.RightArg);
#else
ConOutPrintf(_T("Args: `%s' "), Cmd->If.RightArg);
#endif
ConOutChar(_T('\n'));
if (Cmd->If.Flags & IFFLAG_NEGATE)
{
SpacePad -= 2;
}
Sub = Cmd->Subcommands;
DumpCommand(Sub, SpacePad);
if (Sub->Next)
{
ConOutPrintf(_T("%*s"), SpacePad - 2, _T(""));
ConOutPuts(_T("else\n"));
DumpCommand(Sub->Next, SpacePad);
}
return;
}
default:
ConOutPrintf(_T("*** Unknown type: %x\n"), Cmd->Type);
break;
}
#undef DUMP
}
/*
* Reconstruct a parse tree into text form; used for echoing
* batch file commands and FOR instances.
*/
VOID
EchoCommand(
IN PARSED_COMMAND* Cmd)
{
PARSED_COMMAND* Sub;
REDIRECTION* Redir;
if (!Cmd)
return;
switch (Cmd->Type)
{
case C_COMMAND:
case C_REM:
{
if (SubstituteForVars(Cmd->Command.First, TempBuf))
ConOutPrintf(_T("%s"), TempBuf);
if (SubstituteForVars(Cmd->Command.Rest, TempBuf))
{
ConOutPrintf(_T("%s"), TempBuf);
#ifdef MSCMD_ECHO_COMMAND_COMPAT
/* NOTE: For Windows compatibility, add a trailing space after printing the command parameter, if present */
if (*TempBuf) ConOutChar(_T(' '));
#endif
}
break;
}
case C_QUIET:
return;
case C_BLOCK:
{
BOOLEAN bIsFirstCmdCRLF;
ConOutChar(_T('('));
Sub = Cmd->Subcommands;
bIsFirstCmdCRLF = (Sub && Sub->Next);
#if defined(MSCMD_ECHO_COMMAND_COMPAT) && defined(MSCMD_PARSER_BUGS)
/*
* We will emulate Windows' CMD handling of "CRLF" and "&" multi-command
* enumeration within parenthesized command blocks.
*/
bIsFirstCmdCRLF = bIsFirstCmdCRLF && (Sub->Type != C_MULTI);
#endif
/*
* Single-command block: display all on one line.
* Multi-command block: display commands on separate lines.
*/
if (bIsFirstCmdCRLF)
ConOutChar(_T('\n'));
for (; Sub; Sub = Sub->Next)
{
EchoCommand(Sub);
if (Sub->Next)
#ifdef MSCMD_ECHO_COMMAND_COMPAT
ConOutPuts(_T(" \n "));
#else
ConOutChar(_T('\n'));
#endif
}
if (bIsFirstCmdCRLF)
ConOutChar(_T('\n'));
#ifdef MSCMD_ECHO_COMMAND_COMPAT
/* NOTE: For Windows compatibility, add a trailing space after printing the closing parenthesis */
ConOutPuts(_T(") "));
#else
ConOutChar(_T(')'));
#endif
break;
}
case C_MULTI:
case C_OR:
case C_AND:
case C_PIPE:
{
Sub = Cmd->Subcommands;
EchoCommand(Sub);
ConOutPrintf(_T(" %s "), OpString[Cmd->Type - C_OP_LOWEST]);
EchoCommand(Sub->Next);
break;
}
case C_FOR:
{
ConOutPuts(_T("for"));
if (Cmd->For.Switches & FOR_DIRS) ConOutPuts(_T(" /D"));
if (Cmd->For.Switches & FOR_F) ConOutPuts(_T(" /F"));
if (Cmd->For.Switches & FOR_LOOP) ConOutPuts(_T(" /L"));
if (Cmd->For.Switches & FOR_RECURSIVE) ConOutPuts(_T(" /R"));
if (Cmd->For.Params)
ConOutPrintf(_T(" %s"), Cmd->For.Params);
if (Cmd->For.List && SubstituteForVars(Cmd->For.List, TempBuf))
ConOutPrintf(_T(" %%%c in (%s) do "), Cmd->For.Variable, TempBuf);
else
ConOutPrintf(_T(" %%%c in (%s) do "), Cmd->For.Variable, Cmd->For.List);
EchoCommand(Cmd->Subcommands);
break;
}
case C_IF:
{
ConOutPuts(_T("if"));
if (Cmd->If.Flags & IFFLAG_IGNORECASE)
ConOutPuts(_T(" /I"));
if (Cmd->If.Flags & IFFLAG_NEGATE)
ConOutPuts(_T(" not"));
if (Cmd->If.LeftArg && SubstituteForVars(Cmd->If.LeftArg, TempBuf))
ConOutPrintf(_T(" %s"), TempBuf);
ConOutPrintf(_T(" %s"), IfOperatorString[Cmd->If.Operator]);
if (SubstituteForVars(Cmd->If.RightArg, TempBuf))
ConOutPrintf(_T(" %s "), TempBuf);
Sub = Cmd->Subcommands;
EchoCommand(Sub);
if (Sub->Next)
{
ConOutPuts(_T(" else "));
EchoCommand(Sub->Next);
}
break;
}
default:
ASSERT(FALSE);
break;
}
for (Redir = Cmd->Redirections; Redir; Redir = Redir->Next)
{
if (SubstituteForVars(Redir->Filename, TempBuf))
{
#ifdef MSCMD_ECHO_COMMAND_COMPAT
ConOutPrintf(_T("%c%s%s "),
_T('0') + Redir->Number,
RedirString[Redir->Mode], TempBuf);
#else
ConOutPrintf(_T(" %c%s%s"),
_T('0') + Redir->Number,
RedirString[Redir->Mode], TempBuf);
#endif
}
}
}
/*
* "Unparse" a command into a text form suitable for passing to CMD /C.
* Used for pipes. This is basically the same thing as EchoCommand(),
* but writing into a string instead of to standard output.
*/
PTCHAR
UnparseCommand(
IN PARSED_COMMAND* Cmd,
OUT PTCHAR Out,
IN PTCHAR OutEnd)
{
/*
* Since this function has the annoying requirement that it must avoid
* overflowing the supplied buffer, define some helper macros to make
* this less painful.
*/
#define CHAR(Char) \
do { \
if (Out == OutEnd) return NULL; \
*Out++ = Char; \
} while (0)
#define STRING(String) \
do { \
if (Out + _tcslen(String) > OutEnd) return NULL; \
Out = _stpcpy(Out, String); \
} while (0)
#define PRINTF(Format, ...) \
do { \
UINT Len = _sntprintf(Out, OutEnd - Out, Format, __VA_ARGS__); \
if (Len > (UINT)(OutEnd - Out)) return NULL; \
Out += Len; \
} while (0)
#define RECURSE(Subcommand) \
do { \
Out = UnparseCommand(Subcommand, Out, OutEnd); \
if (!Out) return NULL; \
} while (0)
PARSED_COMMAND* Sub;
REDIRECTION* Redir;
if (!Cmd)
return Out;
switch (Cmd->Type)
{
case C_COMMAND:
case C_REM:
{
/* This is fragile since there could be special characters, but
* Windows doesn't bother escaping them, so for compatibility
* we probably shouldn't do it either */
if (!SubstituteForVars(Cmd->Command.First, TempBuf)) return NULL;
STRING(TempBuf);
if (!SubstituteForVars(Cmd->Command.Rest, TempBuf)) return NULL;
STRING(TempBuf);
break;
}
case C_QUIET:
{
CHAR(_T('@'));
RECURSE(Cmd->Subcommands);
break;
}
case C_BLOCK:
{
CHAR(_T('('));
for (Sub = Cmd->Subcommands; Sub; Sub = Sub->Next)
{
RECURSE(Sub);
if (Sub->Next)
CHAR(_T('&'));
}
CHAR(_T(')'));
break;
}
case C_MULTI:
case C_OR:
case C_AND:
case C_PIPE:
{
Sub = Cmd->Subcommands;
RECURSE(Sub);
PRINTF(_T(" %s "), OpString[Cmd->Type - C_OP_LOWEST]);
RECURSE(Sub->Next);
break;
}
case C_FOR:
{
STRING(_T("for"));
if (Cmd->For.Switches & FOR_DIRS) STRING(_T(" /D"));
if (Cmd->For.Switches & FOR_F) STRING(_T(" /F"));
if (Cmd->For.Switches & FOR_LOOP) STRING(_T(" /L"));
if (Cmd->For.Switches & FOR_RECURSIVE) STRING(_T(" /R"));
if (Cmd->For.Params)
PRINTF(_T(" %s"), Cmd->For.Params);
if (Cmd->For.List && SubstituteForVars(Cmd->For.List, TempBuf))
PRINTF(_T(" %%%c in (%s) do "), Cmd->For.Variable, TempBuf);
else
PRINTF(_T(" %%%c in (%s) do "), Cmd->For.Variable, Cmd->For.List);
RECURSE(Cmd->Subcommands);
break;
}
case C_IF:
{
STRING(_T("if"));
if (Cmd->If.Flags & IFFLAG_IGNORECASE)
STRING(_T(" /I"));
if (Cmd->If.Flags & IFFLAG_NEGATE)
STRING(_T(" not"));
if (Cmd->If.LeftArg && SubstituteForVars(Cmd->If.LeftArg, TempBuf))
PRINTF(_T(" %s"), TempBuf);
PRINTF(_T(" %s"), IfOperatorString[Cmd->If.Operator]);
if (!SubstituteForVars(Cmd->If.RightArg, TempBuf)) return NULL;
PRINTF(_T(" %s "), TempBuf);
Sub = Cmd->Subcommands;
RECURSE(Sub);
if (Sub->Next)
{
STRING(_T(" else "));
RECURSE(Sub->Next);
}
break;
}
default:
ASSERT(FALSE);
break;
}
for (Redir = Cmd->Redirections; Redir; Redir = Redir->Next)
{
if (!SubstituteForVars(Redir->Filename, TempBuf))
return NULL;
PRINTF(_T(" %c%s%s"), _T('0') + Redir->Number,
RedirString[Redir->Mode], TempBuf);
}
return Out;
#undef CHAR
#undef STRING
#undef PRINTF
#undef RECURSE
}