diff --git a/base/shell/cmd/batch.c b/base/shell/cmd/batch.c index 1876b84deab..714480cb6c7 100644 --- a/base/shell/cmd/batch.c +++ b/base/shell/cmd/batch.c @@ -399,9 +399,28 @@ INT Batch(LPTSTR fullname, LPTSTR firstword, LPTSTR param, PARSED_COMMAND *Cmd) #endif } - /* Check if this is a "CALL :label" */ - if (*firstword == _T(':')) - ret = cmd_goto(firstword); + /* If this is a "CALL :label args ...", call a subroutine of + * the current batch file, only if extensions are enabled. */ + if (bEnableExtensions && (*firstword == _T(':'))) + { + LPTSTR expLabel; + + /* Position at the place of the parent file (which is the same as the caller) */ + bc->mempos = (bc->prev ? bc->prev->mempos : 0); + + /* + * Jump to the label. Strip the label's colon; as a side-effect + * this will forbid "CALL :EOF"; however "CALL ::EOF" will work! + */ + bc->current = Cmd; + ++firstword; + + /* Expand the label only! (simulate a GOTO command as in Windows' CMD) */ + expLabel = DoDelayedExpansion(firstword); + ret = cmd_goto(expLabel ? expLabel : firstword); + if (expLabel) + cmd_free(expLabel); + } /* If we have created a new context, don't return * until this batch file has completed. */ @@ -548,8 +567,16 @@ LPTSTR ReadBatchLine(VOID) TRACE("ReadBatchLine(): textline: \'%s\'\n", debugstr_aw(textline)); +#if 1 + // + // FIXME: This is redundant, but keep it for the moment until we correctly + // hande the end-of-file situation here, in ReadLine() and in the parser. + // (In an EOF, the previous BatchGetString() call will return FALSE but + // we want not to run the ExitBatch() at first, but wait later to do it.) + // if (textline[_tcslen(textline) - 1] != _T('\n')) _tcscat(textline, _T("\n")); +#endif return textline; } diff --git a/base/shell/cmd/call.c b/base/shell/cmd/call.c index 55f39d086f1..c9b11937492 100644 --- a/base/shell/cmd/call.c +++ b/base/shell/cmd/call.c @@ -30,58 +30,189 @@ #include "precomp.h" +/* Enable this define for "buggy" Windows' CMD CALL-command compatibility */ +#define MSCMD_CALL_QUIRKS + + /* * Perform CALL command. */ - INT cmd_call(LPTSTR param) { - TCHAR line[CMDLINE_LENGTH + 1]; - TCHAR *first; - BOOL bInQuote = FALSE; + PARSED_COMMAND* Cmd = NULL; + BOOL bOldIgnoreParserComments; +#ifndef MSCMD_CALL_QUIRKS + BOOL bOldHandleContinuations; +#else + SIZE_T nNumCarets; +#endif + PTSTR first; - TRACE ("cmd_call: (\'%s\')\n", debugstr_aw(param)); - if (!_tcsncmp (param, _T("/?"), 2)) + TRACE("cmd_call(\'%s\')\n", debugstr_aw(param)); + + if (!_tcsncmp(param, _T("/?"), 2)) { - ConOutResPaging(TRUE,STRING_CALL_HELP); + ConOutResPaging(TRUE, STRING_CALL_HELP); return 0; } - /* Do a second round of %-variable substitutions */ - if (!SubstituteVars(param, line, _T('%'))) + /* Fail if no command or label has been provided */ + if (*param == _T('\0')) return (nErrorLevel = 1); - /* Find start and end of first word */ - first = line; - while (_istspace(*first)) - first++; + /* Ignore parser comments (starting with a colon) */ + bOldIgnoreParserComments = bIgnoreParserComments; + bIgnoreParserComments = FALSE; - for (param = first; *param; param++) +#ifndef MSCMD_CALL_QUIRKS + /* Disable parsing of escape carets */ + bOldHandleContinuations = bHandleContinuations; + bHandleContinuations = FALSE; + first = param; +#else + /* + * As the original escape carets have been dealt with during the first + * command parsing step, the remaining ones need to be doubled so that + * they can again survive the new parsing step done below. + * But do it the Windows' CMD "buggy" way: **all** carets are doubled, + * even those inside quotes. However, this causes carets inside quotes + * to remain doubled after the parsing step... + */ + + /* Count all the carets */ + nNumCarets = 0; + first = param; + while (first) { - if (!bInQuote && (_istspace(*param) || _tcschr(_T(",;="), *param))) - break; - bInQuote ^= (*param == _T('"')); + first = _tcschr(first, _T('^')); + if (first) + { + ++nNumCarets; + ++first; + } } - /* Separate first word from rest of line */ - memmove(param + 1, param, (_tcslen(param) + 1) * sizeof(TCHAR)); - *param++ = _T('\0'); + /* Re-allocate a large enough parameter string if needed */ + if (nNumCarets > 0) + { + PTCHAR Src, Dest, End; - if (*first == _T(':') && bc) + // TODO: Improvement: Use the scratch TempBuf if the string is not too long. + first = cmd_alloc((_tcslen(param) + nNumCarets + 1) * sizeof(TCHAR)); + if (!first) + { + WARN("Cannot allocate memory for new CALL parameter string!\n"); + error_out_of_memory(); + return (nErrorLevel = 1); + } + + /* Copy the parameter string and double the escape carets */ + Src = param; + Dest = first; + while (*Src) + { + if (*Src != _T('^')) + { + /* Copy everything before the next caret (or the end of the string) */ + End = _tcschr(Src, _T('^')); + if (!End) + End = Src + _tcslen(Src); + memcpy(Dest, Src, (End - Src) * sizeof(TCHAR)); + Dest += End - Src; + Src = End; + continue; + } + + /* Copy the original caret and double it */ + *Dest++ = *Src; + *Dest++ = *Src++; + } + *Dest = _T('\0'); + } + else + { + first = param; + } +#endif + + /* + * Reparse the CALL parameter string as a command. + * Note that this will trigger a second round of %-variable substitutions. + */ + Cmd = ParseCommand(first); + + /* Restore the global parsing state */ +#ifndef MSCMD_CALL_QUIRKS + bHandleContinuations = bOldHandleContinuations; +#endif + bIgnoreParserComments = bOldIgnoreParserComments; + + /* + * If no command is there, yet no error occurred, this means that + * a whitespace label was given. Do not consider this as a failure. + */ + if (!Cmd && !bParseError) + { +#ifdef MSCMD_CALL_QUIRKS + if (first != param) + cmd_free(first); +#endif + return (nErrorLevel = 0); + } + + /* Reset bParseError so as to continue running afterwards */ + bParseError = FALSE; + + /* + * Otherwise, if no command is there because a parse error occurred, + * or if this an unsupported command: not a standard one, including + * FOR and IF, fail and bail out. + */ + if (!Cmd || (Cmd->Type == C_FOR) || (Cmd->Type == C_IF) || + ((Cmd->Type != C_COMMAND) && (Cmd->Type != C_REM))) + { + // FIXME: Localize + ConErrPrintf(_T("%s was unexpected.\n"), first); + +#ifdef MSCMD_CALL_QUIRKS + if (first != param) + cmd_free(first); +#endif + if (Cmd) FreeCommand(Cmd); + return (nErrorLevel = 1); + } + +#ifdef MSCMD_CALL_QUIRKS + if (first != param) + cmd_free(first); +#endif + + first = Cmd->Command.First; + param = Cmd->Command.Rest; + + /* "CALL :label args ..." - Call a subroutine of the current batch file, only if extensions are enabled */ + if (bEnableExtensions && (*first == _T(':'))) { INT ret; - /* CALL :label - call a subroutine of the current batch file */ - while (*param == _T(' ')) - param++; + /* A batch context must be present */ + if (!bc) + { + // FIXME: Localize + ConErrPuts(_T("Invalid attempt to call batch label outside of batch script.\n")); + FreeCommand(Cmd); + return (nErrorLevel = 1); + } ret = Batch(bc->BatchFilePath, first, param, NULL); nErrorLevel = (ret != 0 ? ret : nErrorLevel); - - return nErrorLevel; + } + else + { + nErrorLevel = DoCommand(first, param, NULL); } - nErrorLevel = DoCommand(first, param, NULL); + FreeCommand(Cmd); return nErrorLevel; } diff --git a/base/shell/cmd/cmd.h b/base/shell/cmd/cmd.h index bb2221fb23d..0812596719b 100644 --- a/base/shell/cmd/cmd.h +++ b/base/shell/cmd/cmd.h @@ -424,6 +424,9 @@ VOID ParseErrorEx(IN PCTSTR s); extern BOOL bParseError; extern TCHAR ParseLine[CMDLINE_LENGTH]; +extern BOOL bIgnoreParserComments; +extern BOOL bHandleContinuations; + /* Prototypes from PATH.C */ INT cmd_path (LPTSTR); diff --git a/base/shell/cmd/goto.c b/base/shell/cmd/goto.c index 5174b62417f..777963344a2 100644 --- a/base/shell/cmd/goto.c +++ b/base/shell/cmd/goto.c @@ -41,7 +41,24 @@ INT cmd_goto(LPTSTR param) TRACE("cmd_goto(\'%s\')\n", debugstr_aw(param)); - if (!_tcsncmp(param, _T("/?"), 2)) + /* + * Keep the help message handling here too. + * This allows us to reproduce the Windows' CMD "bug" + * (from a batch script): + * + * SET label=/? + * CALL :%%label%% + * + * calls GOTO help, due to how CALL :label functionality + * is internally implemented. + * + * See https://stackoverflow.com/q/31987023/13530036 + * for more details. + * + * Note that the choice of help parsing forbids + * any label containing '/?' in it. + */ + if (_tcsstr(param, _T("/?"))) { ConOutResPaging(TRUE, STRING_GOTO_HELP1); return 0; diff --git a/base/shell/cmd/parser.c b/base/shell/cmd/parser.c index 1680c5be4d8..29797bf36ae 100644 --- a/base/shell/cmd/parser.c +++ b/base/shell/cmd/parser.c @@ -93,6 +93,9 @@ static BOOL bLineContinuations; 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 @@ -206,6 +209,9 @@ restart: if (!ReadLine(ParseLine, TRUE)) { /* ^C pressed, or line was too long */ + // + // FIXME: Distinguish with respect to BATCH end of file !! + // bParseError = TRUE; } else @@ -445,7 +451,7 @@ ParseToken( IN TCHAR ExtraEnd OPTIONAL, IN PCTSTR Separators OPTIONAL) { - return ParseTokenEx(0, ExtraEnd, Separators, TRUE); + return ParseTokenEx(0, ExtraEnd, Separators, bHandleContinuations); } @@ -1294,7 +1300,7 @@ ParseCommandBinaryOp( if (OpType == C_OP_LOWEST) // i.e. CP_MULTI { /* Ignore any parser-level comments */ - if (*CurrentToken == _T(':')) + if (bIgnoreParserComments && (*CurrentToken == _T(':'))) { /* Ignore the rest of the line, including line continuations */ while (ParseToken(0, NULL) != TOK_END) @@ -1445,7 +1451,7 @@ ParseCommandOp( /* Parse the prefix "quiet" operator '@' as a separate command. * Thus, @@foo@bar is parsed as: '@', '@', 'foo@bar'. */ - ParseTokenEx(_T('@'), _T('('), STANDARD_SEPS, TRUE); + ParseTokenEx(_T('@'), _T('('), STANDARD_SEPS, bHandleContinuations); return ParseCommandBinaryOp(OpType); }