Retrieve %ERRORLEVEL% with complicated call in FOR loop - windows

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

Related

unable to use for loop output to set variable in batch

I'm using a for loop to acces a text file with a bunch of files + their directory formatted like this:
//srv/something/somethingelse/movie.mpg
//srv/something/somethingelse/movie2.mkv
//srv/something/somethingelse/movie3.mpg
//srv/something/somethingelse/movie4.mkv
I have to replace .mpg and .mkv with .xml, and then write that output away to another text file, which I'm trying to do like this:
for /F "tokens=*" %%A in (%~dp0temporary\movies.txt) do (
set string=%%A
set find=.mkv
set replace=.xml
set string=%%string:!find!=!replace!%%
set find=.mpg
set string=%%string:!find!=!replace!%%
echo %string%>>%~dp0temporary\xml.txt
)
The output I want is this:
//srv/something/somethingelse/movie.xml
//srv/something/somethingelse/movie2.xml
//srv/something/somethingelse/movie3.xml
//srv/something/somethingelse/movie4.xml
But what I get is this:
Echo is off.
Echo is off.
Echo is off.
Echo is off.
I have been searching on this for over an hour but I can't find anything that works
Here is the rewritten batch code which produces the expected output from input file.
#echo off
setlocal EnableDelayedExpansion
set "vidLoc=//srv"
set "resultLoc=c:"
del "%~dp0temporary\xml.txt" 2>nul
for /F "usebackq delims=" %%A in ("%~dp0temporary\movies.txt") do (
set "FileNameWithPath=%%A"
set "FileNameWithPath=!FileNameWithPath:.mkv=.xml!"
set "FileNameWithPath=!FileNameWithPath:.mpg=.xml!"
set "FileNameWithPath=!FileNameWithPath:%vidLoc%=%resultLoc%!"
echo !FileNameWithPath!>>"%~dp0temporary\xml.txt"
)
endlocal
All environment variable references enclosed in percent signs are expanded already on parsing entire for block. Just the environment variable references enclosed in exclamation marks are expanded delayed on executing the command. This can be seen on opening a command prompt window and running from there the batch file without #echo off at top or with this line being changed to #echo on.
Executing in a command prompt window set /? results in getting help of this command output on several window pages where usage of delayed expansion for for and if blocks is explained on a simple example.
And running in a command prompt window for /? prints help of command for into the output window.
For just replacing the file extension you could also use:
#echo off
del "%~dp0temporary\xml.txt" 2>nul
for /F "usebackq delims=*" %%A in ("%~dp0temporary\movies.txt") do (
echo %%~dpnA.xml>>"%~dp0temporary\xml.txt"
)
But this faster code changes also all forward slashes / to backslashes \ as the backslash character is the directory separator on Windows.
Mofi is right: move the line with setlocal enabledelayedexpansion out of any code block enclosed in (parentheses).
However, try next approach using Command Line arguments (Parameters) modifier ~:
#ECHO OFF >NUL
#SETLOCAL enableextensions
for /F "tokens=*" %%A in (%~dp0temporary\movies.txt) do (
rem full_path=%%~dpnA
rem extension=%%~xA
echo %%~dpnA.xml
)>%~dp0temporary\xml.txt

Windows Batch for Pipe out Variable

How can i get the value to myVar attribute to use in the other part of code.
set myVar =
ECHO "httpsxxxxxx_cmidxxxxx_2014-04-12_14-54-53_abc3654.xml" | find /i "2014-04-12" | (set /p myVar= & set myVar)
ECHO myvar "%myVar%" /**** This is empty String**********/
Maybe what you are asking is:
This line
echo "someValue" | find /i "Value" | (set /p myVar= & set myVar)
will show you the variable has been set. echo command send its output to find command, and the output of it is piped into the set /p, assigning the value to the variable, value that the set will show in console.
Then the following line
echo myvar "%myVar%"
shows no data. Why?
When cmd finds a pipe definition, with data flowing from one program to other, each of the elements in the pipe is a separate process with its own copy of the environment block of the parent process (where the batch file is running), so the set /p myVar= & set myVar is not running inside the same instance as the batch file. The variable is set in another process, and as the changes are made in a copy of the environment, they are not visible from the parent process.
Try like this :
#echo off
for /f "delims=" %%a in ('ECHO "httpsxxxxxx_cmidxxxxx_2014-04-12_14-54-53_abc3654.xml" ^| find /i "2014-04-12"') do set MyVar=%%a
echo %MyVar%
I don't anderstand what you are trying to do.
But assuming that you're looking for a .xml file who contain the string "2014-04-12" in a directory. The code will be :
#echo off
for /f "delims=" %%a in ('dir /b/a-d *.xml ^| find /i "2014-04-12"') do set MyVar=%%a
Annotating your code:
1: set myVar =
2: ECHO "httpsxxxxxx_cmidxxxxx_2014-04-12_14-54-53_abc3654.xml" | find /i "2014-04-12" | (set /p myVar= & set myVar)
3: ECHO myvar "%myVar%" /**** This is empty String**********/
On line 1:, there is a space between myVar and =. This is incorrect. CMD.EXE considers that there is a variable called "myVar ". Change this to "myVar".
On line 2:, I guess that you intended to use TYPE to output the file "httpsxxxxxx_cmidxxxxx_2014-04-12_14-54-53_abc3654.xml". In any case you could have written this, instead:
FIND /I "2014-04-12" httpsxxxxxx_cmidxxxxx_2014-04-12_14-54-53_abc3654.xml

Escape PIPE character in a function call windows Batch script

I am writing a function to execute shell commands and capture its output in a batch script.
:runShellCmd
setlocal EnableDelayedExpansion
SET lf=-
FOR /F "delims=" %%i IN ('%~1') DO if "%out%" == "" (set out=%%i) else (set out=!out!%lf%%%i)
echo "Cmd output: %out%"
SET "funOut=%out%"
ENDLOCAL & IF "%~1" NEQ "" SET %~2=%out%
goto :EOF
I have been successful in passing simple commands and getting output. But for calls like
CALL :runShellCmd "echo Jatin Kumar | find /c /i "jatin"" it fails with error unexpected | character.
I know we need to escape | with ^ in for but if I try to pass ^| in the function argument string, it changes it to ^^| which again throws error.
Am I missing something?
This is an effect of the CALL command.
The CALL command doubles all carets in one of the batch parser phases.
Normally you wouldn't see this, as the carets will be used as an escape charater directly after the doubling.
See this
call echo ^^^^
call call echo ^^^^
call call call echo ^^^^
call echo "^^^^"
call call echo "^^^^"
call call call echo "^^^^"
Output
^^
^^
^^
"^^^^^^^^"
"^^^^^^^^^^^^^^^^"
"^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^"
But how do you can escape your pipe character?
You can't!
But you can add a caret remover in your function.
:runShellCmd
setlocal EnableDelayedExpansion
set "param=%~1"
set param
set "param=!param:^^=^!"
for .... ('!param!')
Or you could use an escaping trick when calling your function.
set "caret=^"
CALL :runShellCmd "echo Jatin Kumar %%caret%%| find /c /i "
This works, as the %%caret%% will be expanded after the CALL caret doubling phase.
#echo off
setlocal enableextensions disabledelayedexpansion
call :test "echo(this|find "t""
exit /b
:test
set "x=%~1"
for /f "delims=" %%f in ('%x:|=^|%') do echo [%%f]
I think i'm missing something, because this works for me.
EDITED - This should be a more general solution. Not bulletproof but a skeleton.
#echo off
setlocal enableextensions disabledelayedexpansion
call :test "(echo(this&echo(that)|find "t" 2>nul|sort&echo end"
exit /b
:test
set "x=%~1"
set "x=%x:|=^|%"
set "x=%x:>=^>%"
set "x=%x:<=^<%"
set "x=%x:&=^&%"
set "x=%x:)=^)%"
for /f "delims=" %%f in ('%x%') do echo [%%f]

Undefined variable error in a bat file

I tried building a batch file using the commands after searching the site. I am trying to find the number/count of a process running, and then use if to execute another command if the number of such processes is more than 5 at any instance.
When I run the statements line by line in the CMD prompt, it works fine.
However, when I run it through a bat file it gives an error saying- a was unexpected at this time.
Here is the script. Also I am not sure if am using the correct If statement (I did search and use before coming to you, but still just incase):
for /f "tokens=1,*" %a in ('tasklist ^| find /I /C "iexplore.exe" ') do
#set var=%a
echo %var%
if %var% <= 5
::echo "hi"
::end if
Also, have one more syntax to do so:
wmic process where name="iexplore.exe" | find "iexplore.exe" /c
but I am not sure how to assign the output of this command to any variable and go on to compare the value of this command to 5.
You need to use double % for FOR command when used in a batch file.
#echo off
set var=0
for /f "tokens=1,*" %%a in ('tasklist ^| find /I /C "explorer.exe" ') do set var=%%a
echo %var%
if %var% leq 5 (
echo less or equal to 5
) else (
echo 5 or more
)

Echoing in the same line

I had a look at the previous questions of your db and I didn't try an answer, but I try.
I would like to write the following lines code:
echo Executing backup....
backup procedure
echo Ok
but the output should be:
Executing backup... Ok
That's possible?!
I suppose you are using dos/nt-batch.
It is possible with the set /p command, because set /p doesn't print a CrLf
set /p "=Executing backup...." <nul
echo OK
Also it's possible to erase the line with a CR character.
It's important to know that whitespace characters at the front of an set /p are ignored (in Vista, not in XP), so the !cr! has to placed later or at the end.
A CR can only be displayed with delayedExpansion, because %cr% works, but CR characters are removed in the percent expansion phase(or directly after this phase), but not in the delayed expansion phase.
Example of a counter which use only one line for displaying
#echo off
setlocal EnableDelayedExpansion EnableExtensions
call :CreateCR
for /l %%n in (1,1,10000) do (
set /P "=Count %%n!CR!" <nul
)
echo(
goto :eof
:CreateCR
rem setlocal EnableDelayedExpansion EnableExtensions
set "X=."
for /L %%c in (1,1,13) DO set X=!X:~0,4094!!X:~0,4094!
echo !X! > %temp%\cr.tmp
echo\>> %temp%\cr.tmp
for /f "tokens=2 usebackq" %%a in ("%temp%\cr.tmp") do (
endlocal
set cr=%%a
goto :eof
)
goto :eof
EDIT: Explanation, how the variable cr is created (Done with a trick)
After setting variable X to a single dot (the character itself is unimportant), it is repeated to become 8188 characters by way of for /L %%c in (1,1,13) DO set X=!X:~0,4094!!X:~0,4094!
Then the variable, two spaces and both a CR and LF are echoed into a file with echo !X! > %temp%\cr.tmp (Notice the two spaces between the !X! and the > and the natural line endings echo amends internally)
We now have 8192 characters, but the data buffer can only hold 8191 characters, so the last character (the linefeed) will be dropped!
In the next line echo\>> %temp%\cr.tmp, another CR/LF set is appended to the file (the \ in the command is just to output nothing bar the carriage return and line feed, as echo by it's self will output ECHO is ON/OFF), that's important, as a single CR can't be read at the end of a line (More later).
So the file now contains <8188 .'s><SPACE><SPACE><CR><CR><LF>
The for /f "tokens=2 usebackq" %%a in ("%temp%\cr.tmp") do reads the second token, the delimters are standard space and tab, so the second token is only a single CR, as the following CR/LF is removed as standard line ending.
Finally the endlocal is used to return to an environment without the temporary variables X, c and a existing (As with the endlocal in brackets, it allows the setting of cr before the endlocal actually takes affect at the end of the brackets (could also be written as for /f "tokens=2 usebackq" %%a in ("%temp%\cr.tmp") do endlocal&set cr=%%a&goto :eof)
Additionally
This was my first way to create a CR character, but it needs some time and a temporary file.
Later I saw a simpler method of retrieving the CR from a copy /z command.
for /f %%a in ('copy /Z "%~dpf0" nul') do set "CR=%%a"
Try this on Posix system (Linux)
echo -n "Executing backup.... "
echo -n "backup procedure "
echo "Ok"
It is much harder on Windows. You will need to use something like this:
#echo off
echo|set /p ="Executing backup...."
echo|set /p =" backup procedure"
Check this post: http://www.pcreview.co.uk/forums/showpost.php?s=1a20b16775d915998b30bd76a0ec5d35&p=4432915&postcount=7.
It's a bit of a hack, but here is an article describing how to do it for Windows.
From the article, the final result (edited for your setup) looks like this:
SET /P var=Backing up
%Result%....<NUL
Backup_process %Result% >NUL 2>&1
IF ERRORLEVEL 1
ECHO FAIL
ELSE
ECHO OK
I've done something similar using a VBScript.
Put this code in EchoNoNewline.vbs:
If WScript.Arguments.Named.Exists("TEXT") Then
WScript.StdOut.Write WScript.Arguments.Named.Item("TEXT")
End If
From your batch file, use the script like this:
CSCRIPT EchoNoNewLine.vbs //NOLOGO /TEXT:"Executing backup...."
backup procedure
CSCRIPT EchoNoNewLine.vbs //NOLOGO /TEXT:"Ok"
at What does a forward slash before a pipe in cmd do to remove the line ending of an echo?
the best suggestion is:
to echo text without a linefeed is very inefficient, as a pipe creates two new instances of cmd.exe.
It's much simpler and faster to use
<nul set /p "=My Text"
The redirect from NUL will also stop the waiting for user input.

Resources