在 IF 语句中对数组使用 DelayedExpansion 索引变量失败

using a DelayedExpansion index variable for an array within an IF statement fails

我有一个批处理文件,我在其中显示目录中的所有 *.pem 文件,让用户可以选择一个,此时只是试图通过使用数字向用户显示他们选择的文件他们选择 ECHO 数组的内容。我相信不知何故,因为它在 IF 语句内部,索引变量 "selectedPem" 没有在数组的索引括号中扩展,因为它使用的是 % 而不是使用延迟扩展!,这似乎在数组的索引括号。我认为这是因为如果我在 IF 语句之前设置 "selectedPem" 变量,它会正确回显。我认为唯一可行的选择是以某种方式使用子例程。为什么 selectedPem 变量在 IF 语句中不起作用?

@echo off
setlocal enabledelayedexpansion 
set dir=%~dp0
cd %dir%

ECHO Performing initial search of private and public keys...
set pemI=0
set keyI=0
for %%p in (*.pem) do (
    REM echo %%~nxp
    set pems[%pemI%]=%%~nxp
    REM echo !pems[%pemI%]!
    set /a pemI=%pemI%+1
)
REM echo %pems[0]%
set /a totalPems=%pemI%-1
REM This is the test selectedPem variable that showed me the variable works 
REM if I set it outside 
REM of the "if defined pems[0]" statement
REM set selectedPem=
if defined pems[0] (
    echo PEM Files Found:
    for /l %%p in (0,1,%totalPems%) do (
        echo %%p^) !pems[%%p]!
    )
    ECHO Select a pem file to be used as your private key. Otherwise, press 
    Q to not select one.
    set /P selectedPem=Enter a number or Q:
    IF "!selectedPem!"=="q" (
        ECHO Skipping private key selection.
    ) ELSE (
        ECHO !selectedPem!
        ECHO %pems[0]%
        REM The below ECHO only works when the above selectedPem is set 
        REM outside of the "IF defined" statement
        ECHO !pems[%selectedPem%]!

        REM Tried these other implementations:
        REM ECHO !pems[!!selectedPem!!]!
        REM ECHO %pems[!selectedPem!]%
        REM setlocal disabledelayedexpansion
        REM ECHO %pems[%%selectedPem%%]%

        set privatekey=!pems[%selectedPem%]!
        ECHO You chose %privatekey%
    )   
)`

输出:

Performing initial search of private and public keys...
PEM Files Found:
0) brandon.pem
Select a pem file to be used as your private key. Otherwise, press Q to not 
select one.
Enter a number or Q:0
0
brandon.pem
ECHO is off.
You chose

我知道输出 "ECHO is off." 是因为它将我的数组变量引用解释为空。 感谢您花时间阅读我的脚本,我可能过于复杂了。

使用一些 goto 可以避免大多数(代码块)。

在无法做到这一点的情况下,使用 ! 和另一种延迟扩展,并适当地加倍 %%

未测试:

@echo off
setlocal enabledelayedexpansion 
set dir=%~dp0
cd %dir%

ECHO Performing initial search of private and public keys...
set pemI=0
set keyI=0
for %%p in (*.pem) do (
    REM echo %%~nxp
    set pems[!pemI!]=%%~nxp
    REM call echo %%pems[!pemI!]%%
    set /a pemI+=1
)
REM echo %pems[0]%
set /a totalPems=%pemI%
REM This is the test selectedPem variable that showed me the variable works 
REM if I set it outside 
REM of the "if defined pems[0]" statement
REM set selectedPem=

if pemI==0 (Echo no *.pem files found & goto :End)

echo PEM Files Found:
for /l %%p in (0,1,%totalPems%) do (
    echo %%p^) !pems[%%p]!
)
ECHO Select a pem file to be used as your private key. Otherwise, press 
Q to not select one.
set /P selectedPem=Enter a number or Q:
IF /I "!selectedPem!"=="q" (ECHO Skipping private key selection.& goto :End

ECHO !selectedPem!
ECHO %pems[0]%
REM The below ECHO only works when the above selectedPem is set 
REM outside of the "IF defined" statement
ECHO !pems[%selectedPem%]!

REM Tried these other implementations:
REM ECHO !pems[!!selectedPem!!]!
REM ECHO %pems[!selectedPem!]%
REM setlocal disabledelayedexpansion
REM ECHO %pems[%%selectedPem%%]%

set privatekey=!pems[%selectedPem%]!
ECHO You chose %privatekey%
)   
:end
REM ddo wahatever
pause

您似乎很了解 delayed expansion,因为您大部分时间都在正确使用它。

但是,我通常将代码中的内容称为嵌套变量,因为批处理脚本中没有真正的数组,因为每个数组元素不过是一个单独的标量变量(pems[0]pems[1], pems[2], 等等);所以我们也可以将类似的东西称为伪数组。

无论如何,像 echo !pems[%selectedPem%]! 这样的东西可以正确扩展,除非它被放置在 selectedPem 之前更新的行或代码块中,因为那样我们需要延迟扩展 selectedPem 还有。 echo !pems[!selectedPem!]! 不起作用,因为它试图扩展变量 pems[],而 selectedPem 被解释为文字字符串。

在继续之前,让我们先回到echo !pems[%selectedPem%]!:为什么这是可扩展的?好吧,诀窍是我们这里有两个扩展阶段,正常或立即扩展 (%),然后是延迟扩展 (!)。重要的是顺序:嵌套变量的内部变量(因此是数组索引)必须在外部变量(因此是数组元素)之前被耗尽。像 echo %pems[!selectedPem!]% 这样的东西无法正确扩展,因为(很可能)没有名为 pems[!selectedPem!] 的变量,并且在将其扩展为空字符串后,两个 ! 消失,因此延迟扩展阶段从未见过他们。

现在让我们更进一步:要在同时更新 selectedPem 的行或代码块中扩展类似于 echo !pems[%selectedPem%]! 的内容,我们必须避免立即扩展。我们已经知道延迟展开不能嵌套,但是还有一些其他的展开:

  1. call command在延迟扩展后引入了另一个扩展阶段,再次处理%-signs;所以完整的序列是立即扩展、延迟扩展和另一个 %-扩展。忽略第一个,让我们以嵌套变量的内部变量先于外部变量展开的方式使用后两个,意思是:call echo %%pems[!selectedPem!]%%。为了跳过立即扩展阶段,我们只需将 % 符号加倍,每个符号都被一个文字替换,因此在立即扩展后我们有 echo %pems[!selectedPem!]% 。接下来就是延时扩容,那么说下%-扩容。
    您应该注意 call 方法非常慢,因此大量使用会大大降低脚本的整体性能。
  2. for loop 变量的扩展发生在立即扩展之后,延迟扩展之前。因此,让我们包装一个 for 循环,它只迭代一次 returns selectedPem 的值,就像这样:for %%Z in (!selectedPem!) do echo !pems[%%Z]!。这是有效的,因为 for 不会访问文件系统,除非出现通配符 *?,无论如何都不应该在变量名或(伪)数组索引中使用它们。
    除了标准的 for 循环之外,还可以使用 for /Ffor /F %%Z in ("!selectedPem!") do echo !pems[%%Z]!(不需要像 "delims=" 这样的选项字符串,以防 selectedPem 需要包含只是一个索引号)。
    也可以使用 for /L loop(当然是用于数字索引):for /L %%Z in (!selectedPem!,1,!selectedPem!) do echo !pems[%%Z]!.
  3. set /A command建立的隐式扩展,意味着%!都不是读取变量所必需的,也可以使用,但前提是数组元素包含数字值(注意 /A 代表算术)。由于这是 set /A 特有的功能,因此这种扩展发生在命令执行期间,而这又发生在延迟扩展之后。所以我们可以这样使用:set /A "pem=pems[!selectedPem!]" & echo !pem!.
  4. 为了完整起见,这里还有一种方法:set /A,当在cmd而不是批处理上下文中执行时,在控制台上输出(最后)结果;鉴于这构成了索引号,我们可以通过 for /F 捕获它并像这样扩展数组元素:for /F %%Z in ('set /A "selectedPem"') do echo !pems[%%Z]!set /Afor /F 在新的 cmd 实例中执行,因此这种方法当然不是最快的方法。

关于这个主题,有一篇非常全面的 post,您应该仔细阅读它的详细信息:Arrays, linked lists and other data structures in cmd.exe (batch) script

对于命令行和批处理脚本的解析以及一般的变量扩展,请参考这个很棒的线程:How does the Windows Command Interpreter (CMD.EXE) parse scripts?


现在让我们看看您的代码。有几个问题:

  • 使用 cd /D 而不是 cd 以便在必要时更改驱动器;顺便说一句,临时变量 dir 不是必需的(为了便于阅读,我建议不要使用等同于内部或外部命令的变量名),只需立即在路径上使用 cd
  • 您错过了在行 set pems[%pemI%]=%%~nxp 中使用延迟扩展,它应该显示为 set pems[!pemI!]=%%~nxp,因为 pemI 在周围的 for 循环中更新;
  • 你也需要在行 set /a pemI=%pemI%+1 中延迟扩展,或者你使用 set /A 的隐式变量扩展,所以 set /A pemI=pemI+1 可以工作;然而,这甚至可以更简化:set /A pemI+=1,这是完全等价的;
  • 我会使用不区分大小写的比较,尤其是在涉及用户输入时,例如您检查 Q,这将是 if /I "!selectedPem!"=="q"
  • 现在我们来到一行,需要我们在上面学到的东西:ECHO !pems[%selectedPem%]! 需要变成 call echo %%pems[!selectedPem!]%%;在评论中也插入了使用 for 的替代方法 (rem);
  • 然后还有一行同样的问题:set privatekey=!pems[%selectedPem%]!需要变成call set privatekey=%%pems[!selectedPem!]%%(或者也是基于for的方法);
  • 您又一次错过了使用延迟扩展的机会,这次是在 ECHO You chose %privatekey% 行,应该是 echo You chose !privatekey!;
  • 最后我改进了你代码的引用,特别是 set 命令的引用,最好像 set "VAR=Value" 这样写,这样任何不可见的尾随空格都不会成为值的一部分, 如果值本身包含特殊字符,则值本身会受到保护,但引号不会成为值的一部分,这可能会干扰特别是串联;

所以这是固定代码(删除了所有原始 rem 评论):

@echo off
setlocal EnableDelayedExpansion 
cd /D "%~dp0"

echo Performing initial search of private and public keys...
set "pemI=0"
set "keyI=0"
for %%p in (*.pem) do (
    set "pems[!pemI!]=%%~nxp"
    set /A "pemI+=1"
)
set /A "totalPems=pemI-1"
if defined pems[0] (
    echo PEM Files Found:
    for /L %%p in (0,1,%totalPems%) do (
        echo %%p^) !pems[%%p]!
    )
    set /P selectedPem="Enter a number or Q: "
    if /I "!selectedPem!"=="q" (
        echo Skipping private key selection.
    ) else (
        echo !selectedPem!
        echo %pems[0]%
        call echo %%pems[!selectedPem!]%%
        rem for %%Z in (!selectedPem!) do echo !pems[%%Z]!
        call set "privatekey=%%pems[!selectedPem!]%%"
        rem for %%Z in (!selectedPem!) do set "privatekey=!pems[%%Z]!"
        echo You chose !privatekey!
    )   
)

我不得不承认我没有详细检查你脚本的逻辑。