如何安全地回显FOR变量%%〜p,后跟字符串文字

时间:2019-05-21 12:01:50

标签: batch-file for-loop cmd

我有一个由%%p命令创建的变量for /f 当我尝试将其与一些其他引用一起使用时,例如:%%~dp,然后在其后写一些文本,它将访问另一个变量

set var="%%~dpabc.txt"

代码输出

%%~dpa instead of %%~dp

3 个答案:

答案 0 :(得分:4)

因此,您必须对多个令牌使用FOR / F,例如

for /f "tokens=1-16" %%a in (file) do echo %%~dpabc.txt

或者您的代码可能具有嵌套的FOR循环。像

for %%a in (something) do (
  for %%p in (somethingelse) do (
    echo %%~dpabc.txt
  )
)

甚至是类似的东西

for %%a in (something) do call :sub
exit /b

:sub
for %%p in (somethingelse) do echo %%~dpabc.txt
exit /b

以上所有三个代码示例将打印出%%~dpa的驱动器和路径,后跟“ bc.txt”。根据文档,FOR变量是全局变量,因此子例程FOR循环的DO子句可以访问%%a%%p

Aschipfl does a good job documenting the rules for how modifiers and variable letters are parsed

每当在字符串文字之前使用FOR变量时,都必须格外小心,不能将字符串文字解释为FOR变量扩展的一部分。从您的示例可以看出,这可能很困难。使文字动态化,问题就更糟了。

set /p "myFile=Enter a file name: "
for %%a in (something) do (
  for %%p in (somethingelse) do (
    echo %%~dp%myFile%
  )
)

如果用户输入“ abc.txt”,那么我们就回到了起点。但是看一下代码,您可能会遇到潜在问题并不明显。

正如Gerhard和Mofi所说,如果使用的字符不能被解释为修饰符,则可以确保安全。但这并不总是那么容易,尤其是在使用FOR / F返回多个令牌的情况下。

有解决方案!

1)使用!!停止FOR变量解析并延迟扩展

如果您查看rules for how cmd.exe parses scripts,您会发现在阶段5发生延迟扩展之前,阶段4已扩展了FOR变量。这为使用!!作为目标的硬停止提供了机会。 FOR扩展,前提是启用了延迟扩展。

setlocal enableDelayedExpansion
for %%a in (something) do (
  for %%p in (somethingelse) do (
    echo %%~dp!!abc.txt
  )
)

%%~dp在阶段4中正确展开,然后在阶段5 !!中扩展为空,产生了所需的驱动器号,后跟“ abc.txt”。

但这不能解决所有情况。 !可以用作FOR变量,但这应该很容易避免,除非在极端情况下。

更麻烦的是必须启用延迟扩展。在这里这不是问题,但是如果FOR变量扩展为包含!的字符串,则该字符将通过延迟扩展来解析,并且结果很可能会弄乱。

因此,只有在您知道FOR变量值不包含!!的情况下,!延迟扩展黑客才能安全使用。

2)使用中间环境变量

避免所有情况下出现问题的唯一简单的万无一失的方法是将FOR变量的值转移到中间环境变量,然后切换延迟的扩展并使用整个所需的字符串。

for %%a in (something) do (
  for %%p in (somethingelse) do (
    set "drive=%%~dp"
    setlocal enableDelayedExpansion
    echo !drive!abc.txt
    endlocal
  )
)

3)通过环境变量使用Unicode字符

有一个复杂的防弹解决方案,但是在理解其工作原理之前,它需要大量的背景信息。

cmd.exe命令处理器内部将所有字符串表示为Unicode,环境变量也表示为Unicode-可以使用0x00以外的任何Unicode代码点。这也适用于FOR可变字符。 FOR可变字符的顺序基于Unicode代码点的数字值。

但是从批处理脚本或在命令提示符下键入的cmd.exe代码仅限于活动代码页支持的字符。这似乎是一个死胡同-如果您无法使用代码访问Unicode字符,这有什么用呢?

有一个简单但不直观的解决方案:cmd.exe可以使用包含活动代码页之外的Unicode值的预定义环境变量值!

所有FOR变量修饰符都是前128个Unicode代码点内的ASCII字符。因此,如果您定义名为$ 1到$ n的变量以包含从代码点256(0x100)开始的连续范围的Unicode字符,那么可以确保您的FOR变量永远不会与修饰符混淆。

因此,如果$ 1包含代码点0x100,则将FOR变量称为%%%$1%。而且,您可以自由使用`%%〜dp%$ 1%之类的修饰符。

此策略的另一个好处是,在解析带有“ tokens = 1-30”之类的令牌范围时,相对容易跟踪FOR变量,因为变量名称本质上是顺序的。活动代码页字符排序通常与Unicode代码点的顺序不匹配,除非您使用Unicode变量hack,否则很难访问所有30个令牌。

现在使用Unicode代码点定义$ n变量并不是一件容易的事。幸运的是,它已经完成了:-)下面的代码演示了如何定义和使用$ n变量。

@echo off
setlocal disableDelayedExpansion
call :defineForChars 1
for /f "tokens=1-16" %%%$1% in (file) do echo %%~d%$16%abc.txt
exit /b

:defineForChars  Count
::
:: Defines variables to be used as FOR /F tokens, from $1 to $n, where n = Count*256
:: Also defines $max = Count*256.
:: No other variables are defined or tampered with.
::
:: Once defined, the variables are very useful for parsing lines with many tokens, as
:: the values are guaranteed to be contiguous within the FOR /F mapping scheme.
::
:: For example, you can use $1 as a FOR variable by using %%%$1%.
::
::   FOR /F "TOKENS=1-31" %%%$1% IN (....) DO ...
::
::      %%%$1% = token 1, %%%$2% = token 2, ... %%%$31% = token 31
::
:: This routine never uses SETLOCAL, and works regardless whether delayed expansion
:: is enabled or disabled.
::
:: Three temporary files are created and deleted in the %TEMP% folder, and the active
:: code page is temporarily set to 65001, and then restored to the starting value
:: before returning. Once defined, the $n variables can be used with any code page.
::
for /f "tokens=2 delims=:." %%P in ('chcp') do call :DefineForCharsInternal %1
exit /b
:defineForCharsInternal
set /a $max=%1*256
>"%temp%\forVariables.%~1.hex.txt" (
  echo FF FE
  for %%H in (
    "0 1 2 3 4 5 6 7 8 9 A B C D E F"
  ) do for /l %%N in (1 1 %~1) do for %%A in (%%~H) do for %%B in (%%~H) do (
    echo %%A%%B 0%%N 0D 00 0A 00
  )
)
>nul certutil.exe -decodehex -f "%temp%\forVariables.%~1.hex.txt" "%temp%\forVariables.%~1.utf-16le.bom.txt"
>nul chcp 65001
>"%temp%\forVariables.%~1.utf8.txt" type "%temp%\forVariables.%~1.utf-16le.bom.txt"
<"%temp%\forVariables.%~1.utf8.txt" (for /l %%N in (1 1 %$max%) do set /p "$%%N=")
for %%. in (dummy) do >nul chcp %%P  
del "%temp%\forVariables.%~1.*.txt"
exit /b

:defineForChars例程是在DosTips上开发的,是easily access many tokens with a FOR /F statement更大的团队努力的一部分。

该线程的以下帖子介绍了:defineForChars例程和变体:

答案 1 :(得分:3)

此行为是由for变量引用及其~修饰符的解析的贪婪性质引起的。鉴于已经检测到前面的% / %%符号,基本上遵循这些规则:

  • 检查下一个字符是否为~;如果是,则:
    • 在不区分大小写的集合fdpnxsatz中尽可能多地使用以下字符(每个字符甚至多次),这些字符位于定义for变量引用或{{1}的字符之前}-标志;如果遇到这样的$符号,则:
      • 扫描$;如果找到,则:
        • 如果:之后有一个字符,请将其用作:变量引用并按预期方式扩展,除非未定义,否则不要扩展;
        • 如果for是最后一个字符,则 :将崩溃!
      • 否则(未找到cmd.exe)不会展开任何内容;
    • 否则(如果未遇到:-符号)使用所有修饰符扩展$变量;
  • 否则(如果未找到for)将下一个字符用作~变量引用并扩展,除非未定义该变量,或者甚至没有跟随的字符,则不要扩展;

答案 2 :(得分:1)

正如for meta-variable parsing rules中已经解释的那样,~修饰符检测以贪婪的方式发生。但是您可以停止通过另一个for元变量(最终会扩展为空)或jebcomment中建议的~$修饰符进行解析,甚至不需要另一个for元变量,因此可以使用任何现有的元变量:

rem // Using `%%~#` will expand to an empty string (note that `#` is not a valid `~`-modifier):
for %%# in ("") do (
    rem // Establish a `for`-loop that defines meta-variables `%%a` to `%%p`:
    for /F "tokens=1-16" %%a in ("1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16") do (
        rem /* Since `d`, `p` and `a` are all valid `~`-modifiers and a `for` meta-variable
        rem    `%%b` exists while `b` is not a valid `~`-modifier, `%%~dpab` is expanded: */
        echo(%%~dpabc.txt
        rem /* `for` meta-variable parsing is stopped after `%%~dp`, because the following `%`
        rem    is not a valid `~`-modifier, and neither exists a `for` meta-variable named `%`;
        rem    `%%~#` is expanded to an empty sting then (parsing surely stops at `#`): */
        echo(%%~dp%%~#abc.txt
        rem /* The following does not even require a particular `for` meta-variable like `%%#`,
        rem    it just uses the existing one `%%p` with the `~$`-modifier that specifies an
        rem    environment variable; since there is no variable name in between `$` and `:`,
        rem    there is certainly no such variable (since they must have names), hence `$~%:p`
        rem    expands to an empty string; note that `~$` is always the very last modifier: */
        echo(%%~dp%%~$:pabc.txt
    )
)

请注意,如果存在名为for的{​​{1}}元变量(这种情况不太常见,但有可能),这种方法将失败。