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!)
A couple tricks must be combined in order to do that. The Batch file placed in the for /F
set is executed in a new cmd.exe context, so we need a method to report its errorlevel when such command ends. The only way to do that is by first executing the desired Batch file via a cmd.exe /C
placed explicitly, and after it insert a command that report its errorlevel. However, the commands placed in the for /F
set are executed in the command-line context, not in the Batch file context, so it is necessary to enable delayed expansion (/V:ON
switch) in the explicit cmd.exe. After that, a simple echo !errorlevel!
show the errorlevel returned by the Batch file.
@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%
This is "path with spaces\foo.bat":
@echo off
echo String from foo.bat
exit /B 12345
And this is the output of previous code:
String output from foo.bat: "String from foo.bat"
Errorlevel returned by foo.bat: 12345
If delayed expansion is already enabled before in the main batch file then a quite different syntax have to be used.
^!ERRORLEVEL^!
This is necessary to avoid, that the !ERRORLEVEL! is evaluated in the context of the batch file, before the FOR line will be executed.
^^^&
is necessary to bring a single ^&
to the inner cmd /V:ON
expression
@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"
)
)
EDIT: Reply to OP's comments
My first solution assumes that the program always output one line, so it took the errorlevel from the second output line. However, the new specification indicate that when the program ends because an error, the first output line is not sent. It is very easy to fix this detail, and the corrected code is below.
@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%
However, the original specification indicate that the executed program is a BATCH file called "C:\path with spaces\foo.bat"
. This code correctly works when the executed program is a Batch .BAT file as requested, but it does not work if the executed program is an .exe file, as I clearly indicated in my first comment: "Note that C:\path with spaces\foo.bat must be a .bat file! This method don't works if such file is an .exe". This way, you just need to create the "foo.bat" file with the execution of the .exe and an exit /B %errorlevel%
command in the next line. Of course, if you directly test this method by directly executing the .exe program, it never will work...
2ND EDIT: Some explanations added
The purpose of this solution is that giving a Batch file "C:\path with spaces\foo.bat"
that show some output and return an errorlevel value, like this one:
@echo off
echo String from foo.bat
exit /B 12345
... it may be called by another Batch file that get both the output and the errorlevel. This can be done in a very easy way using an auxiliary file:
@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%
However, the request indicate: "not to write to some temporary file---I want to do everything in memory".
The way to get the output from another command is via FOR /F
, like this:
FOR /F "delims=" %%G in ('CALL "C:\path with spaces\foo.bat" "blah blah='foobar' blah"') do set foo=%%G
However, The Batch file placed in the FOR /F set is executed in a new cmd.exe context. To be more precise, the execution of foo.bat
in previous FOR command is entirely equivalent to the next code:
CMD /C CALL "C:\path with spaces\foo.bat" "blah blah='foobar' blah" > tempFile & TYPE tempFile & DEL tempFile
Each line in tempFile
is assigned to the %%G
replaceable parameter and the set foo=%%G
command is executed. Note that previous line is executed as if it was entered at the command-prompt (command-line context), so several commands does not work in this context (like goto/call to a label, setlocal/endlocal, etc) and delayed expansion is set to the initial value of cmd.exe, usually disabled.
In order to simplify the following description, we use a shorter, but similar FOR /F
command that is shown with the equivalent internal code of the execution of foo.bat
below it:
FOR /F %%G in ('CALL "foo.bat"') do set foo=%%G
-> CMD /C CALL "foo.bat" <- we also omit the "tempFile" parts
This way, we need a method to report the errorlevel of foo.bat
when such command ends. The only way to do that is by first executing the desired Batch file via a cmd.exe /C
placed explicitly, and after it insert a command that report its errorlevel. That is:
FOR /F %%G IN ('"CMD /C CALL foo.bat ^& echo !errorlevel!"') do set foo=%%G
-> CMD /C "CMD /C CALL foo.bat ^& echo !errorlevel!"
Note that the ampersand sign must be escaped this way: ^&
; otherwise it will incorrectly split the commands placed in the set of the FOR /F
command. In previous line both the foo.bat
and the echo !errorlevel!
command are executed via the nested CMD /C
.
However, the commands placed in the FOR /F set are executed in the command-line context as explained before, so it is necessary to enable delayed expansion (/V:ON
switch) in the explicit cmd.exe; otherwise, the echo !errorlevel!
just not works:
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!"
Ok. Note that the previous description assumed that delayed expansion was DISABLED when the FOR /F command is executed. What are the changes if it was enabled? 1. The !errorlevel!
is replaced by the current errorlevel value, and 2. Any caret that is used to escape a character is removed. These changes are done when the line is parsed, before the FOR /F command is executed. Lets see this point with detail:
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
Previous line issue an error because the unescaped &
, as usual. We need to preserve both the exclamation marks and the caret of the ampersand. Preserve exclamation marks is easy: ^!errorlevel^!
, but how we preserve the caret in ^&
? If we use ^^&
the first caret preserve the second one, but the next character to parse is &
alone and the usual error appears! So, the right way is: ^^^&
; the first caret preserve the second one and give ^
, and the ^&
preserve the ampersand:
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