在 FOR 循环中使用复杂的调用检索 %ERRORLEVEL%

Retrieve %ERRORLEVEL% with complicated call in FOR loop

在一个 Windows 批处理文件中,我需要将程序执行的结果检索到一个变量中,当程序调用因空格和多个选项而变得复杂时。经过多次讨论,通过使用 CALL:

找到了解决方法
FOR /F "delims=" %%G IN ('CALL "C:\path with spaces\foo.bat" "blah blah='foobar' blah"') do set foo=%%G

请参阅以下问题了解更多详情并了解上下文:

实际上批处理文件调用 PostgreSQL 9.3,如下所示:

SET PSQL_EXE=C:\Program Files\Foo\Bar\PostgreSQL.3\bin\psql.exe
SET FOO=1
REM psql query should result in a 0 or 1 based on the mydbproperty table value
FOR /F %%G IN ('call "%PSQL_EXE%" -U pguser -d MyDB -p %PG_PORT% -c "select string_value from mydb.uri as uri, mydb.property as prop where uri.id = prop.property_uriid and uri.namespace_uri='http://example.com/foo/bar/' and uri.simplename = 'fooBar'" -q -t -w') DO SET FOO=%%G
REM next line doesn't have ERRORLEVEL set
IF !ERRORLEVEL! NEQ 0 EXIT !ERRORLEVEL!

不幸的是,这种格式似乎会产生一个单独的 cmd 实例,因此调用 pgsql 时发生的任何错误(例如缺少密码文件)都不会被传回并且 %ERRORLEVEL% 没有得到设置。具体pgsql打印出来:

psql: fe_sendauth: no password supplied

然而 %ERRORLEVEL%(和 !ERRORLEVEL!)仍然是 0

有关详细信息,请参阅以下问题:

所以现在的问题是如何找出 %ERRORLEVEL%,既然我已经成功获得了 psql 的响应。我不想写一些临时文件---我想在内存中做所有事情。

(请注意,是的,我尝试从数据库中查询并存储在 FOO 中的值将是 01;不是这样很重要,但它确实让事情变得更加混乱。)

我试图通过简单地查看 return 在 %%G 中编辑的内容来测试 Aacini 提出的解决方案:

FOR /F "delims=" %%G IN ('"CMD /V:ON /C CALL "%PSQL_EXE%" -U user -d MyDB -p %PG_PORT% -c "select string_value from mydb.uri as uri, mydb.database_property as prop where uri.id = prop.property_uriid and uri.namespace_uri='http://example.com/foo/bar/' and uri.simplename = 'fooBar'" -q -t -w ^& ECHO !ERRORLEVEL!"') DO (
  ECHO %%G
)

因为 psql 找不到密码文件,这会向 stderr 打印一个错误(如预期的那样)并且 ECHO 输出 0--- 所以 ERRORLEVEL 没有得到发送回 DO 子句。但是,如果我从 FOR 循环中分离出命令并直接 运行 它,它会显示 ERRORLEVEL (2) 就好了!

"%PSQL_EXE%" -U user -d MyDB -p %PG_PORT% -c "select string_value from mydb.uri as uri, mydb.database_property as prop where uri.id = prop.property_uriid and uri.namespace_uri='http://example.com/foo/bar/' and uri.simplename = 'fooBar'" -q -t -w
ECHO !ERRORLEVEL!

更新:在被告知如果我已经打开了延迟扩展我需要逃避转义后,我接受了给定的答案并更新了它以考虑到我正在调用的程序不会 return 一个值,如果它产生错误。所以这就是我所做的;首先我确保两个变量未定义:

SET "var1="
SET "var2="

然后我使用@Aacini 和@jeb 给出的循环,但在循环中我这样做:

if NOT DEFINED var1 (
  SET "var1=%%G"
) else (
  SET "var2=%%G"
)

然后在完成后在循环外:

if DEFINED var2 (
  ECHO received value: !var1!
  ECHO ERRORLEVEL: !var2!
) ELSE (
  ECHO ERRORLEVEL: !var1!
)

这似乎行得通。 (难以置信---什么体操!)

为了做到这一点,必须结合一些技巧。放置在 for /F 集合中的批处理文件在新的 cmd.exe 上下文中执行,因此我们需要一种方法来在此类命令结束时报告其错误级别。唯一的方法是首先通过显式放置的 cmd.exe /C 执行所需的批处理文件,然后 在它之后 插入一个报告其错误级别的命令。但是放在for /F集合中的命令是在命令行上下文中执行的,而不是在Batch文件上下文中执行的,所以需要启用延迟扩展(/V:ON 开关)在显式 cmd.exe 中。之后,一个简单的 echo !errorlevel! 显示批处理文件 return 的错误级别。

@echo off
setlocal

set "foo="
FOR /F "delims=" %%G IN ('"CMD /V:ON /C CALL "path with spaces\foo.bat" "blah blah='foobar' blah" ^& echo !errorlevel!"') do (
   if not defined foo (
      set "foo=%%G"
   ) else (
      set "errlevel=%%G"
   )
)

echo String output from foo.bat: "%foo%"
echo Errorlevel returned by foo.bat: %errlevel%

这是"path with spaces\foo.bat":

@echo off
echo String from foo.bat
exit /B 12345

这是前面代码的输出:

String output from foo.bat: "String from foo.bat"
Errorlevel returned by foo.bat: 12345

如果在主批处理文件中已经启用延迟扩展,则必须使用完全不同的语法。
^!ERRORLEVEL^! 这个要避免,那个!ERRORLEVEL!在执行 FOR 行之前,在批处理文件的上下文中进行评估。
^^^& 是将单个 ^& 带到内部 cmd /V:ON 表达式

所必需的
@echo off
setlocal EnableDelayedExpansion

set "foo="
REM *** More carets must be used in the next line ****
FOR /F "delims=" %%G IN ('"CMD /V:ON /C CALL "path with spaces\foo.bat" "blah blah='foobar' blah" ^^^& echo ^!errorlevel^!"') do (
   if not defined foo (
      set "foo=%%G"
   ) else (
      set "errlevel=%%G"
   )
)

编辑回复 OP 的评论

我的第一个解决方案假设程序总是输出一行,所以它从第二个输出行获取错​​误级别。但是,新规范表示当程序因错误而结束时,不会发送第一行输出。这个细节很容易修复,更正后的代码如下。

@echo off
setlocal EnableDelayedExpansion

set "foo="
FOR /F "delims=" %%G IN ('"CMD /V:ON /C CALL "path with spaces\foo.bat" "blah blah='foobar' blah" ^^^& echo errlevel=^!errorlevel^!"') do (
   set "str=%%G"
   if "!str:~0,8!" equ "errlevel" (
      set "errlevel=!str:~9!"
   ) else (
      set "foo=%%G"
   )
)

echo String output from foo.bat: "%foo%"
echo Errorlevel returned by foo.bat: %errlevel%

然而,原始规范表明执行的程序是一个名为"C:\path with spaces\foo.bat" 的BATCH 文件。当执行的程序是请求的批处理 .BAT 文件时,这段代码可以正常工作,但如果执行的程序是 .exe 文件,它就不起作用,正如我在第一条评论中明确指出的那样:"Note that C:\path with spaces\foo.bat must be a .bat file! This method don't works if such file is an .exe"。这样,您只需在下一行中执行 .exe 和 exit /B %errorlevel% 命令来创建 "foo.bat" 文件。当然,如果你直接执行.exe程序直接测试这个方法,是绝对不行的...

第二次编辑添加了一些解释

这个解决方案的目的是给批处理文件 "C:\path with spaces\foo.bat" 显示一些输出和 return 一个错误级别值,就像这个:

@echo off
echo String from foo.bat
exit /B 12345

... 它可能会被另一个同时获得输出和错误级别的批处理文件调用。这可以使用辅助文件以非常简单的方式完成:

@echo off

call "C:\path with spaces\foo.bat" > foo.txt
set errlevel=%errorlevel%
set /P "foo=" < foo.txt

echo String output from foo.bat: "%foo%"
echo Errorlevel returned by foo.bat: %errlevel%

然而,请求表明:"not to write to some temporary file---I want to do everything in memory"。

从另一个命令获取输出的方法是通过 FOR /F,像这样:

FOR /F "delims=" %%G in ('CALL "C:\path with spaces\foo.bat" "blah blah='foobar' blah"') do set foo=%%G

但是,放置在 FOR /F 集中的批处理文件在新的 cmd.exe 上下文中执行。更准确地说,在前面的 FOR 命令中 foo.bat 的执行 完全等同于 下一个代码:

CMD /C CALL "C:\path with spaces\foo.bat" "blah blah='foobar' blah" > tempFile & TYPE tempFile & DEL tempFile

tempFile中的每一行都分配给%%G可替换参数,并执行set foo=%%G命令。请注意,上一行的执行就像是在命令提示符下输入的一样(命令行上下文),因此有几个命令在此上下文中不起作用(例如goto/call a label, setlocal/endlocal, etc) and delayed expansion 设置为 cmd.exe 的初始值,通常禁用。

为了简化下面的描述,我们使用一个更短但相似的FOR /F命令,下面显示了执行foo.bat的等效内部代码:

FOR /F %%G in ('CALL "foo.bat"') do set foo=%%G
-> CMD /C CALL "foo.bat"                <- we also omit the "tempFile" parts

这样,我们需要一种方法来在此类命令结束时报告 foo.bat 的错误级别。唯一的方法是首先通过显式放置的 cmd.exe /C 执行所需的批处理文件,然后 在它之后 插入一个报告其错误级别的命令。即:

FOR /F %%G IN ('"CMD /C CALL foo.bat ^& echo !errorlevel!"') do set foo=%%G
-> CMD /C "CMD /C CALL foo.bat ^& echo !errorlevel!"

注意&符号必须这样转义:^&;否则它将错误地拆分放置在 FOR /F 命令集中的命令。在上一行中,foo.batecho !errorlevel! 命令都是通过嵌套的 CMD /C.

执行的

但是,FOR /F 集合中的命令如前所述在命令行上下文中执行,因此有必要在显式 [=144] 中启用延迟扩展(/V:ON 开关) =];否则,echo !errorlevel! 就不起作用:

FOR /F %%G IN ('"CMD /V:ON /C CALL foo.bat ^& echo !errorlevel!"') do set foo=%%G
-> CMD /C "CMD /V:ON /C CALL foo.bat ^& echo !errorlevel!"

好的。请注意,前面的描述假设在执行 FOR /F 命令时延迟扩展被禁用。如果启用,会有哪些变化? 1. !errorlevel! 被当前的错误级别值替换,并且 2. 任何用于转义字符的插入符被删除.这些更改是在分析行时完成的, 执行 FOR /F 命令之前。让我们详细看看这一点:

SETLOCAL ENABLEDELAYEDEXPANSION
FOR /F %%G IN ('"CMD /V:ON /C CALL foo.bat ^& echo !errorlevel!"') do set foo=%%G
-> After parsed:
FOR /F %%G IN ('"CMD /V:ON /C CALL foo.bat & echo 0"') do set foo=%%G

上一行发出错误,因为未转义 &,像往常一样。我们需要保留感叹号和符号的 插入符号[=​​121=]。保留感叹号很容易:^!errorlevel^!,但我们如何保留 ^& 中的插入符号?如果我们使用 ^^& 第一个插入符保留第二个插入符,但下一个要解析的字符是单独的 & 并且会出现通常的错误!所以,正确的做法是:^^^&;第一个插入符号保留第二个插入符号并给出 ^^& 保留与符号:

SETLOCAL ENABLEDELAYEDEXPANSION
FOR /F %%G IN ('"CMD /V:ON /C CALL foo.bat ^^^& echo ^!errorlevel^!"') do set foo=%%G
-> After parsed:
FOR /F %%G IN ('"CMD /V:ON /C CALL foo.bat ^& echo !errorlevel!"') do set foo=%%G