首页 > 解决方案 > 在 IF 语句中对数组使用 DelayedExpansion 索引变量失败

问题描述

我有一个批处理文件,我在其中显示目录中的所有 *.pem 文件,让用户可以选择一个,此时只是试图通过使用他们选择的数字来向用户显示他们选择的文件 ECHO数组的内容。我相信不知何故,因为它在 IF 语句内部,索引变量“selectedPem”没有在数组的索引括号中扩展,因为它使用 % 而不是使用延迟扩展!,这似乎在数组的索引中不起作用括号。我认为这是因为如果我在 IF 语句之前设置“selectedPem”变量,它会正确 ECHO。我认为这个工作的唯一选择是以某种方式使用子程序。为什么 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 已关闭”。而是输出,因为它将我的数组变量引用解释为空。感谢您花时间阅读我的脚本,我可能过于复杂了。

标签: batch-fileif-statementdelayedvariableexpansion

解决方案


您似乎很清楚延迟扩展,因为您大部分时间都在正确使用它。

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

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

在继续之前,让我们先回到echo !pems[%selectedPem%]!:为什么这是可扩展的?好吧,诀窍是我们在这里有两个扩展阶段,正常或立即扩展 ( %),然后是延迟扩展 ( !)。重要的是顺序:嵌套变量的内部之一(因此数组索引)必须在外部变量(因此数组元素)之前展开。echo %pems[!selectedPem!]%无法正确扩展类似的东西,因为(很可能)没有变量 named pems[!selectedPem!],并且在将其扩展为空字符串后,两者!消失了,因此延迟扩展阶段永远不会看到它们。

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

  1. call命令在延迟扩展之后引入了另一个扩展阶段,该阶段再次处理%-signs;所以完整的顺序是立即扩展,延迟扩展和另一个%扩展。忽略第一个,让我们利用后两个嵌套变量的内部变量在外部变量之前扩展,意思是:call echo %%pems[!selectedPem!]%%. 要跳过立即扩展阶段,我们只需将符号加倍,每个%符号都替换为一个字面值,因此我们echo %pems[!selectedPem!]%在立即扩展之后。下一步是延迟扩展,然后是所说的下一个%扩展。
    您应该注意该call方法非常慢,因此大量使用可能会大大降低脚本的整体性能。
  2. for循环变量的扩展发生在立即扩展之后,但在延迟扩展之前。所以让我们包裹一个for循环,它只迭代一次并返回 的值selectedPem,如下所示for %%Z in (!selectedPem!) do echo !pems[%%Z]!:这是有效的,因为for除非出现通配符*?出现,否则不会访问文件系统,无论如何都不应该在变量名或(伪)数组索引中使用它。
    代替标准for循环,for /F也可以使用:(for /F %%Z in ("!selectedPem!") do echo !pems[%%Z]!没有选项字符串"delims=",如果selectedPem预计只包含一个索引号)。也可以使用循环(当然,对于数字索引
    for /Lfor /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 /A在新cmd实例中由执行for /F,所以这种方法当然不是最快的。

有一篇关于这个主题的精彩而全面的帖子,您应该详细阅读它:cmd.exe (batch) script 中的数组、链表和其他数据结构

对于命令行和批处理脚本的解析以及一般的变量扩展,请参阅这个很棒的线程:Windows 命令解释器(CMD.EXE)如何解析脚本?


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

  • 如有必要,使用cd /D代替cd来更改驱动器;顺便说一句,临时变量dir不是必需的(为了便于阅读,我建议不要使用与内部或外部命令相同的变量名),只需cd立即在路径上使用即可;
  • 您错过了在 line 中使用延迟扩展set pems[%pemI%]=%%~nxp,它应该读set pems[!pemI!]=%%~nxppemI在周围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);
  • 你又一次错过了使用延迟扩展,这次是在 lineECHO 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!
    )   
)

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


推荐阅读