3

我有一个接受可变数量参数的函数。每个参数都是函数将直接修改的引用。这是我调用该函数的方式:

set "A=" && set "B=" && set "C="
call :variadic_func A B C
echo [%A%][%B%][%C%]

goto :eof

如果我不使用 setlocal 来限制变量范围,则该函数可以正常工作。该函数创建引用 X、Y 和 Z 并为它们分配 1、2 和 3。当函数返回时,调用者看到它的变量 A、B 和 C 分别为 1、2 和 3。很好。假设这是一个可变参数函数,它会计算出它在运行时有多少个参数。

:variadic_func
set "x=%1" && set "y=%2" && set "z=%3"
set "%x%=1" && set "%y%=2" && set "%z%=3"
goto :eof

输出:

C:\scratch\variadic_batch>variadic.bat
[1][2][3]

但我想用setlocal. 所以这意味着我写给 X、Y 和 Z 的任何值都会在endlocal. 如何从函数中获取值?

:variadic_func
setlocal
set "x=%1" && set "y=%2" && set "z=%3"
set "%x%=1" && set "%y%=2" && set "%z%=3"

endlocal && (
    call set "%x%=%%%%x%%%%"
    call set "%y%=%%%%y%%%%"
    call set "%z%=%%%%z%%%%"
)

goto :eof

不幸的是,调用上下文接收值%x%%y%%z%。我以为上面的代码会这样展开: 1.%x%先展开得到call set A=%%A%%. 然后调用被执行,它会评估A=%A%. 但我最终只是将文本分配给%A%变量 A 而不是对其进行评估。

C:\scratch\variadic_batch>variadic.bat
[%x%][%y%][%z%]

为什么它不像我预期的那样工作,我该如何解决?

(我只是想setlocal EnableDelayedExpansion在函数调用之前做一个,所以当我在函数中执行 endlocal 时,延迟扩展可能仍然可用,但即使这样可行,如果函数不依赖调用者,那就太好了在延迟扩展块中......我什至不知道延迟扩展块是否堆叠)

4

3 回答 3

3

非常有趣的问题,我很惊讶它是多么容易解决:-)

编辑 - 正如 Aacini 在他的评论中指出的那样,我的原始答案并没有完全回答这个问题。在底部,我有一个可以直接回答问题的版本。我还更新了我的原始答案,以包含我发现的更多限制

如果您规定要返回的所有变量的名称都以常量前缀开头,则可以非常轻松地返回任意数量的变量。返回变量前缀可以作为参数之一传入。

只需以下行:
for /f "delims=" %%A in ('set prefix.') do endlocal & set "%%A"

set prefix在任何迭代发生之前,命令的全部结果都会被缓冲。第一次迭代执行返回到 CALL 之前存在的环境状态所需的唯一 ENDLOCAL。随后的 ENDLOCAL 迭代不会造成任何损害,因为 CALLed 函数中的 ENDLOCAL 仅适用于在 CALL 中发出的 SETLOCAL。额外的冗余 ENDLOCAL 将被忽略。

这个非常简单的解决方案有一些非常好的功能:

  • 理论上返回的变量数量没有限制。
  • 返回的值可以包含几乎任何字符组合。
  • 返回值可以接近 8191 字节的理论最大长度。

还有一些限制:

  • 返回值不能包含换行符
  • 如果返回值的最后一个字符是回车,那么最后的回车将被删除。
  • 如果在进行 CALL 时启用了延迟扩展,则包含的任何返回值都!将被破坏。
  • 我还没有想出一个优雅的方法来将返回的变量设置为未定义。

这是一个返回可变数量值的可变参数函数的简单示例

@echo off
setlocal
set varBeforeCall=ok

echo(
call :variadic callA 10 -34 26
set callA
set varBeforeCall

echo(
call :variadic callB 1 2 5 10 50 100
set callB
set varBeforeCall

exit /b


:variadic  returnPrefix  arg1  [arg2 ...]
  @echo off
  setlocal enableDelayedExpansion
  set /a const=!random!%%100

  :: Clear any existing returnPrefix variables
  for /f "delims==" %%A in ('set %1. 2^>nul') do set "%%A="

  :: Define the variables to be returned
  set "%~1.cnt=0"
  :argLoop
  if "%~2" neq "" (
    set /a "%~1.cnt+=1"
    set /a "%~1.!%~1.cnt!=%2*const"
    shift /2
    goto :argLoop
  )

  :: Return the variables accross the ENDLOCAL barrier
  for /f "delims=" %%A in ('set %1. 2^>nul') do endlocal & set "%%A"
exit /b

这是一个示例运行结果:

callA.1=40
callA.2=-136
callA.3=104
callA.cnt=3
varBeforeCall=ok

callB.1=24
callB.2=48
callB.3=120
callB.4=240
callB.5=1200
callB.6=2400
callB.cnt=6
varBeforeCall=ok

这是启用延迟扩展时可以安全调用的版本

通过一些额外的代码,可以消除在启用延迟扩展并且返回值包含时调用函数的限制!

返回值会根据需要进行操作,以!在启用延迟扩展时进行保护。代码经过优化,使得相对昂贵的微型计算(特别是 CALL)仅在启用延迟扩展且值包含时才执行!

返回的值仍然不能包含换行符。!一个新的限制是,如果返回值包含并且在进行 CALL 时启用了延迟扩展,则所有回车将被删除。

这是一个演示。

@echo off
setlocal
set varBeforeCall=ok

echo(
echo Results when delayed expansion is Disabled
call :variadic callA 10 -34 26
set callA
set varBeforeCall

setlocal enableDelayedExpansion
echo(
echo Results when delayed expansion is Enabled
call :variadic callB 1 2 5 10 50 100
set callB
set varBeforeCall

exit /b


:variadic  returnPrefix  arg1  [arg2 ...]
  @echo off

  :: Determine if caller has delayed expansion enabled
  setlocal
  set "NotDelayed=!"

  setlocal enableDelayedExpansion
  set /a const=!random!%%100

  :: Clear any existing returnPrefix variables
  for /f "delims==" %%A in ('set %1. 2^>nul') do set "%%A="

  :: Define the variables to be returned
  set "%~1.cnt=0"
  :argLoop
  if "%~2" neq "" (
    set /a "%~1.cnt+=1"
    set /a "%~1.!%~1.cnt!=%2*const"
    shift /2
    goto :argLoop
  )
  set %~1.trouble1="!const!\^^&^!%%"\^^^^^&^^!%%
  set %~1.trouble2="!const!\^^&%%"\^^^^^&%%

  :: Prepare variables for return when caller has delayed expansion enabled
  if not defined NotDelayed for /f "delims==" %%A in ('set %1. 2^>nul') do (
    for /f delims^=^ eol^= %%V in ("!%%A!") do if "%%V" neq "!%%A!" (
      set "%%A=!%%A:\=\s!"
      set "%%A=!%%A:%%=\p!"
      set "%%A=!%%A:"=\q!"
      set "%%A=!%%A:^=\c!"
      call set "%%A=%%%%A:^!=^^^!%%" ^^!
      set "%%A=!%%A:^^=^!"
      set "%%A=!%%A:\c=^^!"
      set "%%A=!%%A:\q="!"
      set "%%A=!%%A:\p=%%!"
      set "%%A=!%%A:\s=\!"
    )
  )

  :: Return the variables accross the ENDLOCAL barrier
  for /f "delims=" %%A in ('set %1. 2^>nul') do endlocal & endlocal & set "%%A"
exit /b

以及一些示例结果:

Results when delayed expansion is Disabled
Environment variable callA not defined
callA.1=780
callA.2=-2652
callA.3=2028
callA.cnt=3
callA.trouble1="78\^&!%"\^&!%
callA.trouble2="78\^&%"\^&%
varBeforeCall=ok

Results when delayed expansion is Enabled
Environment variable callB not defined
callB.1=48
callB.2=96
callB.3=240
callB.4=480
callB.5=2400
callB.6=4800
callB.cnt=6
callB.trouble1="48\^&!%"\^&!%
callB.trouble2="48\^&%"\^&%
varBeforeCall=ok

请注意,无论在进行 CALL 时是否启用了延迟扩展,返回的故障值的格式都是一致的。如果不是因为!.

编辑:这是一个直接回答问题的版本

原始问题规定每个返回变量的名称都应该在参数列表中提供。我修改了我的算法,在函数中为每个变量名加上一个点。然后我稍微修改了最后的返回 FOR 语句以去除前导点。有一个限制,返回的变量的名称不能以点开头。

此版本包括安全返回技术,允许在启用延迟扩展时调用。

@echo off
setlocal disableDelayedExpansion

echo(
set $A=before
set $varBeforeCall=ok
echo ($) Values before CALL:
set $
echo(
echo ($) Values after CALL when delayed expansion is Disabled:
call :variadic $A $B
set $

setlocal enableDelayedExpansion
echo(
set #A=before
set #varBeforeCall=ok
echo (#) Values before CALL:
set #
echo(
echo (#) Values after CALL when delayed expansion is Enabled:
call :variadic #A #B #C
set #

exit /b


:variadic  arg1  [arg2 ...]
  @echo off

  :: Determine if caller has delayed expansion enabled
  setlocal
  set "NotDelayed=!"

  setlocal enableDelayedExpansion

  :: Clear any existing . variables
  for /f "delims==" %%A in ('set . 2^>nul') do set "%%A="

  :: Define the variables to be returned
  :argLoop
  if "%~1" neq "" (
    set /a num=!random!%%10
    set ^".%~1="!num!\^^&^!%%"\^^^^^&^^!%%"
    shift /1
    goto :argLoop
  )

  :: Prepare variables for return when caller has delayed expansion enabled
  if not defined NotDelayed for /f "delims==" %%A in ('set . 2^>nul') do (
    for /f delims^=^ eol^= %%V in ("!%%A!") do if "%%V" neq "!%%A!" (
      set "%%A=!%%A:\=\s!"
      set "%%A=!%%A:%%=\p!"
      set "%%A=!%%A:"=\q!"
      set "%%A=!%%A:^=\c!"
      call set "%%A=%%%%A:^!=^^^!%%" ^^!
      set "%%A=!%%A:^^=^!"
      set "%%A=!%%A:\c=^^!"
      set "%%A=!%%A:\q="!"
      set "%%A=!%%A:\p=%%!"
      set "%%A=!%%A:\s=\!"
    )
  )

  :: Return the variables accross the ENDLOCAL barrier
  for /f "tokens=* delims=." %%A in ('set . 2^>nul') do endlocal & endlocal & set "%%A"
exit /b

和样本结果:

($) Values before CALL:
$A=before
$varBeforeCall=ok

($) Values after CALL when delayed expansion is Disabled:
$A="5\^&!%"\^&!%
$B="5\^&!%"\^&!%
$varBeforeCall=ok

(#) Values before CALL:
#A=before
#varBeforeCall=ok

(#) Values after CALL when delayed expansion is Enabled:
#A="7\^&!%"\^&!%
#B="2\^&!%"\^&!%
#C="0\^&!%"\^&!%
#varBeforeCall=ok
于 2013-01-08T23:34:53.080 回答
3

这是一个有趣的话题!如果您事先知道该函数将获得多少个变量,您可以在末尾组装适当的行以通过这种方式将值返回给调用者的环境:

:variadic_func
setlocal EnableDelayedExpansion
set "x=%1" & set "y=%2" & set "z=%3"
set "%x%=1" & set "%y%=2" & set "%z%=3"
for /F "tokens=1-3" %%a in ("!%x%! !%y%! !%z%!") do (
   endlocal
   set "%x%=%%a" & set "%y%=%%b" & set "%z%=%%c"
)
exit /B

但是,如果变量个数未知,则不能使用以前的方法。

(我曾经exit /B终止子程序并且goto :EOF仅用于主文件)

无论如何,您的示例并不精确,因为如果您不知道有多少变量,则不能使用固定名称作为“x”、“y”或“z”。处理这种情况的唯一方法是将名称存储在数组中,然后处理数组元素。

这样,在函数结束之前,我们可以组装一个“var=value”对列表,这些对将在 endlocal 之后的 FOR 中执行,因此变量将在调用者的环境中定义:

@echo off

call :variadic_func One Two Three
echo THREE VARS: One=[%One%] Two=[%Two%] Three=[%Three%] Four=[%Four%] Five=[%Five%]
call :variadic_func One Two Three Four Five
echo FIVE VARS:  One=[%One%] Two=[%Two%] Three=[%Three%] Four=[%Four%] Five=[%Five%]
goto :EOF

:variadic_func
setlocal EnableDelayedExpansion
rem Collect the list of variable names in "var" array:
set i=0
:nextVar
   if "%1" equ "" goto endVars
   set /A i+=1
   set var[%i%]=%1
   shift
goto nextVar
:endVars
rem Assign random numbers to the variables (for example):
for /L %%i in (1,1,%i%) do (
   set !var[%%i]!=!random!
)
rem Assemble the list of "var=value" assignments that will be executed at end:
set assignments=
for /L %%i in (1,1,%i%) do (
   for %%v in (!var[%%i]!) do (
      set assignments=!assignments! "%%v=!%%v!"
   )
)
rem Execute the list of variable assignments in the caller's environment:
endlocal & for %%a in (%assignments%) do set %%a
exit /B

输出:

THREE VARS: One=[29407] Two=[21271] Three=[5873] Four=[] Five=[]
FIVE VARS:  One=[30415] Two=[2595] Three=[22479] Four=[13956] Five=[26412]

编辑:

我从 dbenham 的解决方案中借用了该方法来返回任意数量的变量,没有任何限制,除了他指出的那些。这是新版本:

@echo off

call :variadic_func One Two Three
echo THREE VARS: One=[%One%] Two=[%Two%] Three=[%Three%] Four=[%Four%] Five=[%Five%]
call :variadic_func One Two Three Four Five
echo FIVE VARS:  One=[%One%] Two=[%Two%] Three=[%Three%] Four=[%Four%] Five=[%Five%]
goto :EOF

:variadic_func
setlocal EnableDelayedExpansion
rem Assemble the list of variable names in "var" array:
set i=0
:nextVar
   if "%1" equ "" goto endVars
   set /A i+=1
   set var[%i%]=%1
   shift
goto nextVar
:endVars
rem Assign random numbers to the variables (for example):
for /L %%i in (1,1,%i%) do (
   set !var[%%i]!=!random!
)
rem Complete "var[i]=name" array contents to "var[i]=name=value"
for /L %%i in (1,1,%i%) do (
   for %%v in (!var[%%i]!) do (
      set "var[%%i]=%%v=!%%v!"
   )
)
rem Execute the list of variable assignments in the caller's environment:
for /F "tokens=1* delims==" %%a in ('set var[') do endlocal & set "%%b"
exit /B

安东尼奥

于 2013-01-08T22:24:15.527 回答
0

从值中删除一对百分比。 call set "%x%=%%%%x%%%%"进入call set "%x%=%%%x%%%"

目前评估如下:

:: Here is the base command
call set "%x%=%%%%x%%%%"
:: The single percents are evaluated and the doubles are escaped.
set "A=%%x%%"
:: The doubles are escaped again leaving literal % signs
"A=%x%"

你想要如下:

:: Here is the base command
call set "%x%=%%%x%%%"
:: The single percents are evaluated and the doubles are escaped.
set "A=%A%"
:: The single percents are evaluated.
"A=1"

使用该命令进行变量扩展时,由于批处理转义,首先计算call单个百分比%,然后是双百分比秒。%%

批处理命令从左到右读取。因此,当有偶数个%符号时%%%%,第一个和第三个百分号将用作第二个和第四个的转义字符,没有留下任何百分号用于变量评估。

于 2013-01-08T20:26:13.130 回答