Retrieve %ERRORLEVEL% with complicated call in FOR loop

时间:2015-12-14 17:57:30

标签: windows batch-file for-loop psql errorlevel

In a Windows batch file I needed to retrieve the result of a program execution into a variable, when the program call was complicated with spaces and several options. After much discussion, a workaround was found by using CALL:

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

Please see the following question for more details and to understand the context:

Retrieve command output to variable when command has spaces

In reality the batch file calls PostgreSQL 9.3, like this:

SET PSQL_EXE=C:\Program Files\Foo\Bar\PostgreSQL\9.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!

Unfortunately it appears that this format results in a separate cmd instance, so any error that occurred in calling pgsql (e.g. the lack of a password file) does not get passed back and the %ERRORLEVEL% does not get set. Specifically pgsql prints out:

psql: fe_sendauth: no password supplied

Yet %ERRORLEVEL% (and !ERRORLEVEL!) is still 0.

See the following question for more info:

PSQL Error Level in Batch For Loop

So now the question is how to find out the %ERRORLEVEL%, now that I've succeeding in getting the response of psql. I'd prefer not to write to some temporary file---I want to do everything in memory.

(Note that, yes, the value I'm trying to query from the database and store in FOO will be either a 0 or a 1; not that it matters, but it does make things more confusing.)

I tried to test Aacini's proposed solution by simply seeing what would be returned in %%G:

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
)

Because psql could not find a password file, this prints an error to stderr (as expected) and the ECHO outputs 0---so the ERRORLEVEL is not getting sent back to the DO clause. But if I split out the command from the FOR loop and just run it directly, it shows the ERRORLEVEL (2) just fine!

"%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!

Update: After being advised that I need to escape the escapes if I have delayed expansion already turned on, I took the given answer and updated it to take into account that the program I'm calling won't return a value if it generates an error. So here's what I do; first I make sure two variables are undefined:

SET "var1="
SET "var2="

Then I use the loop that @Aacini and @jeb gave, but inside the loop I do this:

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

Then outside the loop after it has finished:

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

That seems to work. (Unbelievably---what gymnastics!)

1 个答案:

答案 0 :(得分:3)

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

@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%

这是#34;路径,包含空格\ 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" ) )
@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%

编辑回复OP的评论

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

"C:\path with spaces\foo.bat"

但是,原始规范表明执行的程序是一个名为exit /B %errorlevel%的BATCH文件。当执行的程序是请求的Batch .BAT文件时,此代码正常工作,但如果执行的程序是.exe文件则不起作用,正如我在第一条评论中明确指出的那样:"注意C:\ space \ foo.bat的路径必须是.bat文件!如果此类文件是.exe",则此方法不起作用。这样,你只需要创建" foo.bat"在下一行执行.exe和"C:\path with spaces\foo.bat"命令的文件。当然,如果你通过直接执行.exe程序直接测试这个方法,它永远不会工作......

2ND EDIT 添加了一些解释

此解决方案的目的是提供显示某些输出的批处理文件@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%

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

FOR /F

然而,请求表明:&#34;不写入某个临时文件---我想在内存中做所有事情&#34;。

从另一个命令获取输出的方法是通过FOR /F "delims=" %%G in ('CALL "C:\path with spaces\foo.bat" "blah blah='foobar' blah"') do set foo=%%G ,如下所示:

foo.bat

但是,放置在FOR / F集中的批处理文件在新的cmd.exe上下文中执行。更准确地说,先前FOR命令中CMD /C CALL "C:\path with spaces\foo.bat" "blah blah='foobar' blah" > tempFile & TYPE tempFile & DEL tempFile 的执行完全等同于到下一个代码:

tempFile

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

为了简化以下描述,我们使用一个更短但相似的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.bat命令集中的命令。在前一行中,echo !errorlevel!CMD /C命令都是通过嵌套的/V:ON执行的。

但是,FOR / F集中的命令是在命令行上下文中执行的,如前所述,因此必须在显式cmd.exe中启用延迟扩展(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!" 不起作用:

!errorlevel!

确定。请注意,前面的描述假设执行FOR / F命令时延迟扩展为DISABLED。如果启用了哪些更改? 1。 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 将替换为当前的错误级别值, 2。将删除用于转义字符的所有插入符号。这些更改在解析行时完成,执行FOR / F命令之前完成。让我们详细了解这一点:

&

上一行发出错误,因为未转义的^!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 保留&符号:

{{1}}