批处理函数参数中特殊字符的奇怪行为?

7

假设你运行 test.bat "blabla,blabla,^>blabla", "blaby"

test.bat 实现:

@SETLOCAL
@ECHO OFF
SET list=%~1
ECHO "LIST: %list%"
ECHO "ARG 1: %~1"
ECHO "ARG 2: %~2"
@ENDLOCAL   
@GOTO :EOF

输出结果与预期相符:

"LIST: blabla,blabla,>blabla"
"ARG 1: blabla,blabla,^>blabla"
"ARG 2: blaby"

但是如果你将test.bat作为批处理文件中的一个函数:

@SETLOCAL
CALL :TEST "blabla,blabla,^>blabla", "blaby"
@ENDLOCAL
@GOTO :EOF

:TEST
@SETLOCAL
@ECHO OFF
SET list=%~1
ECHO "LIST: %list%"
ECHO "ARG 1: %~1"
ECHO "ARG 2: %~2"
@ENDLOCAL   
@GOTO :EOF

运行后,输出如下:
"LIST: blabla,blabla,^"
"ARG 1: blabla,blabla,^^>blabla"
"ARG 2: blaby"

啥?

  1. blabla在LIST里去哪了?
  2. ARG 1有^^?为什么?

能有人解释一下函数参数和命令行参数中特殊字符的行为差异吗?

2个回答

10

通过简单地使用以下内容,您可以获得与第一批脚本相同的结果:

call test.bat "blabla,blabla,^>blabla", "blaby"

你遇到的问题源于批处理如何解析CALL语句的一个不幸特性,这在第6阶段中描述了:《Windows命令解释器(CMD.EXE)如何解析脚本?》

天啊 - 我以为我之前已经理解过连字符重复了,但显然没有。针对jeb的评论,我进行了大量编辑。

CMD.EXE的设计师希望像call echo ^^这样的语句与echo ^^产生相同的结果。两个语句都将^^在第2阶段缩减为^,其中处理了特殊字符。但是CALL语句必须再次经过第1和第2阶段。因此,在幕后,当CMD.EXE在第6阶段识别CALL语句时,会将剩余的插入符号加倍到^^,然后第二轮第2阶段将其缩减回到^。这两个语句都向屏幕上回显单个插入符号。

不幸的是,CMD.EXE盲目地加倍所有插入符,即使它们被引用也是如此。但是,引用的插入符不会被视为转义符,它是一个字面值。插入符不再被消耗。非常不幸。

在解析器的第6阶段中,运行call test.bat "blabla,blabla,^>blabla", "blaby"变成了call test.bat "blabla,blabla,^^>blabla" "blaby"

这很容易解释为什么你的输出中ARG 1看起来像它的样子。

至于blabla去哪里了? 这有点棘手。

当你的脚本执行SET list=%~1时,引号被移除,^^被视为转义插入符,缩减为^,而>则不再被转义。因此,你的SET语句的输出被重定向到一个名为“blabla”的文件中。当然,SET没有输出,所以你的硬盘上应该有一个长度为零的“blabla”文件。

编辑 - 如何使用“延迟扩展”正确传递所需的参数

davor在他的答案中尝试了在调用过程中反转插入符的效果。但这不可靠,因为你无法确定插入符可能被加倍的次数。最好让调用者进行调整来补偿调用。这很棘手 - 你必须使用jeb所称的“延迟扩展”。

在批处理脚本中,你可以定义一个包含所需参数字符串的变量,并延迟扩展直到加倍插入符,方法是用另一个%转义%。对于每个CALL语句,你都需要加倍百分比。

@echo off
setlocal
set arg1="blabla,blabla,^>blabla"
call :TEST %%arg1%% "blaby"
echo(
call call :TEST %%%%arg1%%%% "blaby"
::unquoted test
exit /b

:TEST
setlocal
set list=%~1
echo "LIST: %list%"
echo "ARG 1: %~1"
echo "ARG 2: %~2"
exit /b
以上代码产生了期望的结果:
"LIST: blabla,blabla,>blabla"
"ARG 1: blabla,blabla,^>blabla"
"ARG 2: blaby"

"LIST: blabla,blabla,>blabla"
"ARG 1: blabla,blabla,^>blabla"
"ARG 2: blaby"

当从命令行运行时,扩展规则会有所不同。在命令行中无法转义%。相反,您必须在百分号内添加一个脱字符,以防止扩展阶段在第一遍识别名称,然后在第二阶段去除脱字符时,第二次扩展变量。

以下使用davor的原始TEST.BAT。

C:\test>test.bat "blabla,blabla,^>blabla" "blaby"
"LIST: blabla,blabla,>blabla"
"ARG 1: blabla,blabla,^>blabla"
"ARG 2: blaby"

C:\test>set arg1="blabla,blabla,^>blabla"

C:\test>test.bat %arg1% "blaby"
"LIST: blabla,blabla,>blabla"
"ARG 1: blabla,blabla,^>blabla"
"ARG 2: blaby"

C:\test>call test.bat %^arg1% "blaby"
"LIST: blabla,blabla,>blabla"
"ARG 1: blabla,blabla,^>blabla"
"ARG 2: blaby"

C:\test>set arg2=%^arg1%

C:\test>call call test.bat %^arg2% "blaby"
"LIST: blabla,blabla,>blabla"
"ARG 1: blabla,blabla,^>blabla"
"ARG 2: blaby"

通过引用传递值,而非使用转义符号——一个替代方法!

总的来说,转义规则过于复杂。这就是为什么高级批处理脚本通常会通过引用传递字符串值,而不是作为字面量。期望的字符串被放在一个变量中,然后变量名称被作为参数传递。延迟扩展被用来获取确切的字符串,而不用担心由于特殊字符、CALL字符加倍或百分比剥离而导致的损坏。

这里是一个演示这个概念的简单test.bat文件

@echo off
setlocal enableDelayedExpansion
set "var1=!%~1!"
echo var1=!var1!

call :test var1
exit /b

:test
set "var2=!%~1!"
echo var2=!var2!

这里是演示它如何工作的示例。

C:\test>set complicatedString="This & that ^" ^& the other thing ^^ is 100% difficult to escape

C:\test>set complicatedString
complicatedString="This & that ^" & the other thing ^ is 100% difficult to escape

C:\test>test.bat complicatedString
var1="This & that ^" & the other thing ^ is 100% difficult to escape
var2="This & that ^" & the other thing ^ is 100% difficult to escape

C:\test>call test.bat complicatedString
var1="This & that ^" & the other thing ^ is 100% difficult to escape
var2="This & that ^" & the other thing ^ is 100% difficult to escape

C:\test>call call test.bat complicatedString
var1="This & that ^" & the other thing ^ is 100% difficult to escape
var2="This & that ^" & the other thing ^ is 100% difficult to escape

+1 即使“call echo this ^& that”的部分完全错误 :-) - jeb
2
在第二阶段,它不会回显任何内容,因为插入符被消耗了。所以后来就没有任何可以加倍的插入符了,因此只有一个裸的和完整的“call echo”失败了。在“call echo”中不能逃避任何东西,只能通过延迟扩展实现。 - jeb
@jeb - 谢谢 - 我想我已经修复了关于CMD.EXE为什么会将插入符号翻倍的讨论。又是一次可怕的微软设计。如果微软允许我们在使用CALL时根据需要明确地双重转义,那么生活将变得更加轻松。 - dbenham
感谢您提供的链接和解释,dbenham。我觉得这非常复杂,可能是因为我仍然没有掌握批处理的基础知识。难怪我在过去几天中遇到了这些参数传递方面的很多问题。 - Davor Josipovic
1
@dbenham,你提出了一些非常有用的技巧。可惜我只能点赞+1。谢谢。 - Davor Josipovic
@dbenham 万分感谢——通过使用按引用传递的解决方法,我的问题得到了解决! - Mark Berry

0
经过一些测试和dbenham的回答,似乎需要预先处理双插入符并将其替换为单插入符。
@SETLOCAL
CALL :TEST "blabla,blabla,^>blabla", "blaby"
@ENDLOCAL
@GOTO :EOF

:TEST
@SETLOCAL
@ECHO OFF
SET "list=%~1" & REM CHANGED
SET "list=%list:^^=^%" & REM ADDED
ECHO "LIST: %list%"
ECHO "ARG 1: %~1"
ECHO "ARG 2: %~2"
@ENDLOCAL   
@GOTO :EOF

输出结果如下:

"LIST: blabla,blabla,^>blabla"
"ARG 1: blabla,blabla,^^>blabla"
"ARG 2: blaby"

还要注意一件奇怪的事情: 在代码行 SET "list=%list:^^=^%" 中,%% 之间的 ^^ 被视为两个字符,而不是转义后的 ^

1
这个方法只有在你知道如何调用你的例程时才有效。如果使用 call call :test ... 进行调用,它将会出错。虽然很少见,但在某些罕见情况下是希望进行双重调用的。更麻烦的是你原来的 "TEST.BAT" 。它可以通过 TEST ... 或者 CALL TEST ... 进行执行。你的脚本无法确定是否需要修复插入符号。我将更新我的回答,展示如何通过修改 CALL 语句来使用 "延迟扩展" 技术来实现你期望的结果,这正是 jeb 密秘参考的方法。 - dbenham

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