Windows批处理中的安全数字比较

6

我知道在批处理文件中比较相等的东西时,通常会将两侧都用引号括起来,例如:

IF "%myvar% NEQ "0" 

但是,如果使用“大于”或“小于”进行比较,则无法正常工作,因为操作数将被视为带引号的字符串。因此,您可以改为执行以下操作:

IF %myvar% GTR 20000

需要注意的是,如果变量 %myvar% 没有被声明,那么它就相当于执行

IF GTR 20000

这是一个语法错误。

我想到了以下的解决方法:

IF 1%myvar% GTR 120000

我希望我的代码能够得到 IF 1 GTR 120000 的结果,即使 myvar 未定义,看起来它是有效的。

这种方式比较数字是否相等并且考虑到了未声明变量的情况,这是一个安全的方法吗?还是说我刚刚开启了一个新的潜在问题?


2
如果您无法完全控制值数据,那么最好确保任何未受保护的毒字符不存在。我的一般建议是在使用变量之前验证其是否已定义,“If [Not] Defined MyVar ...”,但这主要是为了可读性。 - Compo
1
为确保准确性,在比较变量之前,我会相应地重新分配变量:set /a myvar=myvarset /a myvar*=1。如果变量未定义或不是数字,则 set /a 将把变量设置为 0 - Stephan
1
@Stephan:哦,小心!如果“var”无效(非八进制)且具有前导0,则“set /a avar =%var%”将不会更改“avar”的值。(尽管设置了“errorlevel”) - Magoo
1
@Magoo:像set var=0z这样的吗?没错,set /a newvar=%var%会报错并让newvar保持不变。但是尝试一下set /a var=var(没有%!)或者set /a var*=1。甚至可以尝试set /a newvar=var - Stephan
1
为确保myvar已定义且仅包含数字,您可以使用if defined myvar Echo:%myvar%|findstr /BE "[0123456789]*" >NUl 2>&1 &&(Echo Only numbers)||(Echo invalid chars)。注意:使用findstr的范围[0-9]也将包括上标²³ - user6811411
显示剩余2条评论
2个回答

4
假设批处理文件包含以下内容:
@echo off
:PromptUser
rem Undefine environment variable MyVar in case of being already defined by chance.
set "MyVar="
rem Prompt user for a positive number in range 0 to 20000.
set /P "MyVar=Enter number [0,20000]: "

正如我在如何防止Windows命令解释器在用户输入错误时退出批处理文件执行?中所解释的那样,用户有自由输入任何内容,包括可能导致语法错误或执行批处理文件不符合预期的字符串。


1. 用户未输入任何内容

如果用户只按下 RETURNENTER 键,命令 SET 不会修改环境变量 MyVar。在这种情况下,可以通过在提示用户之前明确未定义环境变量 MyVar 并检查用户是否输入了字符串来验证:

if not defined MyVar goto PromptUser

注意:可以使用与“set“ MyVar =”不同的内容,例如“set“ MyVar =1000”来定义默认值,甚至可以在提示符上输出默认值,让用户有可能只需按 RETURN ENTER 即可使用默认值。
2. 用户输入了一个带有一个或多个“"”的字符串
用户可能故意或错误地输入了一个带有一个或多个“"”的字符串。例如,在启用了CapsLock的非数字键盘上按下German keyboard2会导致输入",除非使用 German(IBM),对于该键盘,CapsLock仅在字母上通过软件激活。因此,如果用户快速或没有查看屏幕而按下2RETURN,则用户可能会错误地输入双引号字符而不是2

如果MyVar包含一个或多个",那么所有%MyVar%"%MyVar%"的环境变量引用都会出现问题,因为%MyVar%会被Windows命令处理器替换为用户输入的字符串,其中包含一个或多个",这几乎总是导致语法错误或批处理文件执行了它不应该执行的操作。参见Windows Command Interpreter (CMD.EXE)如何解析脚本?

有两个解决方案:

  1. 启用延迟扩展,并使用!MyVar!"!MyVar!"引用环境变量,此时用户输入字符串不再影响由cmd.exe解析后执行的命令行。
  2. 如果该字符串永远不包含双引号字符,则从用户输入字符串中删除所有"
字符"在一个应该是介于020000(十进制)范围内的数字字符串中绝对无效。因此,可以使用两行代码来防止由"引起的用户输入字符串处理错误。请注意保留HTML标签。
set "MyVar=%MyVar:"=%"
if not defined MyVar goto PromptUser

Windows命令处理器在替换%MyVar:"=%为结果字符串之前,会先删除此行上的双引号。因此,最终执行的命令行set "MyVar=用户输入的任何内容"在执行时是安全的。

上述示例中错误输入了"而不是2,导致执行set "MyVar=",这将取消定义环境变量MyVar,这就是为什么在进一步处理用户输入之前必须再次使用之前使用的IF条件的原因。

3. 用户输入了无效字符

用户应该输入范围在020000之间的正十进制数。因此,除0123456789之外的任何其他字符都是无效的。例如,可以通过以下方式检查任何无效字符:

for /F delims^=0123456789^ eol^= %%I in ("%MyVar%") do goto PromptUser

命令FOR在整个字符串只包含数字的情况下不会执行goto PromptUser。在所有其他情况下,包括以;开头的字符串,零个或多个数字之后,由于输入字符串包含非数字字符,将导致执行goto PromptUser

4. 带有前导0的用户输入数字

Windows命令处理器将带有前导0的数字解释为八进制数。但是,即使用户在开头输入一个或多个0,该数字也应被解释为十进制数。因此,在进一步处理变量值之前,应删除前导零。

for /F "tokens=* delims=0" %%I in ("%MyVar%") do set "MyVar=%%I"
if not defined MyVar set "MyVar=0"

FOR 去除赋值给 MyVar 的字符串开头的所有 0,并将剩余的字符串分配给循环变量 I,然后将其赋值给环境变量 MyVar

FOR 在这种情况下运行 set "MyVar=%%I",即使用户输入了 0000,也会导致执行 set "MyVar=",在这种特殊情况下取消定义环境变量 MyVar。但是,0 是一个有效的数字,因此必须使用 IF 条件来重新定义 MyVar,并在用户输入数字 0 时用字符串值 0 替换其中一个或多个零。

5. 用户输入过大的数字

现在可以安全地使用带有运算符 GTR 的命令 IF 来验证用户是否输入了过大的数字。

if %MyVar% GTR 20000 goto PromptUser

这个最后的验证即使用户输入大于最大正32位整数值的82378488758723872198735897,也可以工作,因为范围溢出会导致在执行此IF条件时使用2147483647。有关详细信息,请参见weird results with IF上我的答案。

6. 可能的解决方案1

一个完整的批处理文件,用于安全评估用户输入的数字范围在 020000 之间,仅适用于十进制数

@echo off
set "MinValue=0"
set "MaxValue=20000"

:PromptUser
rem Undefine environment variable MyVar in case of being already defined by chance.
set "MyVar="
rem Prompt user for a positive number in range %MinValue% to %MaxValue%.
set /P "MyVar=Enter number [%MinValue%,%MaxValue%]: "

if not defined MyVar goto PromptUser
set "MyVar=%MyVar:"=%"
if not defined MyVar goto PromptUser
for /F delims^=0123456789^ eol^= %%I in ("%MyVar%") do goto PromptUser
for /F "tokens=* delims=0" %%I in ("%MyVar%") do set "MyVar=%%I"
if not defined MyVar set "MyVar=0"
if %MyVar% GTR %MaxValue% goto PromptUser
rem if %MyVar% LSS %MinValue% goto PromptUser

rem Output value of environment variable MyVar for visual verification.
set MyVar
pause

这个解决方案还可以让批处理文件编写者输出错误消息,告诉用户为什么输入字符串未被批处理文件接受。
如果MinValue的值为0,则最后一个IF条件与运算符LSS是不必要的,这就是为什么在此用例中它被注释掉并使用REM命令。

7. 可能的解决方案2

这是另一个安全的解决方案,但缺点是用户无法输入具有一个或多个前导0的十进制数,即使通常被用户期望为十进制。

@echo off
set "MinValue=0"
set "MaxValue=20000"

:PromptUser
rem Undefine environment variable MyVar in case of being already defined by chance.
set "MyVar="
rem Prompt user for a positive number in range %MinValue% to %MaxValue%.
set /P "MyVar=Enter number [%MinValue%,%MaxValue%]: "

if not defined MyVar goto PromptUser
setlocal EnableDelayedExpansion
set /A "Number=MyVar" 2>nul
if not "!Number!" == "!MyVar!" endlocal & goto PromptUser
endlocal
if %MyVar% GTR %MaxValue% goto PromptUser
if %MyVar% LSS %MinValue% goto PromptUser

rem Output value of environment variable MyVar for visual verification.
set MyVar
pause

此解决方案使用延迟环境变量扩展,作为上述第2点的第一选项。

算术表达式用于将用户输入字符串转换为带符号的32位整数,将字符串解释为十进制、八进制或十六进制数,并将其转换回分配给环境变量Number的字符串,其中Windows命令处理器使用十进制数字系统。由于无效的用户字符串导致算术表达式计算错误输出被重定向到设备NUL以抑制它。

接下来,使用延迟扩展验证算术表达式创建的数字字符串是否与用户输入的字符串不同。如果这个IF条件为真,则表示用户输入无效,包括数字具有前导零被cmd.exe解释为八进制,或者输入十六进制数字如0x140xe3

通过字符串比较后,可以安全地使用运算符GTRLSSMyVar的值与200000进行比较。

请阅读 此答案 了解有关命令 SETLOCALENDLOCAL 的详细信息,因为运行 setlocal EnableDelayedExpansionendlocal 比仅启用和禁用延迟环境变量扩展要做的更多。

8. 可能的解决方案 3

如果值0不在有效范围内,即用户输入的数字必须大于0,则可以使用更少的命令行来提供另一种解决方案。

@echo off
set "MinValue=1"
set "MaxValue=20000"

:PromptUser
rem Undefine environment variable MyVar in case of being already defined by chance.
set "MyVar="
rem Prompt user for a positive number in range %MinValue% to %MaxValue%.
set /P "MyVar=Enter number [%MinValue%,%MaxValue%]: "
set /A MyVar+=0
if %MyVar% GTR %MaxValue% goto PromptUser
if %MyVar% LSS %MinValue% goto PromptUser

rem Output value of environment variable MyVar for visual verification.
set MyVar
pause

这段代码使用set /A MyVar+=0将用户输入的字符串转换为32位有符号整数值,并按照aschipfl他的评论中的建议将其转换回字符串。
如果用户没有输入任何字符串,则命令行中带有算术表达式后MyVar的值为0。如果用户输入的字符串的第一个字符不是这些字符之一-+0123456789,例如"/(,则MyVar的值也为0
一个以数字、减号或加号开头且下一个字符为数字的用户输入字符串将被转换为整数值,然后再转换回字符串值。输入的字符串可以是十进制数、八进制数或十六进制数。请查看我在Symbol equivalent to NEQ, LSS, GTR, etc. in Windows batch files上的答案,其中详细解释了Windows命令处理器如何将字符串转换为整数值。
这段代码的缺点是,如果由于在德语键盘上按下2和(键时按住Shift导致输入字符串错误,例如7"(而不是728,则此代码无法检测到。当用户输入错误时,MyVar的值为7。Windows命令处理器只解释直到第一个非有效字符为止的字符作为整数值,并忽略字符串的其余部分。
使用此代码的批处理文件在处理过程中不会因为语法错误而意外退出,这是安全的,无论用户输入什么。但是,如果用户错误地输入了一个数字,有时代码不会检测到,导致继续处理批处理文件,并使用用户不想使用的数字。

1

回应挑剔的要求

Mofi一直要求我写出自己的解决方案,并且要比他的代码更“简短”,正如我指出的,他使用&而不是(后跟一个命令,然后是回车和另一个命令,或者是`(后跟一个回车符,然后是另一个命令,再跟一个回车符,最后是另一个命令)设置了一个先例,这使得这个任务很难达成一致。

我也认为这并不是提供答案的重点,我的意思是,以前我会这么认为,但当改变很小,主要是修复逻辑或提供一个稍微不同的解决方案时,这真的有很大的区别吗?这真的值得成为一个单独的答案吗?

话虽如此,我没有看到更好的方法,除非编辑他的回答...但这仍然存在未解决的问题,即如何判断哪个更短。

不幸的是,在与Mofi讨论时,他编辑了他的答案,结果可能导致无效的选择。

尽管我已经指出了这一点,我相信这只是他的一个小小的疏忽,但我觉得不在这里发布代码已经导致他积极恶化了他的问题的质量,这是挑剔的一个可能的结果。

Mofi是那项活动的主要推动者,但我不喜欢它对他的影响,因为我试图避免这种影响发生在我的代码上,所以我决定发布代码比较来给他们带来一些解决。

请注意,我将发布他的原始代码(最近一个没有使用错误方法的代码),然后重构为我写的方式,然后再发布我的原始代码,然后重构为我认为他会写的方式(可能不按顺序,但我会说明每个)

因此下面是结果

Mofi原版:

很难说你是否应该计算每一行,有些情况下会使用&来排队命令,而IFS从不使用括号,这不是我通常会做的事。

@echo off
set "MinValue=0"
set "MaxValue=20000"

:PromptUser
rem Undefine environment variable MyVar in case of being already defined by chance.
set "MyVar="
rem Prompt user for a positive number in range %MinValue% to %MaxValue%.
set /P "MyVar=Enter number [%MinValue%,%MaxValue%]: "

if not defined MyVar goto PromptUser
setlocal EnableDelayedExpansion
set /A "Number=MyVar" 2>nul
if not "!Number!" == "!MyVar!" endlocal & goto PromptUser
endlocal
if %MyVar% GTR %MaxValue% goto PromptUser
if %MyVar% LSS %MinValue% goto PromptUser

rem Output value of environment variable MyVar for visual verification.
set MyVar
pause

我的代码重构为Mofi的形式

@ECHO OFF
SETLOCAL EnableDelayedExpansion
SET /A "_Min=-1","_Max=20000"
:Menu
  CLS
  SET "_Input="
  REM Prompt user for a positive number in range %_Min% to %_Max%.
  SET /P "_Input=Enter number [%_Min%,%_Max%]: "
  SET /A "_Tmp=%_input%" && if /I "!_input!" EQU "!_Tmp!" if !_Input! GEQ %_Min% if !_Input! LEQ %_Max% SET _Input & pause & GOTO :EOF 
GOTO :Menu

Mofi的代码重构

Mofi的上述代码被我重新压缩了,其中(跟随第一个命令,除非用于IF语句,而)则跟随最后一个命令。这也使得真正执行验证的整个部分易于辨别,它只是:PromptUser函数内的部分,不包括REM行或空白行,这是13行代码。

@(SETLOCAL
  echo off
  SET /A "MinValue=0","MaxValue=20000")

CALL :Main

( ENDLOCAL
  EXIT /B )

:Main
  CALL :PromptUser MyVar
  REM Output value of environment variable MyVar for visual verIFication.
  SET MyVar
  PAUSE
GOTO :EOF


:PromptUser
  SET "MyVar="
  rem Prompt user for a positive number in range %MinValue% to %MaxValue%.
  SET /P "MyVar=Enter number [%MinValue%,%MaxValue%]: "
  
  IF NOT DEFINED MyVar GOTO :PromptUser
  Setlocal EnableDelayedExpansion
  SET /A "Number=MyVar" 2>nul
  
  IF not "!Number!" == "!MyVar!" (
    Endlocal
    GOTO :PromptUser  )
  Endlocal
  IF %MyVar% GTR %MaxValue% (
    GOTO :PromptUser  )
  IF %MyVar% LSS %MinValue% (
    GOTO :PromptUser )
GOTO :EOF

我的简洁代码

为了比较,这是我重构Mofi代码后的同样简洁的形式。再次强调,只有函数内部的代码在这里“发挥作用”并需要进行比较。当我最初编写代码时,我忘记了尝试匹配Mofi的形式,这使我在下一行保留了我的&&或将其全部作为单行。因此,我将发布两个变体。

@(SETLOCAL ENABLEDELAYEDEXPANSION
  ECHO OFF
  SET /A "_Min=-1","_Max=20000" )

CALL :Main

( ENDLOCAL
  EXIT /B )

:Main
  CALL :Menu _input
  REM Output value of environment variable _input for visual verIFication.
  SET _input
  PAUSE
GOTO :EOF


:Menu
  CLS
  SET "_input="
  REM Prompt user for a positive number in range %_Min% to %_Max%. Store it in "_input"
  SET /P "_Input=Enter number [%_Min%,%_Max%]: "
  SET /A "_Tmp=%_input%" && (
    IF /I "!_input!" EQU "!_Tmp!" IF !_Input! GEQ %_Min% IF !_Input! LEQ %_Max% GOTO :EOF )
GOTO :Menu

我的紧凑形式代码 2

@(SETLOCAL ENABLEDELAYEDEXPANSION
  ECHO OFF
  SET /A "_Min=-1","_Max=20000" )

CALL :Main

( ENDLOCAL
  EXIT /B )

:Main
  CALL :Menu
  REM Output value of environment variable _input for visual verification.
  SET _input
  PAUSE
GOTO :EOF


:Menu
  CLS
  SET "_input="
  REM Prompt user for a positive number in range %_Min% to %_Max%. Store it in "_input"
  SET /P "_Input=Enter number [%_Min%,%_Max%]: "
  SET /A "_Tmp=%_input%" || GOTO :Menu 
  IF /I "!_input!" EQU "!_Tmp!" (
    IF !_Input! GEQ %_Min% (
      IF !_Input! LEQ %_Max% (
        GOTO :EOF ) ) )
GOTO :Menu

非常感谢您的回答。我真的很喜欢看看其他程序员如何编写我的代码以及如何使用100%语法正确的代码解决与我不同的任务,并考虑尽可能多的用例,这真的太棒了。 - Mofi
好的,我不会像在 "!_input!" EQU "!_Tmp!" 中那样使用 EQU 运算符来比较两个字符串。我会在这个 IF 条件中使用字符串比较运算符 ==。但是,在 Mofi's Code Refactored 中的其他所有内容都写得非常好。我也喜欢 紧凑形式 变体。再次感谢您的回答。 - Mofi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接