哪种正则表达式方法最适合验证用户输入? (for /f with delims vs. echo %var%|Findstr /ri)

Which regex method is best for validating user input? (for /f with delims vs. echo %var%|Findstr /ri)

我想验证用户的输入并将输入限制为仅字母数字字符(也可以允许下划线),但我不确定哪种方法最适合。

我在 SA 上看到过各种示例,第一个向我提出问题的是以下示例:

:input
set "in="
set /p "in=Please enter your username: "

ECHO(%in%|FINDSTR /ri "^[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ][0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ]*$" >nul || (

    goto input

)

我看到第二个案例与第一个案例相同(正如预期的那样,前导 ^ 和结尾 *$)。

为什么在以下也有效的情况下还需要额外的大小写和 ^ *$?:

:input
set "in="
set /p "in=Please enter your username: "

ECHO(%in%|FINDSTR /ri "[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ]" >nul || (

    goto input

)

最后FOR /F 循环方法我在这里也注意到了:

for /f "delims=1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ" %%a in ("%in%") do goto :input

与前面提到的 FINDSTR 正则表达式相比,使用它有什么(缺点)优势吗?

首先,您必须使用延迟扩展来引用环境变量 in,以避免在用户输入包含 ><|&" 等关键字符的字符串时由于语法错误而退出批处理文件执行.始终考虑到用 %variable% 指定的变量在执行命令行之前被扩展,这很容易破坏用户输入变量字符串的批处理。

其次,强烈建议在提示后立即验证用户是否输入任何内容,即在提示命令行后使用if not defined in goto input

第三,我认为 FOR 方法更好,因为速度更快。

FINDSTR 不是 cmd.exe 的内部命令,如 FOR。因此,当在没有路径和文件扩展名的批处理文件中指定 FINDSTR 时,Windows 命令解释器必须首先搜索此可执行文件,并希望通过 [=16= 真正找到 %SystemRoot%\System32\findstr.exe ] 和 PATH.

接下来使用防病毒进程 运行在后台执行 findstr.exe 会触发防病毒进程的扫描进程,从而导致执行延迟。

Windows 命令解释器执行 FINDSTR 这样的应用程序总是需要更长的时间,因为 cmd.exe 的内部命令的执行即使有没有防病毒扫描进程 运行ning。因此 FOR 循环方法很可能(未经我验证)比 FINDSTR 方法更快。

在使用 FINDSTR 时,需要正则表达式字符 ^*$,因为正则表达式搜索字符串 [0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ] 结果为正如果处理后的行包含至少 1 个数字或字母,则匹配。因此不会检查行(= 变量字符串)是否仅包含数字和字母。根据选项 /I[0-9A-Za-z] 的较短字符 class 定义 [0-9A-Z] 不能在这种情况下使用,如 aschipfl 在他下面的评论中所解释的那样。

^指定搜索的字符串必须在行首找到,用*指定必须找到0个或多个数字或字母,用$搜索的字符串必须在行尾找到。或者换句话说,整行(用户输入)不是完全空的,如之前检查的那样,必须完全仅由数字和字母组成,以进行肯定匹配。

对于每个内部或外部命令,可以通过 运行 在命令提示符 window 中使用 /? 作为参数来获取命令帮助。打开命令提示符尝试一下 window and 运行 findstr /? and for /? and set /?.

为了安全地验证用户输入,这两种方法都是可靠的,但您必须改进它们:


findstr方法

首先,让我们关注一下^[...][...]*$这样的搜索字符串(其中...代表一个字符class,意思是一组字符):一个字符class [...] 匹配集合 ... 中的任意一个字符; * 表示重复,因此匹配零次或多次出现,因此 [...]* 匹配集合 ... 中出现的零次或多次字符;因此,[...][...]* 匹配集合 ... 中出现的一个或多个字符。前导 ^ 将匹配锚定到行首,尾随 $ 将匹配锚定到行尾;因此,当指定两个锚点时,整行必须匹配搜索字符串。

关于字符 classes [...]:根据线程 What are the undocumented features and limitations of the Windows FINDSTR command?,classes 是错误的;例如,class [A-Z] 匹配小写字母 bz,而 [a-z] 匹配大写字母 AY(如果 case-insensitive 搜索完成,这当然无关紧要,因此当给出 /I 时); class [0-9] 可能匹配 ²³,具体取决于当前代码页;例如,[A-Z][a-z] 可能匹配 Áá 等特殊字母,这也取决于当前代码页。因此,为了安全地只匹配某些字符,不要使用范围,而是单独指定每个字符,如 [0123456789][ABCDEFGHIJKLMNOPQRSTUVWXYZ][abcdefghijklmnopqrstuvwxyz].

所有这些导致我们进入以下 findstr 命令行:

findstr /R /I "^[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ][0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ]*$"

然而,使用管道 echo 的整个方法可能仍然会失败,因为像 "&^% 这样的特殊字符!()<>| 可能会导致语法错误或其他意外行为。为避免这种情况,我们需要为任一侧(继承当前环境)建立 delayed expansion, so the special characters become hidden from the command parser. However, since pipes (|) 初始化新的 cmd 实例,我们需要确保在左侧进行实际变量扩展 child cmd 实例而不是 parent 实例,像这样:

:INPUT
set "IN="
set /P IN="Please enter your username: "

cmd /V /C echo(^^!IN^^!| findstr /R /I "^[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ][0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ]*$" > nul || goto :INPUT

需要额外的显式cmd实例来启用延迟扩展(/V),因为管道启动的实例已禁用延迟扩展。

只有在 parent cmd 实例中也启用了延迟扩展的情况下才需要感叹号 ^^! 的双重转义;如果不是,单次转义 ^! 就足够了,但双次转义也无妨。


for /F方法

这种方法使生活更轻松,因为不涉及管道,因此您不必处理多个 cmd 实例,但仍有改进的余地。同样,特殊字符可能会造成麻烦,因此需要启用延迟扩展。

for /F loop ignores empty lines and such beginning with the default eol character, the semicolon ;. To disable the eol option, simply define one of the delimiter characters, so eol becomes hidden behind delims. Empty lines are not iterated, so the goto command in your approach would never execute in case of empty user input. Therefore, we must capture empty user input explicitly, using an if statement。现在所有这些导致以下代码:

setlocal EnableDelayedExpansion
:INPUT
set "IN="
set /P IN="Please enter your username: "

if not defined IN goto :INPUT
for /F "delims=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ eol=0" %%Z in ("!IN!") do goto :INPUT

endlocal

这种方法只检测大写字母;要同时包含小写字母,您必须将它们添加到 delims 选项:delims=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.

请注意,变量 INendlocal 之后不再可用,但这应该是脚本的最后一个命令。

为了检测for /F循环是否迭代,有一个未记录的特性,我们可以利用它:for /F returns a non-zero如果不迭代则退出代码,因此可以使用 conditional execution operators && or || ;所以,当用户输入为空时,循环不迭代,则||;为此,for /F 循环必须包含在 parentheses:

setlocal EnableDelayedExpansion
:INPUT
set "IN="
set /P IN="Please enter your username: "

if not defined IN goto :INPUT
(for /F "delims=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ eol=0" %%Z in ("!IN!") do rem/) && goto :INPUT

endlocal