Getting function parameters gets command line parameters - batch - batch-file

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...)

Related

How to Fix Echo command nested in if statement

I am trying to create a script that will create another batch script. However this code will not run properly, immediately exiting as soon as the if statement is evaluated.
set yes=yes
pause
IF /I %yes% == yes (
ECHO REM Music>>mf.bat
ECHO FOR /f %%i in (C:\CopyToRoot\MusicFileAndLocation.txt) do set MusicFile=%%i>>mf.bat
)`
However if the second line in the if statement is removed the code executes without issue.
What am I doing wrong? is there something I am missing about the echo statement?
Perhaps:
#Echo Off
Set "yes=yes"
Pause
If /I "%yes%"=="yes" (
>"mf.bat" (
Echo #Echo Off
Echo Rem Music
Echo For /F "UseBackQ Delims=" %%%%A In ("C:\CopyToRoot\MusicFileAndLocation.txt"^) Do Set "MusicFile=%%%%A"
)
)
Double the percents and escape any internal closing parentheses.
You need to format the code block with a bit of effort, however, it is much simpler to get rid of the block and simply call a label. Also, the batch file will consume the % so you need to double them in order to redirect the string correctly to file.
#echo off
set yes=yes
pause
IF /I "%yes%"=="yes" call :final
goto :eof
:final
echo REM Music>mf.bat
echo FOR /f %%%%i in (C:\CopyToRoot\MusicFileAndLocation.txt) do set MusicFile=%%%%i>>mf.bat
Note I use single redirect on the first rem line to overwrite the file, else it will append each time you run the code, if that was the intention, simply do double redirect >>

Why doesn't my if statement block seem to be reached in batch?

I am facing a problem with my following batch script, where I can see how the execution of the command %nr% --f !path2! never seems to happen and I don't understand the reason.
What am I doing wrong? Too many nested conditions ?
EDIT: adding WRONG code where comments are enabled
rem The call to this batch script will be this
rem C:/Projects/DevelopmentTools/SDKs/TP/B/Scrpt/Exg_Serial_Flasher.bat EF.hex DH.hex C:/Projects/DevelopmentTools/SDKs/TP/B/E/Output/CN/Exe/
setlocal enabledelayedexpansion
set nr=nr.exe
if "%1"=="" (
if "%2"=="" (
if "%3"=="" (
echo "[Error]"
set "runScript="
)
)
) else (
set "input1=%1"
set "input2=%2"
set "path3=%3%nr%"
set "myPath"=%3"
set "path1=!myPath!!input1!"
set "path2=!myPath!!input2!"
rem Control variable
set "runScript=true"
)
if defined runScript (
if exist "%path3%" (
%nr% --check
if exist !path1! (
%nr% --f !input1!
echo !ERRORLEVEL!
if !ERRORLEVEL! EQU 0 (
echo !input1! set correctly
if exist !path2! (
echo Setting !exgSerial!
%nr% --f !input2!
if !ERRORLEVEL! EQU 0 (
echo Everything went fine
)
)
)
)
)
)
Thanks!
I suggest following batch code:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
set "ExeFile=nr.exe"
if "%~1" == "" goto ArgumentError
if "%~2" == "" goto ArgumentError
if not "%~3" == "" goto ProcessArguments
:ArgumentError
echo Error: %~nx0 must be called with three arguments.
exit /B 1
:ProcessArguments
rem Assign third argument to an environment variable.
set "FilePath=%~3"
rem Replace forward slashes by backslashes which is the directory separator on Windows.
set "FilePath=%FilePath:/=\%"
rem Make sure the file path ends with a backslash.
if not "%FilePath:~-1%" == "\" set "FilePath=%FilePath%\"
set "HexFile1=%FilePath%%~1"
set "HexFile2=%FilePath%%~2"
set "ExeFile=%FilePath%%ExeFile%"
if not exist "%ExeFile%" echo Error: "%ExeFile%" does not exist. & exit /B 2
if not exist "%HexFile1%" echo Error: "%HexFile1%" does not exist. & exit /B 3
if not exist "%HexFile2%" echo Error: "%HexFile2%" does not exist. & exit /B 3
"%ExeFile%" --check
"%ExeFile%" --f "%HexFile1%"
if errorlevel 1 echo Error: Processing "%HexFile1%" failed. & exit /B 4
"%ExeFile%" --f "%HexFile2%"
if errorlevel 1 echo Error: Processing "%HexFile2%" failed. & exit /B 4
echo Everything worked fine.
endlocal
An error condition is detected as soon as possible with resulting in exiting batch file processing with an appropriate error message and exit code.
There is no need for delayed environment variable expansion which makes processing the batch file faster and avoids problems with directory or file names containing an exclamation mark.
Name of a file or the file path can contain also command line critical characters like space or one of these characters &()[]{}^=;!'+,`~.
There are no nested IF conditions making successful execution flow straight from top to bottom.
For understanding the used commands and how they work, open a command prompt window, execute there the following commands, and read entirely all help pages displayed for each command very carefully.
call /? ... explains %~1 which expands to argument 1 with removing any surrounding quotes.
echo /?
endlocal /? ... used here implicit on exiting batch file processing with exit and explicit at end of batch file.
exit /?
goto /?
if /?
rem /?
setlocal /?
See also Single line with multiple commands using Windows batch file.
I think I found the issue, it has to do with the fact that the "echos" to debug seem to influence the script execution. So in the code of my question, if I enable the echos, the script will fail running whereas if I disable them( commented out as below), the script does work.
Nevertheless, I don't understand why it does fail with the echos in first instance.
Code with disabled echos and working
rem The call to this batch script will be this
rem C:/Projects/DevelopmentTools/SDKs/TP/B/Scrpt/Exg_Serial_Flasher.bat EF.hex DH.hex C:/Projects/DevelopmentTools/SDKs/TP/B/E/Output/CN/Exe/
setlocal enabledelayedexpansion
set nr=nr.exe
if "%1"=="" (
if "%2"=="" (
if "%3"=="" (
echo "[Error]"
set "runScript="
)
)
) else (
set "input1=%1"
set "input2=%2"
set "path3=%3%nr%"
set "myPath"=%3"
set "path1=!myPath!!input1!"
set "path2=!myPath!!input2!"
rem Control variable
set "runScript=true"
)
if defined runScript (
if exist "%path3%" (
%nr% --check
if exist !path1! (
%nr% --f !input1!
rem echo !ERRORLEVEL!
if !ERRORLEVEL! EQU 0 (
rem echo !input1! set correctly
if exist !path2! (
rem echo Setting !exgSerial!
%nr% --f !input2!
if !ERRORLEVEL! EQU 0 (
echo Everything went fine
)
)
)
)
)
)

Calling function from included batch file with a parameter

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.

For loop reading empty variable

I have a subroutine that runs in my batch file, during which I output to a textfile the success of each operation. An example is this...
set Tasks=One Two Three
set LogFile=Log.txt
for %%T in (%Tasks%) do call :Operation %%T
:Operation
set LogEntry=%1
echo %LogEntry%>> %LogFile%
goto :EOF
Using this I can get one, two and three written into the text file but I also get a final entry with an empty variable.
Can anyone see what the issue is?
:operation is just a label. When the for command ends its work, the batch file continues its execution, enters the code after the label and the code inside it gets executed, but this time without any passed parameter.
Place a goto :eof or a exit /b after the for command to avoid it
set Tasks=One Two Three
set LogFile=Log.txt
for %%T in (%Tasks%) do call :Operation %%T
goto :eof
:Operation
set LogEntry=%1
echo %LogEntry%>> %LogFile%
goto :EOF

restore the previous echo status

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.

Resources