reactos/base/shell/cmd/batch.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

585 lines
15 KiB
C

/*
* BATCH.C - batch file processor for CMD.EXE.
*
*
* History:
*
* ??/??/?? (Evan Jeffrey)
* started.
*
* 15 Jul 1995 (Tim Norman)
* modes and bugfixes.
*
* 08 Aug 1995 (Matt Rains)
* i have cleaned up the source code. changes now bring this
* source into guidelines for recommended programming practice.
*
* i have added some constants to help making changes easier.
*
* 29 Jan 1996 (Steffan Kaiser)
* made a few cosmetic changes
*
* 05 Feb 1996 (Tim Norman)
* changed to comply with new first/rest calling scheme
*
* 14 Jun 1997 (Steffen Kaiser)
* bug fixes. added error level expansion %?. ctrl-break handling
*
* 16 Jul 1998 (Hans B Pufal)
* Totally reorganised in conjunction with COMMAND.C (cf) to
* implement proper BATCH file nesting and other improvements.
*
* 16 Jul 1998 (John P Price <linux-guru@gcfl.net>)
* Separated commands into individual files.
*
* 19 Jul 1998 (Hans B Pufal) [HBP_001]
* Preserve state of echo flag across batch calls.
*
* 19 Jul 1998 (Hans B Pufal) [HBP_002]
* Implementation of FOR command
*
* 20-Jul-1998 (John P Price <linux-guru@gcfl.net>)
* added error checking after cmd_alloc calls
*
* 27-Jul-1998 (John P Price <linux-guru@gcfl.net>)
* added config.h include
*
* 02-Aug-1998 (Hans B Pufal) [HBP_003]
* Fixed bug in ECHO flag restoration at exit from batch file
*
* 26-Jan-1999 Eric Kohl
* Replaced CRT io functions by Win32 io functions.
* Unicode safe!
*
* 23-Feb-2001 (Carl Nettelblad <cnettel@hem.passagen.es>)
* Fixes made to get "for" working.
*
* 02-Apr-2005 (Magnus Olsen <magnus@greatlord.com>)
* Remove all hardcoded strings in En.rc
*/
#include "precomp.h"
/* The stack of current batch contexts.
* NULL when no batch is active.
*/
BATCH_TYPE BatType = NONE;
PBATCH_CONTEXT bc = NULL;
#ifdef MSCMD_BATCH_ECHO
BOOL bBcEcho = TRUE;
#endif
BOOL bEcho = TRUE; /* The echo flag */
/* Buffer for reading Batch file lines */
TCHAR textline[BATCH_BUFFSIZE];
/*
* Returns a pointer to the n'th parameter of the current batch file.
* If no such parameter exists returns pointer to empty string.
* If no batch file is current, returns NULL.
*/
BOOL
FindArg(
IN TCHAR Char,
OUT PCTSTR* ArgPtr,
OUT BOOL* IsParam0)
{
PCTSTR pp;
INT n = Char - _T('0');
TRACE("FindArg: (%d)\n", n);
*ArgPtr = NULL;
if (n < 0 || n > 9)
return FALSE;
n = bc->shiftlevel[n];
*IsParam0 = (n == 0);
pp = bc->params;
/* Step up the strings till we reach
* the end or the one we want. */
while (*pp && n--)
pp += _tcslen(pp) + 1;
*ArgPtr = pp;
return TRUE;
}
/*
* Batch_params builds a parameter list in newly allocated memory.
* The parameters consist of null terminated strings with a final
* NULL character signalling the end of the parameters.
*
*/
static LPTSTR BatchParams(LPTSTR s1, LPTSTR s2)
{
LPTSTR dp = (LPTSTR)cmd_alloc((_tcslen(s1) + _tcslen(s2) + 3) * sizeof (TCHAR));
/* JPP 20-Jul-1998 added error checking */
if (dp == NULL)
{
WARN("Cannot allocate memory for dp!\n");
error_out_of_memory();
return NULL;
}
if (s1 && *s1)
{
s1 = _stpcpy (dp, s1);
*s1++ = _T('\0');
}
else
s1 = dp;
while (*s2)
{
BOOL inquotes = FALSE;
/* Find next parameter */
while (_istspace(*s2) || (*s2 && _tcschr(STANDARD_SEPS, *s2)))
s2++;
if (!*s2)
break;
/* Copy it */
do
{
if (!inquotes && (_istspace(*s2) || _tcschr(STANDARD_SEPS, *s2)))
break;
inquotes ^= (*s2 == _T('"'));
*s1++ = *s2++;
} while (*s2);
*s1++ = _T('\0');
}
*s1 = _T('\0');
return dp;
}
/*
* Free the allocated memory of a batch file.
*/
static VOID ClearBatch(VOID)
{
TRACE("ClearBatch mem = %08x ; free = %d\n", bc->mem, bc->memfree);
if (bc->mem && bc->memfree)
cmd_free(bc->mem);
if (bc->raw_params)
cmd_free(bc->raw_params);
if (bc->params)
cmd_free(bc->params);
}
/*
* If a batch file is current, exits it, freeing the context block and
* chaining back to the previous one.
*
* If no new batch context is found, sets ECHO back ON.
*
* If the parameter is non-null or not empty, it is printed as an exit
* message
*/
VOID ExitBatch(VOID)
{
ClearBatch();
TRACE("ExitBatch\n");
UndoRedirection(bc->RedirList, NULL);
FreeRedirection(bc->RedirList);
#ifndef MSCMD_BATCH_ECHO
/* Preserve echo state across batch calls */
bEcho = bc->bEcho;
#endif
while (bc->setlocal)
cmd_endlocal(_T(""));
bc = bc->prev;
#if 0
/* Do not process any more parts of a compound command */
bc->current = NULL;
#endif
/* If there is no more batch contexts, notify the signal handler */
if (!bc)
{
CheckCtrlBreak(BREAK_OUTOFBATCH);
BatType = NONE;
#ifdef MSCMD_BATCH_ECHO
bEcho = bBcEcho;
#endif
}
}
/*
* Exit all the nested batch calls.
*/
VOID ExitAllBatches(VOID)
{
while (bc)
ExitBatch();
}
/*
* Load batch file into memory.
*/
static void BatchFile2Mem(HANDLE hBatchFile)
{
TRACE("BatchFile2Mem()\n");
bc->memsize = GetFileSize(hBatchFile, NULL);
bc->mem = (char *)cmd_alloc(bc->memsize+1); /* 1 extra for '\0' */
/* if memory is available, read it in and close the file */
if (bc->mem != NULL)
{
TRACE ("BatchFile2Mem memory %08x - %08x\n",bc->mem,bc->memsize);
SetFilePointer (hBatchFile, 0, NULL, FILE_BEGIN);
ReadFile(hBatchFile, (LPVOID)bc->mem, bc->memsize, &bc->memsize, NULL);
bc->mem[bc->memsize]='\0'; /* end this, so you can dump it as a string */
bc->memfree=TRUE; /* this one needs to be freed */
}
else
{
bc->memsize=0; /* this will prevent mem being accessed */
bc->memfree=FALSE;
}
bc->mempos = 0; /* set position to the start */
}
/*
* Start batch file execution.
*
* The firstword parameter is the full filename of the batch file.
*/
INT Batch(LPTSTR fullname, LPTSTR firstword, LPTSTR param, PARSED_COMMAND *Cmd)
{
INT ret = 0;
INT i;
HANDLE hFile = NULL;
BOOLEAN bSameFn = FALSE;
BOOLEAN bTopLevel;
BATCH_CONTEXT new;
PFOR_CONTEXT saved_fc;
SetLastError(0);
if (bc && bc->mem)
{
TCHAR fpname[MAX_PATH];
GetFullPathName(fullname, ARRAYSIZE(fpname), fpname, NULL);
if (_tcsicmp(bc->BatchFilePath, fpname) == 0)
bSameFn = TRUE;
}
TRACE("Batch(\'%s\', \'%s\', \'%s\') bSameFn = %d\n",
debugstr_aw(fullname), debugstr_aw(firstword), debugstr_aw(param), bSameFn);
if (!bSameFn)
{
hFile = CreateFile(fullname, GENERIC_READ, FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL |
FILE_FLAG_SEQUENTIAL_SCAN, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
ConErrResPuts(STRING_BATCH_ERROR);
return 1;
}
}
/*
* Remember whether this is a top-level batch context, i.e. if there is
* no batch context existing prior (bc == NULL originally), and we are
* going to create one below.
*/
bTopLevel = !bc;
if (bc != NULL && Cmd == bc->current)
{
/* Then we are transferring to another batch */
ClearBatch();
AddBatchRedirection(&Cmd->Redirections);
}
else
{
struct _SETLOCAL *setlocal = NULL;
if (Cmd == NULL)
{
/* This is a CALL. CALL will set errorlevel to our return value, so
* in order to keep the value of errorlevel unchanged in the case
* of calling an empty batch file, we must return that same value. */
ret = nErrorLevel;
}
else if (bc)
{
/* If a batch file runs another batch file as part of a compound command
* (e.g. "x.bat & somethingelse") then the first file gets terminated. */
/* Get its SETLOCAL stack so it can be migrated to the new context */
setlocal = bc->setlocal;
bc->setlocal = NULL;
ExitBatch();
}
/* Create a new context. This function will not
* return until this context has been exited */
new.prev = bc;
/* copy some fields in the new structure if it is the same file */
if (bSameFn)
{
new.mem = bc->mem;
new.memsize = bc->memsize;
new.mempos = 0;
new.memfree = FALSE; /* don't free this, being used before this */
}
bc = &new;
bc->RedirList = NULL;
bc->setlocal = setlocal;
}
GetFullPathName(fullname, ARRAYSIZE(bc->BatchFilePath), bc->BatchFilePath, NULL);
/* If a new batch file, load it into memory and close the file */
if (!bSameFn)
{
BatchFile2Mem(hFile);
CloseHandle(hFile);
}
bc->mempos = 0; /* Go to the beginning of the batch file */
#ifndef MSCMD_BATCH_ECHO
bc->bEcho = bEcho; /* Preserve echo across batch calls */
#endif
for (i = 0; i < 10; i++)
bc->shiftlevel[i] = i;
/* Parse the parameters and make a raw copy of them without modifications */
bc->params = BatchParams(firstword, param);
bc->raw_params = cmd_dup(param);
if (bc->raw_params == NULL)
{
error_out_of_memory();
return 1;
}
/* If we are calling from inside a FOR, hide the FOR variables */
saved_fc = fc;
fc = NULL;
/* Perform top-level batch initialization */
if (bTopLevel)
{
/* Default the top-level batch context type to .BAT */
BatType = BAT_TYPE;
/* If this is a .CMD file, adjust the type */
TCHAR *dot = _tcsrchr(bc->BatchFilePath, _T('.'));
if (dot && (!_tcsicmp(dot, _T(".cmd"))))
{
BatType = CMD_TYPE;
}
#ifdef MSCMD_BATCH_ECHO
bBcEcho = bEcho;
#endif
}
/* 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. */
while (bc == &new && !bExit)
{
Cmd = ParseCommand(NULL);
if (!Cmd)
{
if (!bParseError)
continue;
/* Echo the pre-parsed batch file line on error */
if (bEcho && !bDisableBatchEcho)
{
if (!bIgnoreEcho)
ConOutChar(_T('\n'));
PrintPrompt();
ConOutPuts(ParseLine);
ConOutChar(_T('\n'));
}
/* Stop all execution */
ExitAllBatches();
ret = 1;
break;
}
/* JPP 19980807 */
/* Echo the command and execute it */
bc->current = Cmd;
ret = ExecuteCommandWithEcho(Cmd);
FreeCommand(Cmd);
}
/* Perform top-level batch cleanup */
if (!bc || bTopLevel)
{
/* Reset the top-level batch context type */
BatType = NONE;
#ifdef MSCMD_BATCH_ECHO
bEcho = bBcEcho;
#endif
}
/* Restore the FOR variables */
fc = saved_fc;
/* Always return the last command's return code */
TRACE("Batch: returns %d\n", ret);
return ret;
}
VOID AddBatchRedirection(REDIRECTION **RedirList)
{
REDIRECTION **ListEnd;
/* Prepend the list to the batch context's list */
ListEnd = RedirList;
while (*ListEnd)
ListEnd = &(*ListEnd)->Next;
*ListEnd = bc->RedirList;
bc->RedirList = *RedirList;
/* Null out the pointer so that the list will not be cleared prematurely.
* These redirections should persist until the batch file exits. */
*RedirList = NULL;
}
/*
* Read a single line from the batch file from the current batch/memory position.
* Almost a copy of FileGetString with same UNICODE handling
*/
BOOL BatchGetString(LPTSTR lpBuffer, INT nBufferLength)
{
LPSTR lpString;
INT len = 0;
#ifdef _UNICODE
lpString = cmd_alloc(nBufferLength);
if (!lpString)
{
WARN("Cannot allocate memory for lpString\n");
error_out_of_memory();
return FALSE;
}
#else
lpString = lpBuffer;
#endif
/* read all chars from memory until a '\n' is encountered */
if (bc->mem)
{
for (; (bc->mempos < bc->memsize && len < (nBufferLength-1)); len++)
{
lpString[len] = bc->mem[bc->mempos++];
if (lpString[len] == '\n' )
{
len++;
break;
}
}
}
if (!len)
{
#ifdef _UNICODE
cmd_free(lpString);
#endif
return FALSE;
}
lpString[len++] = '\0';
#ifdef _UNICODE
MultiByteToWideChar(OutputCodePage, 0, lpString, -1, lpBuffer, len);
cmd_free(lpString);
#endif
return TRUE;
}
/*
* Read and return the next executable line form the current batch file
*
* If no batch file is current or no further executable lines are found
* return NULL.
*
* Set eflag to 0 if line is not to be echoed else 1
*/
LPTSTR ReadBatchLine(VOID)
{
TRACE("ReadBatchLine()\n");
/* User halt */
if (CheckCtrlBreak(BREAK_BATCHFILE))
{
ExitAllBatches();
return NULL;
}
if (!BatchGetString(textline, ARRAYSIZE(textline) - 1))
{
TRACE("ReadBatchLine(): Reached EOF!\n");
/* End of file */
ExitBatch();
return NULL;
}
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;
}
/* EOF */