I'm writing a complex batch patch file with generation of other files. I know that batch is not the best for it but I've mostly got it all working. However, there is a headache of keeping all subroutines duplicated in all files if I don't want to make a separate file for each subroutine. My question was whether there is any way of keeping a library file of multiple subroutines and call them somehow?
So I'm going to answer my own question because I managed to get something working.
Lets say you have main.bat and lib.bat files.
Main.bat
#ECHO off
SETLOCAL
SET return=
SET reference=this is just a reference variable
CALL lib.bat return "subroutine" "static arg" reference
IF NOT "%ERRORLEVEL%"=="0" (
ECHO Execution failed - %ERRORLEVEL%
EXIT /b 1
)
ECHO.
ECHO subroutine return value: "%return%"
ECHO.
CALL lib.bat NUL "procedure" "static arg" reference
ECHO.
CALL lib.bat return "error" "static arg" reference
IF NOT "%ERRORLEVEL%"=="0" (
ECHO Execution failed - %ERRORLEVEL%
EXIT /b 1
)
ENDLOCAL
EXIT /b 0
Lib.bat
#ECHO off
::
:: ===================================================================================
:: Library Main Handler
:: ===================================================================================
:: %~1 - [out] - NUL | reference to a return variable
:: %~2 - [in] - subroutine label to be invoked
:: %~3+ - [in] - optional arguments to the subroutine
::
:: ERRORLEVEL is passed through to the caller
::
SETLOCAL ENABLEDELAYEDEXPANSION ENABLEEXTENSIONS
SET callSub=%~2
SET return=
SET args=
IF "%callSub%"=="" (
ECHO Subroutine label was not provided to the library. 1>&2
EXIT /b 1
)
:buildUpArgumentList
IF "%~3"=="" GOTO end_buildUpArgumentList
SET args=%args% "%~3"
SHIFT /3
GOTO buildUpArgumentList
:end_buildUpArgumentList
IF NOT "%~1"=="NUL" (
call:%callSub% return %args%
IF NOT "!ERRORLEVEL!"=="0" (
EXIT /b !ERRORLEVEL!
)
) ELSE (
call:%callSub% %args%
IF NOT "!ERRORLEVEL!"=="0" (
EXIT /b !ERRORLEVEL!
)
)
(
ENDLOCAL
IF NOT "%~1"=="NUL" (
SET %~1=%return%
)
)
EXIT /b 0
::
:: ===================================================================================
:: Library Subroutine Definitions
:: ===================================================================================
::
:subroutine <r_return> <static> <r_reference>
SETLOCAL
ECHO subroutine^<static^>: "%~2"
ECHO subroutine^<r_reference^>: "!%~3!"
(
ENDLOCAL
SET %~1=subroutine executed OK
)
EXIT /b 0
:procedure <static> <r_reference>
SETLOCAL
ECHO procedure^<static^>: "%~1"
ECHO procedure^<r_reference^>: "!%~2!"
ENDLOCAL
EXIT /b 0
:error <r_return> <static> <r_reference>
SETLOCAL
ECHO error^<static^>: "%~2"
EXIT /b 2
ECHO error^<r_reference^>: "!%~3!"
(
ENDLOCAL
SET %~1=error executed OK
)
EXIT /b 0
Output:
subroutine<static>: "static arg"
subroutine<r_reference>: "this is just a reference variable"
subroutine return value: "subroutine executed OK"
procedure<static>: "static arg"
procedure<r_reference>: "this is just a reference variable"
error<static>: "static arg"
Execution failed - 2
Some comments:
The library subroutine invocation is similar to a regular function signature: () but without parentheses.
You will notice that the library code checks for NUL being passed. If that is the case, return value is neither passed to the subroutine nor is it returned back.
The subroutine arguments support n-number of arguments.
The library supports error level pass-through.
If a provided subroutine label does not exist within the library, batch will return ERRORLEVEL=1 and a message "The system cannot find the batch label specified - [label]."
Additional thoughts:
I am relatively new to batch scripting so not everything is perfect.
This was done in ~1hr and I'm sure its missing some stuff!
Should some escaping be performed when constructing the arguments array (i.e., quotes)?
Any other comments are much appreciated!
A simpler approach is to use the following trick:
Enclose the code that call the library functions between parentheses.
At beginning of the parentheses rename the current Batch file to other name, and rename the library file as the current Batch file.
Now you can call any library function in the same way as before.
Before parentheses ends, rename the files back to original names.
For example:
(
rem Switch the active context to the library file:
ren "%~0" main.bat
ren libraryFile.bat "%~0"
rem From this line on you may call any function in the library file, for example:
call :FUNCTION
rem Switch the context back to original file
ren "%~0" libraryFile.bat
ren main.bat "%~0"
)
For further details, see:
How to package all my functions in a batch file as a seperate file?
Related
I am passing command line arguments to a batch script and setting that to a variable like so:
SET dirWhereKept=%1
My problem is, I call a function inside the batch script with 3 arguments. When trying to get those 3 arguments, it gets the ones passed in via the command line instead:
FOR /f "delims=" %%i IN ('DIR /B') DO (
IF EXIST "%%~i\" (
rem nothing
) ELSE (
CALL :checkIfWantedFile %%i "%%~xi" %%~zi
)
)
:checkIfWantedFile
SET file=%~1
SET fileExtension=%~2
SET fileSizeInBytes=%~3
ECHO FILE: %file%
ECHO EXTENSION: %fileExtension%
ECHO SIZE: %fileSizeInBytes%
For example, if I pass in "Avatar ECE (2009)" as the command line argument (which is a directory) and then when calling the function I pass:
%%i: Avatar.mp4
"%%~xi": ".mp4"
%%~zi: some_int
When I do the ECHO's in checkIfWantedFile, the output will be:
FILE: Avatar ECE (2009)
EXTENSION:
SIZE:
As it's getting the command line arguments and not the function ones.
I've had a look at this and some others but cannot get it to work.
EDIT:
The intent of this batch script is to go into a given directory (supplied by the command line arguments) and extract and video file (.mp4, .mkv, .avi) that maybe be in there. Sometimes, the video files are nested in a sub-directory which is why I am checking if the item in the FOR loop is a folder or not. If it is, then the batch script was intended to go into the sub-directory and check whether there are any wanted video files in there or not and extract them.
I added the option that if the script comes across an unwanted file, it is deleted. This is not a requirement though.
The intent is also is that when the directory (supplied in the command line arguments) is cleared of all video files recursively, it is deleted.
Due to unwanted video sample files sometimes being present, I have put in a check as well to check the size of the file in MB, if it is GTR then the %minimumSize% then it is a wanted file and can be extracted
My full code is below:
#ECHO off
SET "dirWhereKept=%1"
SET mp4=".mp4"
SET mkv=".mkv"
SET avi=".avi"
SET needCompressingDir="E:\Need_compressing"
SET minimumSize=200
CD %needCompressingDir%
CD %dirWhereKept%
FOR /f "delims=" %%i IN ('DIR /B') DO (
IF EXIST "%%~i\" (
rem do nothing
) ELSE (
GOTO :EOF
CALL :checkIfWantedFile "%%i" "%%~xi" "%%~zi"
)
)
:checkIfWantedFile
SET file=%~1
SET fileExtension=%~2
SET fileSizeInBytes=%~3
IF "%fileExtension%" == %mp4% (
CALL :checkFileSize %fileSizeInBytes%
) ELSE (
IF "%fileExtension%" == %mkv% (
CALL :checkFileSize %fileSizeInBytes%
) ELSE (
IF "%fileExtension%" == %avi% (
CALL :checkFileSize %fileSizeInBytes%
) ELSE (
rem this is not required!
CALL :deleteFile
)
)
)
:checkFileSize
SET /a fileSizeInMB=%~1/1024/1024
IF %fileSizeInMB% GTR %minimumSize% (
CALL :moveFileToCompress
)
:deleteFile
ECHO "Delete called!"
:moveFileToCompress
MOVE %file% %needCompressingDir%
Change %%i in the CALL statement to "%%i"
example:
#ECHO OFF
SET "dirWhereKept=%~1"
IF /I NOT "%CD%"=="%~1" PUSHD "%~1"2>Nul||Exit/B
FOR %%i IN (*.*) DO CALL :checkIfWantedFile "%%i" "%%~xi" "%%~zi"
TIMEOUT -1
EXIT/B
:checkIfWantedFile
ECHO FILE: %~1
ECHO EXTENSION: %~2
ECHO SIZE: %~3
Your problem is simply that batch has no concept of a section or procedure. It simply executes the next statement until it reaches end-of-file or exit.
Consequently, the call executes the routine :checkIfWantedFile and when it reaches end-of-file, it returns to the next logical statement after the call - which is the routine :checkIfWantedFile, so it continues executing, using the parameters supplied to the batch itself as %n.
You need to insert a goto :eof line before the :checkIfWantedFile label so that the routine is skipped after it has been executed by the call. Note that the colon in :eof is critical. :eof is defined as end-of-file by cmd.
You are calling a number of subroutines. You need to specifically goto :eof at the end of each and every subroutine, otherwise batch simply continues executing line-by-line. For instance
:deleteFile
ECHO "Delete called!"
:moveFileToCompress
MOVE %file% %needCompressingDir%
If you call :deletefile then the echo is executed, and the move is attempted.
If you call :moveFileToCompress then just the move is attempted.
If you change this to
:deleteFile
ECHO "Delete called!"
GOTO :EOF
:moveFileToCompress
MOVE %file% %needCompressingDir%
goto :eof
then
If you call :deletefile then the echo is executed.
If you call :moveFileToCompress then the move is attempted.
Certainly, a goto :eof at the end-of-file is redundant. I habitually use it so I don't need to remember to insert it before a new added-routine to prevent flow-through.
You would need to follow a similar pattern for each of the subroutines you call.
When I say "report" what I mean is that the batch echoes something to the console, so in your code
:checkIfWantedFile
SET file=%~1
SET fileExtension=%~2
SET fileSizeInBytes=%~3
ECHO FILE: %file%
ECHO EXTENSION: %fileExtension%
ECHO SIZE: %fileSizeInBytes%
This would set the variables and then report their values.
(btw - you may like to use
set file
for reporting these values, which will show the value and name of each existing variable that starts file - a lot less typing...)
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 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
In a DOS Batch File subroutine, how can I turn off echo within the subroutine, but before returning, put it back to what it was before (either on or off)?
For example, if there was a command called echo restore, I would use it like this:
echo on
... do stuff with echoing ...
call :mySub
... continue to do stuff with echoing ...
exit /b
:mySub
#echo off
... do stuff with no echoing ...
echo restore
goto :EOF
My first attempt was an utter failure - thanks jeb for pointing out the errors. For those that are interested, the original answer is available in the edit history.
Aacini has a good solution if you don't mind putting your subroutine in a separate file.
Here is a solution that works without the need of a 2nd batch file. And it actually works this time! :)
(Edit 2 - optimized code as per jeb's suggestion in comment)
:mysub
::Silently get the echo state and turn echo off
#(
setlocal
call :getEchoState echoState
echo off
)
::Do whatever
set return=returnValue
::Restore the echo state, pass the return value across endlocal, and return
(
endlocal
echo %echoState%
set return=%return%
exit /b
)
:getEchoState echoStateVar
#setlocal
#set file=%time%
#set file="%temp%\getEchoState%file::=_%_%random%.tmp"
#(
for %%A in (dummy) do rem
) >%file%
#for %%A in (%file%) do #(
endlocal
if %%~zA equ 0 (set %~1=OFF) else set %~1=ON
del %file%
exit /b
)
If you are willing to put up with the slight risk of two processes simultaneously trying to access the same file, the :getEchoState routine can be simplified without the need of SETLOCAL or a temp variable.
:getEchoState echoStateVar
#(
for %%A in (dummy) do rem
) >"%temp%\getEchoState.tmp"
#for %%A in ("%temp%\getEchoState.tmp") do #(
if %%~zA equ 0 (set %~1=OFF) else set %~1=ON
del "%temp%\getEchoState.tmp"
exit /b
)
The simplest way is to not turn echo off in the first place.
Instead, do what you currently do with the echo off line to the rest of your subroutine - prefix all commands in the subroutine with an # sign. This has the effect of turning off echo for that command, but keeps the echo state for future commands.
If you use commands that execute other commands, like IF or DO, you will also need to prefix the "subcommand" with an # to keep them from being printed when echo is otherwise on.
The easiest way is to extract the subroutine to another .bat file and call it via CMD /C instead of CALL this way:
echo on
... do stuff with echoing ...
cmd /C mySub
... continue to do stuff with echoing ...
exit /b
mySub.bat:
#echo off
... do stuff with no echoing ...
exit /b
This way the echo status will be automatically restored to the value it had when the CMD /C was executed; the only drawback of this method is a slightly slower execution...
Here is a straight forward solution that relies on a single temporary file (using %random% to avoid race conditions). It works and is at least localization resistant, i.e., it works for the two known cases stated by #JoelFan and #jeb.
#set __ME_tempfile=%temp%\%~nx0.echo-state.%random%.%random%.txt
#set __ME_echo=OFF
#echo > "%__ME_tempfile%"
#type "%__ME_tempfile%" | #"%SystemRoot%\System32\findstr" /i /r " [(]*on[)]*\.$" > nul
#if "%ERRORLEVEL%"=="0" (set __ME_echo=ON)
#erase "%__ME_tempfile%" > nul
#::echo __ME_echo=%__ME_echo%
#echo off
...
endlocal & echo %__ME_echo%
#goto :EOF
Add this preliminary code to increase the solution's robustness (although the odd's are high that it's not necessary):
#:: define TEMP path
#if NOT DEFINED temp ( #set "temp=%tmp%" )
#if NOT EXIST "%temp%" ( #set "temp=%tmp%" )
#if NOT EXIST "%temp%" ( #set "temp=%LocalAppData%\Temp" )
#if NOT EXIST "%temp%" ( #exit /b -1 )
:__ME_find_tempfile
#set __ME_tempfile=%temp%\%~nx0.echo-state.%random%.%random%.txt
#if EXIST "%__ME_tempfile%" ( goto :__ME_find_tempfile )
I wasn't really happy with the solution above specially because of the language issue and I found a very simple one just by comparing the result from current echo setting with the result when explicitly set OFF. This is how it works:
:: SaveEchoSetting
:: :::::::::::::::::::::::::::
:: Store current result
#echo> %temp%\SEScur.tmp
:: Store result when explicitly set OFF
#echo off
#echo> %temp%\SESoff.tmp
:: If results do not match, it must have been ON ... else it was already OFF
#for /f "tokens=*" %%r in (%temp%\SEScur.tmp) do (
#find "%%r" %temp%\SESoff.tmp > nul
#if errorlevel 1 (
#echo #echo on > %temp%\SESfix.bat
) else (
#echo #echo off > %temp%\SESfix.bat
)
)
::
:: Other code comes here
:: Do whatever you want with echo setting ...
::
:: Restore echo setting
#call %temp%\SESfix.bat
I was looking for the same solution to the same problem, and after reading your comments I had an idea (which is not the answer to the question, but for my problem is even better).
I wasn't satisfied with the cmd.exe /c mysub.cmd because it makes hard or even impossible to return variables (I didn't check) - (couldn't comment because it's the first time I post here :)
Instead noticed that all we want -in the end- is to suppress stdout:
echo on
rem call "mysub.cmd" >nul
call :mysub >nul
echo %mysub_return_value%
GOTO :eof
:mysub
setlocal
set mysub_return_value="ApplePie"
endlocal & set mysub_return_value=%mysub_return_value%
GOTO :eof
It works fine with labelled subroutines, with subroutines contained in .cmd files, and I suppose it would work fine even with the cmd.exe /c variant (or start).
It also has the plus that we can keep or discard the stderr, replacing >nul with >nul 2>&1
I note that ss64.com scares kids like me stating that with call "Redirection with & | <> also does not work as expected".
This simple test works as expected. He must have been thinking of more complex situations.
After using batch files for many years I was surprised to discover that the equals sign '=' is considered an argument separator.
Given this test script:
echo arg1: %1
echo arg2: %2
echo arg3: %3
and an invocation:
test.bat a=b c
the output is:
arg1: a
arg2: b
arg3: c
Why is that and how can it be avoided? I don't want the user of the script to account for this quirk and quote "a=b", which is counter-intuitive.
This batch script was run on Windows 7.
===== EDIT =====
A little more background: I encountered this problem when writing a bat file to start a Java application. I wanted to consume some args in the bat file and then pass the rest to the java application. So my first attempt was to do shift and then rebuild the args list (since %* is not affected by shift). It looked something like this, and that's when I discovered the issue:
rem Rebuild the args, %* does not work after shift
:args
if not "%1" == "" (
set ARGS=!ARGS! %1
shift
goto args
)
The next step was to not use shift anymore, but rather implement shift by hand by removing one character at a time from %* until a space is encountered:
rem Remove the 1st arg if it was the profile
set ARGS=%*
if not "%FIRST_ARG%" == "%KNOA_PROFILE%" goto remove_first_done
:remove_first
if not defined ARGS goto remove_first_done
if "%ARGS:~0,1%" == " " goto remove_first_done
set ARGS=%ARGS:~1%
goto remove_first
:remove_first_done
But this is ugly and might still fail in some cases I haven't considered. So finally I decided to write a Java program to deal with the argument parsing! In my case this is fine, since I am launching a server and the penalty of an extra java invocation is minimal. It's mind-boggling what you end up doing sometimes.
You might wonder why didn't I take care of the args in the Java application itself? The answer is that I want to be able to pass JVM options like -Xmx which must be processed before invoking java.
I'm guessing it does this so that /param=data is the same as /param data
I don't know any tricks to fix that stupid (probably by design) parsing issue but I was able to come up with a super ugly workaround:
#echo off
setlocal ENABLEEXTENSIONS
set param=1
:fixnextparam
set p=
((echo "%~1"|find " ")>nul)||(
call :fixparam %param% "%~1" "%~2" %* 2>nul
)
if "%p%"=="" (set "p%param%=%1") else shift
shift&set /A param=%param% + 1
if not "%~1"=="" goto fixnextparam
echo.1=%p1%
echo.2=%p2%
echo.3=%p3%
echo.4=%p4%
echo.5=%p5%
goto:EOF
:fixparam
set p%1=
for /F "tokens=4" %%A in ("%*") do (
if "%%~A"=="%~2=%~3" set p=!&set "p%1=%%A"
)
goto:EOF
When I execute test.cmd foo=bar baz "fizz buzz" w00t I get:
1=foo=bar
2=baz
3="fizz buzz"
4=w00t
5=
The problem with this is of course that you cannot do %~dp1 style variable expansion.
It is not possible to do call :mylabel %* and then use %1 either because call :batchlabel has the same parameter parsing problem!
If you really need %~dp1 handling you could use the WSH/batch hybrid hack:
#if (1==1) #if(1==0) #ELSE
#echo off
#SETLOCAL ENABLEEXTENSIONS
if "%SPECIALPARSE%"=="*%~f0" (
echo.1=%~1
echo.2=%~2
echo.3=%~3
echo.4=%~4
echo.5=%~5
) else (
set "SPECIALPARSE=*%~f0"
cscript //E:JScript //nologo "%~f0" %*
)
#goto :EOF
#end #ELSE
w=WScript,wa=w.Arguments,al=wa.length,Sh=w.CreateObject("WScript.Shell"),p="";
for(i=0;i<al;++i)p+="\""+wa.Item(i)+"\" ";
function PipeStream(i,o){for(;!i.AtEndOfStream;)o.Write(i.Read(1))}
function Exec(cmd,e){
try{
e=Sh.Exec(cmd);
while(e.Status==0){
w.Sleep(99);
PipeStream(e.StdOut,w.StdOut);
PipeStream(e.StdErr,w.StdErr);
}
return e.ExitCode;
}catch(e){return e.number;}
}
w.Quit(Exec("\""+WScript.ScriptFullName+"\" "+p));
#end
I am only answering this: >> Why is that and how can it be avoided?
My suggestion: To use a better language. No i am not joking. batch has too many quirks/nuances such as this plus a lot of other limitations, its just not worth the time coming up with ugly/inefficient workarounds. If you are running Windows 7 and higher, why not try using vbscript or even powershell. These tools/language will greatly help in your daily programming/admin tasks. As an example of how vbscript can propely take care of such an issue:
For i=0 To WScript.Arguments.Count-1
WScript.Echo WScript.Arguments(i)
Next
Output:
C:\test>cscript //nologo myscript.vbs a=b c
a=b
c
Note that it properly takes care of the arguments.
#echo off
setlocal enabledelayedexpansion
if %1 neq ~ (
set "n=%*"
for %%a in ("!n: =" "!") do (
set "s=%%a"
if !s:~0^,2!==^"^" (
set "s=!s:" "= !"
set "s=!s:""="!"
)
set "m=!m! !s!"
)
%0 ~ !m!
)
endlocal
shift
echo arg1: %~1
echo arg1: %~2
echo arg1: %~dp3
exit /b
c:> untested.bat a=b c "%USERPROFILE%"
#jeb: here another method
untested.cmd
#echo off
setlocal enabledelayedexpansion
set "param=%*"
set param="%param: =" "%"
for /f "delims=" %%a in ('echo %param: =^&echo.%') do (
set "str=%%a"
if !str:~0^,2!==^"^" (
set "str=!str:^&echo.=!"
set "str=!str:"^"= !"
set str="!str:~1,-1!"
)
set "m=!m! !str!"
)
Call :sub %m%
endlocal & goto :eof
:sub
echo arg1: %~1
echo arg2: %~2
echo arg3: %~3
echo arg4: %~4
goto :eof
Again, I can't explain the unexpected behavior, but there is a very simple solution. Just change your argument references, then quote the argument when invoking as such:
test.bat script:
echo arg1: %~1
echo arg2: %~2
echo arg3: %~3
Invoke:
test.bat "a=b" c
Output:
arg1: a=b
arg2: c
arg3:
Note that referencing the arguments with a tilde (~) removes the leading/trailing quotes from the argument.