I've done a thorough search on this, but I can't find all of the pieces together;
I have:
BatchA runs for loop
Calls BatchB
BatchB runs a SQL script
SQL Script raises error
BatchB gets error, does (exit /b %ErrorLevel%)
Batch A checks for error
I can't make the last step work, in which BatchA responds to the exit error of batch B.
I've simplified it, and I can't even get the following to work. By not work, I mean that the GOTO statement doesn't execute. I hate for loops in batch!!!!
BatchA
setlocal enabledelayedexpansion
FOR /L %%i IN (0,1,0) DO (
:: Define variables
CALL SET TEST_NAME=%%TESTS[%%i]%%
CALL SET D_TEST=%%D_TESTS%%\%%TEST_NAME%%
CALL SET D_DB_RAW=%%D_TEST%%
CALL SET P_TEST=%%D_TEST%%\%%TEST_NAME%%.bat
:: Run preparation script
CALL ECHO. && ECHO.
CALL ECHO [INFO] TEST: %%TEST_NAME%%
CALL ECHO ----------------------------------------------------------
CALL ECHO [INFO] PREPARING THE DATABASE
CALL PUSHD %%D_PRE_TEST%%
CALL %%P_PRE_TEST_BAT%% %%D_DB%% %%D_DB_RAW%%
CALL POPD
CALL SET ERR_MSG="Error preparing the database before test: %%TEST_NAME%%"
if !ERRORLEVEL! NEQ 0 GOTO L_ERROR
BATCH B
EXIT /b 1
Related
Here's my batch file:
#echo off
set rdslist=rds-instance-1 rds-instance-2
:retryaction
set /P action=Would you like to (1)start or (2)stop these instances %rdslist%:
IF %action%==1 (
set command=start
goto :start
)
IF %action%==2 (
set command=stop
goto :start
)
goto :retryaction
:start
(for %%a in (%rdslist%) do (
aws rds %command%-db-instance --db-instance-identifier %%a
))
pause
It doesn't pause after I run it, but if I place the pause before or inside the for loop it pauses.
aws is another script, not a program. When a batch script executes another batch script without using the call command, program flow is permanently transferred to that second script and does not return to the first script upon completion. When call is used, the second script is run and then flow is returned to the parent script.
Change your for loop to
for %%a in (%rdslist%) do (
call aws rds %command%-db-instance --db-instance-identifier %%a
)
so that your initial script will keep running; otherwise, the script stops after the first instance is completed.
Unrelated to running other batch files or other executables. I have subroutines in my batch file and a main method which calls them. I put echos after each call to a subroutine and the numbers aren't hitting when I expect. It doesn't seem to be executing sequentially.
call :_beginProject %subProject%
echo 1
call :_renameBasicFile Service
echo 2
call :_renameBasicFile Test
echo 3
call :_endProject
echo 4
pause
exit /b
:_beginProject
echo in begin project
Do Other Things
:_renameBasicFile
echo in rename file
Do Other Things
:_endProject
echo in end project
Do Other Things
I thought exit /b meant something else. Turns out what was happening, but wasn't clear, is that when I called _beginProject, ALL Code from _beginProject down was executing, like a switch with no breaks. exit /b is like a break from the current stack of processing. So when you call into a function, you push to a new execution stack, and you need to pop it with exit /b, or you'll execute to the end of the file. So I had to change to this to return control to the calling stack after each label/subroutine:
call :_beginProject %subProject%
echo 1
call :_renameBasicFile Service
echo 2
call :_renameBasicFile Test
echo 3
call :_endProject
echo 4
pause
exit /b
:_beginProject
echo in begin project
Do Other Things
exit /b
:_renameBasicFile
echo in rename file
Do Other Things
exit /b
:_endProject
echo in end project
Do Other Things
exit /b
In my main batch file I include another batch file and want to call a function defined in there, code looks like following:
#echo off
call define_wait.bat
if "%1"=="WAIT" (
call :WAIT_AND_PRINT 5
echo.
)
REM rest...
My define_wait.bat looks like following:
:WAIT_AND_PRINT
set /a time=%1
for /l %%x in (1, 1, %time%) do (
ping -n 1 -w 1000 1.0.0.0 > null
echo|set /p=.
)
goto :EOF
:WAIT
set /a time="%1 * 1000"
ping -n 1 -w %time% 1.0.0.0 > null
goto :EOF
The problem is that if I define the wait function in another batch file it does not work, calling call :WAIT_AND_PRINT 5 does not hand on the parameter correctly (Error: missing operand)... If I copy my code from my define_wait.bat int my main batch file, everything works fine...
How would I make that correctly?
Working function bat that forwards it's parameters to it's subfunction:
#echo off
call %*
goto :EOF
:WAIT_AND_PRINT
set /a time=%1
for /l %%x in (1, 1, %time%) do (
ping -n 1 -w 1000 1.0.0.0 > null
echo|set /p=.
)
goto :EOF
:WAIT
set /a time="%1 * 1000"
ping -n 1 -w %time% 1.0.0.0 > null
goto :EOF
In the main bat I now don't include the batch file anymore but call it directly like following:
call define_wait.bat :WAIT_AND_PRINT 5
I wasn't aware of this until jeb commented it, but here's a quick demonstration of the call bug he mentioned, using some utility functions I had lying around.
functions.bat:
:length <"string">
rem // sets errorlevel to the string length (not including quotation marks)
setlocal disabledelayedexpansion
if "%~1"=="" (endlocal & exit /b 0) else set ret=1
set "tmpstr=%~1"
for %%I in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do (
setlocal enabledelayedexpansion
if not "!tmpstr:~%%I,1!"=="" (
for %%x in ("!tmpstr:~%%I!") do endlocal & (
set /a ret += %%I
set "tmpstr=%%~x"
)
) else endlocal
)
endlocal & exit /b %ret%
:password <return_var>
rem // prompts user for password, masks input, and sets return_var to entered value
setlocal disabledelayedexpansion
<NUL set /P "=Password? "
set "psCommand=powershell -noprofile "$p=read-host -AsSecureString;^
$m=[Runtime.InteropServices.Marshal];$m::PtrToStringAuto($m::SecureStringToBSTR($p))""
for /f "usebackq delims=" %%p in (`%psCommand%`) do endlocal & set "%~1=%%p"
goto :EOF
main.bat:
#echo off & setlocal
rem // demo return value
call :password pass
setlocal enabledelayedexpansion
echo You entered !pass!
rem // demo bubbling up of %ERRORLEVEL%
call :length "!pass!"
echo Password length is %ERRORLEVEL%
endlocal
goto :EOF
rem // ====== FUNCTION DECLARATIONS =======
:length <"string">
:password <return_var>
functions.bat %*
Output:
Password? *********
You entered something
Password length is 9
This web page offers an explanation:
If you execute a second batch file without using CALL you may run into some buggy behaviour: if both batch files contain a label with the same name and you have previously used CALL to jump to that label in the first script, you will find execution of the second script starts at the same label. Even if the second label does not exist this will still raise an error "cannot find the batch label". This bug can be avoided by always using CALL.
If you've ever done any coding in C++, it helps to think of the labels in main.bat as function declarations in a .h file, while the labels in functions.bat would correspond to function definitions in a .cpp file. Or in .NET, the main.bat labels would be like DllImport("functions.bat") so to speak.
Although there are several ways to call a function that reside in a separate library file, all methods require to change the way to call the library functions in the calling program, and/or insert additional code at beginning of the library file in order to identify the called function.
There is an interesting trick that allows to avoid all these details, so both the main and the library files contain the original code, and just 2 lines needs to be added to the main file. The method consist in switch the context of the running main Batch file to the library file; after that, all functions in the library file are available to the running code. The way to do that is renaming the library file with the same name of the main file. After that, when a call :function command is executed, the :function label will be search in the library file! Of course, the files must be renamed back to the original names before the program ends. Ah! I almost forget the key point of this method: both the initial and final renames must be executed in a code block in the main file. A simple example:
main.bat
#echo off
echo Calling :test and :hello functions in the library.bat file:
rem Switch the context to the library file
(ren "%~NX0" temp.bat & ren library.bat "%~NX0"
call :test
echo Back from library.bat :test function
call :hello
echo Back from library.bat :hello function
rem Switch the context back to the main file
ren "%~NX0" library.bat & ren temp.bat "%~NX0")
echo Continue in main file
library.bat
:test
echo I am :test function in library.bat file
exit /B
:hello
echo I am :hello function in library.bat file
exit /B
A drawback of this method is that if a run-time error happens when the files are renamed, the files remains renamed, but this may be fixed in a very simple way. For example, a check.bat file may check if the library.bat file exists, and do the rename back if it was not found.
I'm trying to call one bat file from another, while keeping the target variables local to itself(mostly). The code below is my failed attempt, the var i want is missing, but the local is still around.
I've found some info on using setlocal here, which is how I think I have it. Also the info I'm using to push a var past setlocal is here. It's very possible I'm missing something from these. Any help is appreciated!
The calling bat:
#SET errmsg=Old Message
#CALL Target.bat TEST_JOB errmsg
#ECHO.jobname = %jobname%, but should be undefined
#IF DEFINED errmsg ECHO.Error occurred: %ERRORLEVEL% %errmsg%
The target bat:
#SETLOCAL
#ECHO OFF
:START
SET jobname=%~1
SET errmsgvar=%~2
:MAIN
CALL:ERROR "New Error Message"
ECHO.Should have never returned here
ENDLOCAL
:EXIT
#ECHO ON
#EXIT /b %ERRORLEVEL%
:ERROR "errorMesage"
ENDLOCAL & SET "%errmsgvar%=%~1"
ECHO.%errmsg% ^<-- Testing only, we don't know what the actual var will be
(GOTO) 2>NUL & GOTO:EXIT
The result:
C:\Projects\Batch>Caller.bat
New Error Message <-- Testing only, we don't know what the actual var will be
jobname = TEST_JOB, but should be undefined
Error occurred: 0 Old Message
Edit for clarity:
I'm trying to accomplish several things here...
Allow the caller to specify what var the error message should be stored in | 2nd param in call
On an error exit immediately, saving the error message to the provided var | :ERROR block
Keep all vars in target local, push error message through by setting it on the endlocal line
It seems like my endlocal is being ignored, and its just doing endlocal at the end of the file, which breaks things
As Magoo said, your TEST_JOB value is cleared properly. Your test must have been looking at a holdover result from a prior run. Best to explicitly clear the value in your calling bat, prior to the CALL, so you can never get a false result..
Your errmsg is not being set because of a flaw in your logic. More on that after I provide a little introduction to (goto) 2>nul.
You are attempting to use a relatively new (goto) 2>nul technique that is not widely known. It is effectively an exit /b except additional concatenated commands still execute, but in the context of the caller. I believe the first discovery was published at http://forum.script-coding.com/viewtopic.php?id=9050 (which I cannot read), and the first known English posting is at http://www.dostips.com/forum/viewtopic.php?f=3&t=6491.
Since that DosTips post, many useful tools have resulted:
RETURN.BAT - a function to safely return any value across the ENDLOCAL barrier. This function is a derivative of jeb's original idea
PrintHere.bat - a partial emulation of the unix here doc functionality
EXCEPTION.BAT - Effective exception handling within batch scripts
Within your code, you try to ENDLOCAL in your error routine, but that only affects SETLOCAL that were issued while in your routine. You need to use the (goto) 2>nul trick prior to issuing the ENDLOCAL so that ENDLOCAL works properly on the parent context.
:error "errormesage"
(
(goto) 2>nul
endlocal
set "%errmsgvar%=%~1"
goto :exit
)
I'm worried about your returned ERRORLEVEL. I don't think it is returning the value you want when there is an error.
You must remember that all external commands always set the ERRORLEVEL whether there was an error or not. Almost all internal commands set the ERRORLEVEL to non-zero upon error, and some internal commands set the ERRORLEVEL to 0 upon success (but some preserve the existing ERRORLEVEL if there was no error).
It is safest to explicitly pass your desired error number into your :error routine, and then explicitly set the value upon exit. Here is one way to do it:
:error "errMsg" errNum
(
(goto) 2>nul
endlocal
set "%errmsgvar%=%~1"
cmd /c exit %2
goto :exit
)
But I would modify things so that you always exit the same way regardless whether there was an error or not. The :exit routine below is called with the error message and error number if there was an error, and the values are set appropriately. Upon normal exit, neither value is passed in, so the errmsg variable gets cleared, and the routine returns with the current ERRORLEVEL (presumably 0).
Calling script
#setlocal
#set errmsg=Old Message
#set "jobname="
#call target.bat TEST_JOB errmsg
#if defined jobname (
echo ERROR - jobname = %jobname%
) else (
echo SUCCESS - jobname is undefined
)
#if defined errmsg (
echo.Error occurred: %errorlevel% %errmsg%
) else (
echo No error occurred
)
Target.bat
#echo off
setlocal enableDelayedExpansion
:start
set jobname=%~1
set errmsgvar=%~2
:main
set /a "1/(%random% %% 3)" || call :exit !errorlevel! "Random error"
echo Success
call :exit
:exit [errNum] ["errMsg"]
(
(goto) 2>nul
endlocal
set "%errmsgvar%=%~2"
echo on
exit /b %1
)
The :main code randomly raises an error 33% of the time when it attempts to divide by zero.
Here is some sample output, showing both possible outcomes:
C:\test>test
Success
SUCCESS - jobname is undefined
No error occurred
C:\test>test
Divide by zero error.
SUCCESS - jobname is undefined
Error occurred: 1073750993 Random error
Worked perfectly for me.
I believe your test set jobname at some stage and hence your setlocal/endlocal frames simply restore the original environment.
I'd suggest you execute
set "jobname="
as the first line of your calling bat to force jobname to be clear before the action starts.
Change your second script to the following. If accounts for the ENDLOCAL at the end of the script. I believe the ENDLOCAL in the :ERROR block is for that block only and not the entire file.
#SETLOCAL
#ECHO OFF
:START
SET jobname=%~1
SET errmsgvar=%~2
:MAIN
CALL:ERROR "New Error Message"
ECHO.Should have never returned here
ENDLOCAL
:EXIT
REM case insensitive test for errmsgvar.
IF DEFINED errmsgvar (
IF /I NOT "%errmsgvar%" == "" (
ENDLOCAL & SET "errmsg=%errmsg%"
)
)
#ECHO ON
#EXIT /b %ERRORLEVEL%
:ERROR "errorMesage"
ENDLOCAL & SET "errmsgvar=%~1"
SET "errmsg=%errmsgvar%"
ECHO.%errmsg% ^<-- Testing only, we don't know what the actual var will be
(GOTO) 2>NUL & GOTO:EXIT
Output becomes (extra line breaks for clarity):
Caller.bat
New Error Message <-- Testing only, we don't know what the actual var will be
jobname = TEST_JOB
Error occurred: 0 New Error Message
Change 2nd last line to
ECHO.%errmsgvar%.....
to match the 3rd to last line
By way of explanation...
ENDLOCAL & SET "errmsgvar=%~1"
1. The entire line loaded and %~1 is expanded.
2. ENDLOCAL is executed and ends localization.
3. SET "errmsgvar=%~1" is executed as...
SET "errmsgvar=WhateverWasIn%~1"
This technique is tunneling and is a way to pass variables across locales.
I have a simple function written to check for directories:
:direxist
if not exist %~1 (
echo %~1 could not be found, check to make sure your location is correct.
goto:end
) else (
echo %~1 is a real directory
goto:eof
)
:end is written as
:end
endlocal
I don't understand why the program would not stop after goto:end has been called. I have another function that uses the same method to stop the program and it work fine.
:PRINT_USAGE
echo Usage:
echo ------
echo <file usage information>
goto:end
In this instance, the program is stopped after calling :end; why would this not work in :direxist? Thank you for your help!
I suppose you are mixing call and goto statements here.
A label in a batch file can be used with a call or a goto, but the behaviour is different.
If you call such a function it will return when the function reached the end of the file or an explicit exit /b or goto :eof (like your goto :end).
Therefore you can't cancel your batch if you use a label as a function.
However, goto to a label, will not return to the caller.
Using a synatx error:
But there is also a way to exit the batch from a function.
You can create a syntax error, this forces the batch to stop.
But it has the side effect, that the local (setlocal) variables will not be removed.
#echo off
call :label hello
call :label stop
echo Never returns
exit /b
:label
echo %1
if "%1"=="stop" goto :halt
exit /b
:halt
call :haltHelper 2> nul
:haltHelper
()
exit /b
Using CTRL-C:
Creating an errorcode similar to the CTRL-C errorcode stops also the batch processing.
After the exit, the setlocal state is clean!
See #dbenham's answer Exit batch script from inside a function
Using advanced exception handling:
This is the most powerful solutions, as it's able to remove an arbitrary amount of stack levels, it can be used to exit only the current batch file and also to show the stack trace.
It uses the fact, that (goto), without arguments, removes one element from the stack.
See Does Windows batch support exception handling?
jeb's solution works great. But it may not be appropriate in all circumstances. It has 2 potential drawbacks:
1) The syntax error will halt all batch processing. So if a batch script called your script, and your script is halted with the syntax error, then control is not returned to the caller. That might be bad.
2) Normally there is an implicit ENDLOCAL for every SETLOCAL when batch processing terminates. But the fatal syntax error terminates batch processing without the implicit ENDLOCAL! This can have nasty consequences :-( See my DosTips post SETLOCAL continues after batch termination! for more information.
Update 2015-03-20 See https://stackoverflow.com/a/25474648/1012053 for a clean way to immediately terminate all batch processing.
The other way to halt a batch file within a function is to use the EXIT command, which will exit the command shell entirely. But a little creative use of CMD can make it useful for solving the problem.
#echo off
if "%~1" equ "_GO_" goto :main
cmd /c ^""%~f0" _GO_ %*^"
exit /b
:main
call :label hello
call :label stop
echo Never returns
exit /b
:label
echo %1
if "%1"=="stop" exit
exit /b
I've got both my version named "daveExit.bat" and jeb's version named "jebExit.bat" on my PC.
I then test them using this batch script
#echo off
echo before calling %1
call %1
echo returned from %1
And here are the results
>test jebExit
before calling jebExit
hello
stop
>test daveExit
before calling daveExit
hello
stop
returned from daveExit
>
One potential disadvantage of the EXIT solution is that changes to the environment are not preserved. That can be partially solved by writing the environent to a temporary file before exiting, and then reading it back in.
#echo off
if "%~1" equ "_GO_" goto :main
cmd /c ^""%~f0" _GO_ %*^"
for /f "eol== delims=" %%A in (env.tmp) do set %%A
del env.tmp
exit /b
:main
call :label hello
set junk=saved
call :label stop
echo Never returns
exit /b
:label
echo %1
if "%1"=="stop" goto :saveEnvAndExit
exit /b
:saveEnvAndExit
set >env.tmp
exit
But variables with newline character (0x0A) in the value will not be preserved properly.
If you use exit /b X to exit from the function then it will set ERRORLEVEL to the value of X. You can then use the || conditional processing symbol to execute a command if ERRORLEVEL is non zero.
#echo off
setlocal
call :myfunction PASS || goto :eof
call :myfunction FAIL || goto :eof
echo Execution never gets here
goto :eof
:myfunction
if "%1"=="FAIL" (
echo myfunction: got a FAIL. Will exit.
exit /b 1
)
echo myfunction: Everything is good.
exit /b 0
Output from this script is:
myfunction: Everything is good.
myfunction: got a FAIL. Will exit.
Here's my solution that will support nested routines if all are checked for errorlevel
I add the test for errolevel at all my calls (internal or external)
#echo off
call :error message&if errorlevel 1 exit /b %errorlevel%<
#echo continuing
exit /b 0
:error
#echo in %0
#echo message: %1
set yes=
set /p yes=[no]^|yes to continue
if /i "%yes%" == "yes" exit /b 0
exit /b 1