[CMD] Make the command-line parser more compatible with Windows' CMD one.

All these modifications have been verified with Windows' CMD, either
by using written cmd_rostests and the existing cmd_winetests, or
manually by enabling the flags cmd!fDumpTokens and cmd!fDumpParse
(available in the public symbols) and analyzing how the tokens are
being parsed, as well as the generated command tree.

See also the following links for more details (but remember that these
observations have to be double-checked in Windows' CMD!):

* Parser rules: https://stackoverflow.com/a/4095133/13530036
* Discussion: https://www.dostips.com/forum/viewtopic.php?f=3&t=8355
* Numbers parsing: https://www.dostips.com/forum/viewtopic.php?t=3758
* Label names vs. GOTO and CALL: https://www.dostips.com/forum/viewtopic.php?f=3&t=3803
  and: https://www.dostips.com/forum/viewtopic.php?f=3&t=3803&p=55405#p55405

- Fix REM command parsing. A C_COMMAND-like structure should still
  be built, so that it can show up during batch command echo. However
  some specific handling needs to be done, so use instead a new C_REM
  command type.
  Escape carets are parsed differently than usual: they are explicitly
  kept in the command line and don't participate in line continuations.
  Also, the Windows' CMD behaviour is to discards everything before the
  last line continuation.

- Prefix operator '@' (the "silent" operator) is parsed as a separate
  command. Thus, the command @@foo@bar is parsed as: '@', '@', 'foo@bar'.

- Improve the checks for numbered redirection.
  For this purpose, we check whether this is a number, that is in first
  position in the current parsing buffer or is preceded by a whitespace-
  like separator, including standard command operators (excepting '@' !)
  and double-quotes.

- Empty command blocks, i.e. "( )", standing by themselves, or present
  in IF or FOR commands, are considered invalid. (The closing parenthesis
  is considered "unexpected".)

- Ignore single closing parenthesis when being outside of command blocks,
  thus interpreting it as a command, and ignore explicitly everything
  following on the same line, including line continuations.
  This very specific situation can happen e.g. while running in batch mode,
  when jumping to a label present inside a command block.
  See the code for a thorough explanation.

- Detect whether a parenthesized block is not terminated at the end
  of a command stream (getting a NUL character instead of a newline),
  and if so, bail out early instead of entering into an infinite loop.

- Perform a similar check for the parenthesized list in FOR commands.

- Initialize the static 'InsideBlock' value to a known value.

- The '&' operator (multi-commmand) is allowed to have an empty RHS.
  When such situation occurs, turn the CurrentTokenType to TOK_END
  so as to avoid a parse error later on.

- The main body of a IF statement, or its 'else' clause, as well as
  the main body of a FOR statement, must not be empty, otherwise this
  is considered a syntax error. If so, call ParseError() that sets
  the 'bParseError' flag, and forcing all batch execution to stop.
This commit is contained in:
Hermès Bélusca-Maïto 2020-05-23 00:35:54 +02:00
parent 50ff453434
commit d029a626e9
No known key found for this signature in database
GPG key ID: 3B2539C65E7B93D0
4 changed files with 1014 additions and 310 deletions

View file

@ -664,7 +664,7 @@ ExecuteAsync(PARSED_COMMAND *Cmd)
/* Build the parameter string to pass to cmd.exe */
ParamsEnd = _stpcpy(CmdParams, _T("/S/D/C\""));
ParamsEnd = Unparse(Cmd, ParamsEnd, &CmdParams[CMDLINE_LENGTH - 2]);
ParamsEnd = UnparseCommand(Cmd, ParamsEnd, &CmdParams[CMDLINE_LENGTH - 2]);
if (!ParamsEnd)
{
error_out_of_memory();
@ -785,12 +785,13 @@ ExecuteCommand(
LPTSTR First, Rest;
INT Ret = 0;
/* If we don't have any command, or if this is REM, ignore it */
if (!Cmd || (Cmd->Type == C_REM))
return 0;
/*
* Do not execute any command if we are about to exit CMD, or about to
* change batch execution context, e.g. in case of a CALL / GOTO / EXIT.
*/
if (!Cmd)
return 0;
if (bExit || SeenGoto())
return 0;
@ -812,6 +813,8 @@ ExecuteCommand(
}
cmd_free(First);
}
/* Fall through */
case C_REM:
break;
case C_QUIET:
@ -842,13 +845,13 @@ ExecuteCommand(
Ret = ExecutePipeline(Cmd);
break;
case C_IF:
Ret = ExecuteIf(Cmd);
break;
case C_FOR:
Ret = ExecuteFor(Cmd);
break;
case C_IF:
Ret = ExecuteIf(Cmd);
break;
}
UndoRedirection(Cmd->Redirections, NULL);

View file

@ -259,7 +259,8 @@ INT CommandHistory(LPTSTR param);
/* Prototypes for IF.C */
#define IFFLAG_NEGATE 1 /* NOT */
#define IFFLAG_IGNORECASE 2 /* /I - Extended */
enum {
typedef enum _IF_OPERATOR
{
/** Unary operators **/
/* Standard */
IF_ERRORLEVEL, IF_EXIST,
@ -271,7 +272,8 @@ enum {
IF_STRINGEQ, /* == */
/* Extended */
IF_EQU, IF_NEQ, IF_LSS, IF_LEQ, IF_GTR, IF_GEQ
};
} IF_OPERATOR;
INT ExecuteIf(struct _PARSED_COMMAND *Cmd);
/* Prototypes for INTERNAL.C */
@ -345,42 +347,78 @@ INT CommandMsgbox (LPTSTR);
/* These three characters act like spaces to the parser in most contexts */
#define STANDARD_SEPS _T(",;=")
enum { C_COMMAND, C_QUIET, C_BLOCK, C_MULTI, C_OR, C_AND, C_PIPE, C_IF, C_FOR };
typedef enum _COMMAND_TYPE
{
/* Standard command */
C_COMMAND,
/* Quiet operator */
C_QUIET,
/* Parenthesized block */
C_BLOCK,
/* Operators */
C_MULTI, C_OR, C_AND, C_PIPE,
/* Special parsed commands */
C_FOR, C_IF, C_REM
} COMMAND_TYPE;
typedef struct _PARSED_COMMAND
{
/*
* For IF : this is the 'main' case (the 'else' is obtained via SubCmd->Next).
* For FOR: this is the list of all the subcommands in the DO.
*/
struct _PARSED_COMMAND *Subcommands;
struct _PARSED_COMMAND *Next;
struct _PARSED_COMMAND *Next; // Next command(s) in the chain.
struct _REDIRECTION *Redirections;
BYTE Type;
COMMAND_TYPE Type;
union
{
struct
{
TCHAR *Rest;
PTSTR Rest;
TCHAR First[];
} Command;
struct
{
BYTE Flags;
BYTE Operator;
TCHAR *LeftArg;
TCHAR *RightArg;
} If;
struct
{
BYTE Switches;
TCHAR Variable;
LPTSTR Params;
LPTSTR List;
PTSTR Params;
PTSTR List;
struct _FOR_CONTEXT *Context;
} For;
struct
{
BYTE Flags;
IF_OPERATOR Operator;
PTSTR LeftArg;
PTSTR RightArg;
} If;
};
} PARSED_COMMAND;
PARSED_COMMAND *ParseCommand(LPTSTR Line);
VOID EchoCommand(PARSED_COMMAND *Cmd);
TCHAR *Unparse(PARSED_COMMAND *Cmd, TCHAR *Out, TCHAR *OutEnd);
VOID FreeCommand(PARSED_COMMAND *Cmd);
PARSED_COMMAND*
ParseCommand(
IN PCTSTR Line);
VOID
DumpCommand(
IN PARSED_COMMAND* Cmd,
IN ULONG SpacePad);
VOID
EchoCommand(
IN PARSED_COMMAND* Cmd);
PTCHAR
UnparseCommand(
IN PARSED_COMMAND* Cmd,
OUT PTCHAR Out,
IN PTCHAR OutEnd);
VOID
FreeCommand(
IN OUT PARSED_COMMAND* Cmd);
VOID ParseErrorEx(IN PCTSTR s);
extern BOOL bParseError;

View file

@ -694,7 +694,7 @@ INT CommandExit(LPTSTR param)
*/
INT CommandRem (LPTSTR param)
{
if (!_tcsncmp (param, _T("/?"), 2))
if (_tcsstr(param, _T("/?")) == param)
{
ConOutResPaging(TRUE,STRING_REM_HELP);
}

File diff suppressed because it is too large Load diff