reactos/base/shell/cmd/set.c
Hermès Bélusca-Maïto 17e094cd34
[CMD] SET: Diverse fixes for the arithmetic-expression parser (/A option).
- Detect whether a division by zero is done, and fail if so.

- Detect whether an invalid number is provided:
  * If _tcstol() fails with errno == ERANGE, we've got an overflow or
    underflow.
  * If the next character where _tcstol() is not a whitespace but is a
    character compatible with the first character of an identifier, the
    number is invalid.

- Add + to the list of existing unary operators (!,~,-), and parse them
  where many of these are present. Indeed, expressions like: +3, -+-+3,
  !!-+3 (or with other unary ops, etc.) are valid.

- Operators constituted of more than one characters, can contain
  whitespace separating their constituting characters.
  Thus, "a + = 3" is equivalent to "a += 3" (and the same for -=, *=,
  /=, %=, &=, |= and ^=), and "a < < 3" is equivalent to "a << 3" (and
  the same for >>, <<= and >>=).

- After evaluating everything, if unparsed data remains, fail and bail out.

- Return Windows' CMD-compatible errorlevels.

See https://ss64.com/nt/set.html for more details.

Fixes some cmd_winetests.
2020-09-23 00:22:48 +02:00

751 lines
17 KiB
C

/*
* SET.C - set internal command.
*
*
* History:
*
* 06/14/97 (Tim Norman)
* changed static var in set() to a cmd_alloc'd space to pass to putenv.
* need to find a better way to do this, since it seems it is wasting
* memory when variables are redefined.
*
* 07/08/1998 (John P. Price)
* removed call to show_environment in set command.
* moved test for syntax before allocating memory in set command.
* misc clean up and optimization.
*
* 27-Jul-1998 (John P Price <linux-guru@gcfl.net>)
* added config.h include
*
* 28-Jul-1998 (John P Price <linux-guru@gcfl.net>)
* added set_env function to set env. variable without needing set command
*
* 09-Dec-1998 (Eric Kohl)
* Added help text ("/?").
*
* 24-Jan-1999 (Eric Kohl)
* Fixed Win32 environment handling.
* Unicode and redirection safe!
*
* 25-Feb-1999 (Eric Kohl)
* Fixed little bug.
*
* 30-Apr-2005 (Magnus Olsen <magnus@greatlord.com>)
* Remove all hardcoded strings in En.rc
*/
#include "precomp.h"
#ifdef INCLUDE_CMD_SET
/* Initial size of environment variable buffer */
#define ENV_BUFFER_SIZE 1024
static BOOL
seta_eval(LPCTSTR expr);
static LPCTSTR
skip_ws(LPCTSTR p)
{
while (*p && *p <= _T(' '))
++p;
return p;
}
/* Used to check for and handle:
* SET "var=value", SET /P "var=prompt", and SET /P var="prompt" */
static LPTSTR
GetQuotedString(TCHAR *p)
{
TCHAR *end;
if (*p == _T('"'))
{
p = (LPTSTR)skip_ws(p + 1);
/* If a matching quote is found, truncate the string */
end = _tcsrchr(p, _T('"'));
if (end)
*end = _T('\0');
}
return p;
}
INT cmd_set(LPTSTR param)
{
INT retval = 0;
LPTSTR p;
LPTSTR lpEnv;
LPTSTR lpOutput;
if (!_tcsncmp(param, _T("/?"), 2))
{
ConOutResPaging(TRUE,STRING_SET_HELP);
return 0;
}
param = (LPTSTR)skip_ws(param);
/* If no parameters, show the environment */
if (param[0] == _T('\0'))
{
lpEnv = (LPTSTR)GetEnvironmentStrings();
if (lpEnv)
{
lpOutput = lpEnv;
while (*lpOutput)
{
/* Do not display the special '=X:' environment variables */
if (*lpOutput != _T('='))
{
ConOutPuts(lpOutput);
ConOutChar(_T('\n'));
}
lpOutput += _tcslen(lpOutput) + 1;
}
FreeEnvironmentStrings(lpEnv);
}
retval = 0;
goto Quit;
}
/* The /A does *NOT* have to be followed by a whitespace */
if (!_tcsnicmp(param, _T("/A"), 2))
{
BOOL Success;
/* Save error level since seta_eval() modifies it, as
* we need to set it later according to specific rules. */
INT nOldErrorLevel = nErrorLevel;
StripQuotes(param);
Success = seta_eval(skip_ws(param + 2));
if (!Success)
{
#if 0
/* Might seem random but this is what windows xp does -- This is a message ID */
retval = 9165;
#endif
retval = nErrorLevel;
nErrorLevel = nOldErrorLevel;
}
else
{
retval = 0;
}
goto Quit;
}
if (!_tcsnicmp(param, _T("/P"), 2))
{
TCHAR value[1023];
param = GetQuotedString((LPTSTR)skip_ws(param + 2));
p = _tcschr(param, _T('='));
if (!p)
{
ConErrResPuts(STRING_SYNTAX_COMMAND_INCORRECT);
retval = 1;
goto Quit;
}
*p++ = _T('\0');
ConOutPrintf(_T("%s"), GetQuotedString(p));
ConInString(value, ARRAYSIZE(value));
if (!*value || !SetEnvironmentVariable(param, value))
{
retval = 1;
goto Quit;
}
retval = 0;
goto Quit;
}
param = GetQuotedString(param);
p = _tcschr(param, _T('='));
if (p)
{
/* Set or remove the environment variable */
if (p == param)
{
/* Handle set =val case */
ConErrResPuts(STRING_SYNTAX_COMMAND_INCORRECT);
retval = 1;
goto Quit;
}
*p++ = _T('\0');
if (!SetEnvironmentVariable(param, *p ? p : NULL))
{
retval = 1;
goto Quit;
}
}
else
{
/* Display all the environment variables with the given prefix */
LPTSTR pOrgParam = param;
BOOLEAN bFound = FALSE;
BOOLEAN bRestoreSpace;
/*
* Trim the prefix from "special" characters (only when displaying the
* environment variables), so that e.g. "SET ,; ,;FOO" will display all
* the variables starting by "FOO".
* The SET command allows as well to set an environment variable whose name
* actually contains these characters (e.g. "SET ,; ,;FOO=42"); however,
* by trimming the characters, doing "SET ,; ,;FOO" would not allow seeing
* such variables.
* Thus, we also save a pointer to the original variable name prefix, that
* we will look it up as well below.
*/
while (_istspace(*param) || *param == _T(',') || *param == _T(';'))
++param;
/* Just remove the very last space, if present */
p = _tcsrchr(param, _T(' '));
bRestoreSpace = (p != NULL);
if (!p)
p = param + _tcslen(param);
*p = _T('\0');
lpEnv = GetEnvironmentStrings();
if (lpEnv)
{
lpOutput = lpEnv;
while (*lpOutput)
{
/* Look up for both the original and truncated variable name prefix */
if (!_tcsnicmp(lpOutput, pOrgParam, p - pOrgParam) ||
!_tcsnicmp(lpOutput, param, p - param))
{
ConOutPuts(lpOutput);
ConOutChar(_T('\n'));
bFound = TRUE;
}
lpOutput += _tcslen(lpOutput) + 1;
}
FreeEnvironmentStrings(lpEnv);
}
/* Restore the truncated space for correctly
* displaying the error message, if any. */
if (bRestoreSpace)
*p = _T(' ');
if (!bFound)
{
ConErrResPrintf(STRING_SET_ENV_ERROR, param);
retval = 1;
goto Quit;
}
}
Quit:
if (BatType != CMD_TYPE)
{
if (retval != 0)
nErrorLevel = retval;
}
else
{
nErrorLevel = retval;
}
return retval;
}
static INT
ident_len(LPCTSTR p)
{
LPCTSTR p2 = p;
if (__iscsymf(*p))
{
++p2;
while (__iscsym(*p2))
++p2;
}
return (INT)(p2-p);
}
#define PARSE_IDENT(ident, identlen, p) \
do { \
identlen = ident_len(p); \
ident = (LPTSTR)alloca((identlen + 1) * sizeof(TCHAR)); \
memmove(ident, p, identlen * sizeof(TCHAR)); \
ident[identlen] = 0; \
p += identlen; \
} while (0)
static INT
seta_identval(LPCTSTR ident)
{
LPCTSTR identVal = GetEnvVarOrSpecial(ident);
if (!identVal)
return 0;
else
return _tcstol(identVal, NULL, 0);
}
static BOOL
calc(INT* lval, TCHAR op, INT rval)
{
switch (op)
{
case '*':
*lval *= rval;
break;
case '/':
{
if (rval == 0)
{
// FIXME: Localize
ConErrPuts(_T("Division by zero error.\n"));
nErrorLevel = 0x400023D1; // 1073750993;
return FALSE;
}
*lval /= rval;
break;
}
case '%':
{
if (rval == 0)
{
// FIXME: Localize
ConErrPuts(_T("Division by zero error.\n"));
nErrorLevel = 0x400023D1; // 1073750993;
return FALSE;
}
*lval %= rval;
break;
}
case '+':
*lval += rval;
break;
case '-':
*lval -= rval;
break;
case '&':
*lval &= rval;
break;
case '^':
*lval ^= rval;
break;
case '|':
*lval |= rval;
break;
default:
ConErrResPuts(STRING_INVALID_OPERAND);
nErrorLevel = 0x400023CE; // 1073750990;
return FALSE;
}
return TRUE;
}
static BOOL
seta_stmt(LPCTSTR* p_, INT* result);
static BOOL
seta_unaryTerm(LPCTSTR* p_, INT* result)
{
LPCTSTR p = *p_;
INT rval;
if (*p == _T('('))
{
p = skip_ws(p + 1);
if (!seta_stmt(&p, &rval))
return FALSE;
if (*p++ != _T(')'))
{
ConErrResPuts(STRING_EXPECTED_CLOSE_PAREN);
nErrorLevel = 0x400023CC; // 1073750988;
return FALSE;
}
*result = rval;
}
else if (_istdigit(*p))
{
errno = 0;
rval = _tcstol(p, (LPTSTR*)&p, 0);
/* Check for overflow / underflow */
if (errno == ERANGE)
{
// FIXME: Localize
ConErrPuts(_T("Invalid number. Numbers are limited to 32-bits of precision.\n"));
nErrorLevel = 0x400023D0; // 1073750992;
return FALSE;
}
/*
* _tcstol() stopped at the first non-digit character. If it's not a whitespace,
* or if it's the start of a possible identifier, this means the number being
* interpreted was invalid.
*/
else if (*p && !_istspace(*p) && __iscsymf(*p))
{
// FIXME: Localize
ConErrPuts(_T("Invalid number. Numeric constants are either decimal (42), hexadecimal (0x2A), or octal (052).\n"));
nErrorLevel = 0x400023CF; // 1073750991;
return FALSE;
}
*result = rval;
}
else if (__iscsymf(*p))
{
LPTSTR ident;
INT identlen;
PARSE_IDENT(ident, identlen, p);
*result = seta_identval(ident);
}
else
{
ConErrResPuts(STRING_EXPECTED_NUMBER_OR_VARIABLE);
nErrorLevel = 0x400023CD; // 1073750989;
return FALSE;
}
*p_ = skip_ws(p);
return TRUE;
}
static BOOL
seta_mulTerm(LPCTSTR* p_, INT* result)
{
LPCTSTR p = *p_;
TCHAR op = 0;
INT rval;
if (_tcschr(_T("!~-+"), *p))
{
op = *p;
p = skip_ws(p + 1);
if (!seta_mulTerm(&p, &rval))
return FALSE;
switch (op)
{
case '!':
rval = !rval;
break;
case '~':
rval = ~rval;
break;
case '-':
rval = -rval;
break;
#if 0
case '+':
rval = rval;
break;
#endif
}
}
else
{
if (!seta_unaryTerm(&p, &rval))
return FALSE;
}
*result = rval;
*p_ = p;
return TRUE;
}
static BOOL
seta_ltorTerm(LPCTSTR* p_, INT* result, LPCTSTR ops, BOOL (*subTerm)(LPCTSTR*,INT*))
{
LPCTSTR p = *p_;
INT lval;
/* Evaluate the left-hand side */
if (!subTerm(&p, &lval))
return FALSE;
while (*p && _tcschr(ops, *p))
{
INT rval;
TCHAR op = *p;
p = skip_ws(p + 1);
/* Evaluate the immediate right-hand side */
if (!subTerm(&p, &rval))
return FALSE;
/* This becomes the new left-hand side for the next iteration */
if (!calc(&lval, op, rval))
return FALSE;
}
*result = lval;
*p_ = p;
return TRUE;
}
static BOOL
seta_addTerm(LPCTSTR* p_, INT* result)
{
return seta_ltorTerm(p_, result, _T("*/%"), seta_mulTerm);
}
static BOOL
seta_logShiftTerm(LPCTSTR* p_, INT* result)
{
return seta_ltorTerm(p_, result, _T("+-"), seta_addTerm);
}
static BOOL
seta_bitAndTerm(LPCTSTR* p_, INT* result)
{
LPCTSTR p = *p_;
INT lval;
/* Evaluate the left-hand side */
if (!seta_logShiftTerm(&p, &lval))
return FALSE;
/* Handle << >> operators */
while (*p && _tcschr(_T("<>"), *p))
{
INT rval;
TCHAR op = *p;
/* Check whether the next non-whitespace character is the same operator */
p = skip_ws(p + 1);
if (*p != op)
break;
/* Skip it */
p = skip_ws(p + 1);
/* Evaluate the immediate right-hand side */
if (!seta_logShiftTerm(&p, &rval))
return FALSE;
/* This becomes the new left-hand side for the next iteration */
switch (op)
{
case '<':
{
/* Shift left has to be a positive number, 0-31 otherwise 0 is returned,
* which differs from the compiler (for example gcc) so being explicit. */
if (rval < 0 || rval >= (8 * sizeof(lval)))
lval = 0;
else
lval <<= rval;
break;
}
case '>':
lval >>= rval;
break;
default:
ConErrResPuts(STRING_INVALID_OPERAND);
nErrorLevel = 0x400023CE; // 1073750990;
return FALSE;
}
}
*result = lval;
*p_ = p;
return TRUE;
}
static BOOL
seta_bitExclOrTerm(LPCTSTR* p_, INT* result)
{
return seta_ltorTerm(p_, result, _T("&"), seta_bitAndTerm);
}
static BOOL
seta_bitOrTerm(LPCTSTR* p_, INT* result)
{
return seta_ltorTerm(p_, result, _T("^"), seta_bitExclOrTerm);
}
static BOOL
seta_expr(LPCTSTR* p_, INT* result)
{
return seta_ltorTerm(p_, result, _T("|"), seta_bitOrTerm);
}
static BOOL
seta_assignment(LPCTSTR* p_, INT* result)
{
LPCTSTR p = *p_;
LPTSTR ident;
TCHAR op = 0;
INT identlen, exprval;
PARSE_IDENT(ident, identlen, p);
if (identlen)
{
p = skip_ws(p);
/* Handle = assignment */
if (*p == _T('='))
{
op = *p;
p = skip_ws(p + 1);
}
/* Handle *= /= %= += -= &= ^= |= assignments */
else if (_tcschr(_T("*/%+-&^|"), *p))
{
op = *p;
/* Find the '=', there may be some spaces before it */
p = skip_ws(p + 1);
if (*p != _T('='))
{
op = 0;
goto evaluate;
}
/* Skip it */
p = skip_ws(p + 1);
}
/* Handle <<= >>= assignments */
else if (_tcschr(_T("<>"), *p))
{
op = *p;
/* Check whether the next non-whitespace character is the same operator */
p = skip_ws(p + 1);
if (*p != op)
{
op = 0;
goto evaluate;
}
/* Find the '=', there may be some spaces before it */
p = skip_ws(p + 1);
if (*p != _T('='))
{
op = 0;
goto evaluate;
}
/* Skip it */
p = skip_ws(p + 1);
}
}
evaluate:
/* Allow to chain multiple assignments, such as: a=b=1 */
if (ident && op)
{
INT identval;
LPTSTR buf;
if (!seta_assignment(&p, &exprval))
return FALSE;
identval = seta_identval(ident);
switch (op)
{
/* Handle = assignment */
case '=':
identval = exprval;
break;
/* Handle <<= assignment */
case '<':
{
/* Shift left has to be a positive number, 0-31 otherwise 0 is returned,
* which differs from the compiler (for example gcc) so being explicit. */
if (exprval < 0 || exprval >= (8 * sizeof(identval)))
identval = 0;
else
identval <<= exprval;
break;
}
/* Handle >>= assignment */
case '>':
identval >>= exprval;
break;
/* Other assignments */
default:
if (!calc(&identval, op, exprval))
return FALSE;
}
buf = (LPTSTR)alloca(32 * sizeof(TCHAR));
_sntprintf(buf, 32, _T("%i"), identval);
SetEnvironmentVariable(ident, buf); // TODO FIXME - check return value
exprval = identval;
}
else
{
/* Restore p in case we found an identifier but not an operator */
p = *p_;
if (!seta_expr(&p, &exprval))
return FALSE;
}
*result = exprval;
*p_ = p;
return TRUE;
}
static BOOL
seta_stmt(LPCTSTR* p_, INT* result)
{
LPCTSTR p = *p_;
INT rval;
if (!seta_assignment(&p, &rval))
return FALSE;
/* Loop over each statement */
while (*p == _T(','))
{
p = skip_ws(p + 1);
if (!seta_assignment(&p, &rval))
return FALSE;
}
*result = rval;
*p_ = p;
return TRUE;
}
static BOOL
seta_eval(LPCTSTR p)
{
INT rval;
if (!*p)
{
ConErrResPuts(STRING_SYNTAX_COMMAND_INCORRECT);
nErrorLevel = 1;
return FALSE;
}
if (!seta_stmt(&p, &rval))
return FALSE;
/* If unparsed data remains, fail and bail out */
if (*p)
{
ConErrResPuts(STRING_SYNTAX_COMMAND_INCORRECT); // Actually syntax error / missing operand.
nErrorLevel = 0x400023CE; // 1073750990;
return FALSE;
}
/* Echo the result of the evaluation only in interactive (non-batch) mode */
if (!bc)
ConOutPrintf(_T("%i"), rval);
return TRUE;
}
#endif