For loop reading empty variable - batch-file

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

Related

Open batch file with/without argument

How can I accommodate opening a batch file either with an argument
goto Item_%1
or without one?
set /p x=Select:
goto Item_%x%
What exactly are you aiming to achieve?
My guess is a label targeting System that goes to a certain label when called.
If so, attempting to check %~1 if no parameter is sent will cause the batch to fail as the variable does not exist.
IF NOT DEFINED %~1 does not get around this either.
The way to do it is to assign the label you wish to goto to a variable and Do an IF NOT DEFINED check on the variable- With enableDelayedExpansion
Example:
#ECHO OFF
SETLOCAL enableDelayedExpansion
IF NOT DEFINED !LabelIS! GOTO default
IF DEFINED !LabelIS! GOTO !LabelIS!
:default
ECHO default starting label
pause
exit
:target
ECHO Arrived at target
pause
exit
Note: If relying on user input to set the target, You'll need to vailidate that the input corresponds to a valid label.
Amendment:
If Not "%1"=="" goto somewhere
Is the wrong Syntax
The correct Syntax that will do what the question asked is:
If "%~1"=="" goto default
goto %~1
The following checks the parameter or input by verifying that the corresponding label exists. If there is an empty input or the label does not exist, it keeps asking the user for the label, showing possible options. (As you probably don't want to show each label to the user ("internal" labels), you can hide some by just adding <space># - a label is a single word only, everything after the first space is ignored, but can be used to distiguish them)
#echo off
setlocal
set "label=%~1"
:retry #
findstr /ix ":%label%" "%~f0" && goto %label%
echo Invalid or no Label given: [%label%]. Possible options:
for /f "delims=:" %%a in ('findstr /xr ":.*[^#]$" "%~f0"') do echo/ %%a
set /p "label=Label? "
goto :retry
:Item_1
echo reached Item_1
goto :eof
:anotherlabel
echo reached anotherlabel
goto :eof
:errorhandling #
echo to demonstrate a hidden label
goto :eof

Getting function parameters gets command line parameters - batch

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

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.

Call one batch script in another batch script and perform operation on the values returned by first script

I have a batch script "first.bat" that returns a list of values and an exit code. My task is to call this script in another script "second.bat" and perform the operations on the values returned by the "first.bat" only if the calling of first script has not returned an error code.
I have called the first.bat and stored its output in a text file. After that I check the result code and perform the operation by reading the text file if the result code is success code. And after the operation I delete the text file
Below is the code snippet of second.bat
#ECHO OFF
call first.bat >t
if /i %errorlevel%==100 (
echo Performing operation
for /F "tokens=1" %%a in (t) do echo %%a
for /F "tokens=2" %%a in (t) do echo %%a
del t
) else (
echo Error occurred
)
I want to know can the same thing be done in some elegant way by not writing the output of first.bat to text file.
#ECHO OFF
SETLOCAL
for %%i in (var return1 return2 bat1err) do set "%%i="
FOR /f %%i IN ('call first') DO SET var=%%i
FOR /f "tokens=1,2" %%i IN ("%var%") DO set return1=%%i&set return2=%%j
Should set return1 and return2 to your two values BUT the value of ERRORLEVEL generated by FIRST.bat can't be retrieved.
In your original code, the file t would be generated in any case - and would be zero-length if there was no output from first.bat. I'm confused by your action in only deleting t if no-error-occurs....
So - what we'd really need to do is to change FIRST.bat a little.
If first.bat does not use SETLOCAL, then
set bat1err=%errorlevel%
at an appropriate point would return bat1err set to the errorlevel.
If first.bat does use SETLOCAL, then life gets a little more complicated
set bat1err=%errorlevel%
at an appropriate point would set bat1err in the same way, but you would need to use
ENDLOCAL&set bat1err=%bat1err%
before exiting. This is a parsing trick, cashing in on the way in which the line is first parsed, then executed. What happens is that the line is actually executed as
endlocal&set bat1err=22
or whatever, setting BAT1ERR within the context of the calling, not the called batch.
Another way would be to include %errorlevel% in your output, and simply change the analysis to
FOR /f "tokens=1,2,3" %%i IN ("%var%") DO set bat1err=%%i&return1=%%j&set return2=%%k
OR, depending on quite what the output of first.bat is, you may be able to do it a third way:
If first.bat produces no output for error but a line of output for success,
FOR /f %%i IN ('call first') DO SET var=%%i
if defined var (
FOR /f "tokens=1,2" %%i IN ("%var%") DO set return1=%%i&set return2=%%j
) else (echo Error occurred)
and again return, return2 and var can be analysed with IF DEFINED to make decisions.
That really depends on information we don't have.

Batch command line argument matching

I really can't understand why this refuses to work.
#ECHO OFF
SET CURRDIR=%CD%
if [%1%]==[1] GOTO ONE
if [%1%]==[2] GOTO TWO
if [%1%]==[3] GOTO THREE
:ONE
call "%CURRDIR%\PlanningProduct.bat"
:TWO
call "%CURRDIR%\Organization.bat"
:THREE
call "%CURRDIR%\Measure.bat"
pause
I did the following in the command line
I:\BatchMania>I:\BatchMania\Home.bat 1
and the output I get is funny as follows:
Planning
Organization
Measure
Press any key to continue . . .
This is weird. Hope to never write this kind of code!!!
There are several items that need attention here:
You have implemented "fall-through" scenarios, where THREE or TWO+THREE is executed in 2 distinct cases, or ONE+TWO+THREE in all other cases;
I actually do not think the if statements work as intended: [%1%]==[1] should either be [%1%]==[1%] or [%1]==[1];
Should double backslashes be a problem when this script is run from the root, then consider using %__CD__%;
All if statements can be omitted if you just use goto batch%~1 (or similar) and rename your labels; OR
All number labels can be omitted if you just specify the batch to call in the if statements and/or use if-else constructs.
Here are some alternative implementations:
#ECHO OFF
set CURRDIR=%CD%
goto :BATCH%~1 2>NUL
goto :UHOH
:BATCH1
call "%CURRDIR%\PlanningProduct.bat"
goto :DONE
:BATCH2
call "%CURRDIR%\Organization.bat"
goto :DONE
:BATCH3
call "%CURRDIR%\Measure.bat"
goto :DONE
:UHOH
echo Invalid parameter "%~1"
:DONE
pause
#ECHO OFF
set CURRDIR=%CD%
if "%~1"=="1" (
call "%CURRDIR%\PlanningProduct.bat"
) else if "%~1"=="2" (
call "%CURRDIR%\Organization.bat"
) else if "%~1"=="3" (
call "%CURRDIR%\Measure.bat"
) else (
echo Invalid parameter "%~1"
)
pause
#ECHO OFF
set CURRDIR=%CD%
set BAT=
if "%~1"=="1" set BAT=PlanningProduct.bat
if "%~1"=="2" set BAT=Organization.bat
if "%~1"=="3" set BAT=Measure.bat
call "%CURRDIR%\%BAT%" 2>NUL
pause
Does the below produce what you expect?
:ONE
call "%CURRDIR%\PlanningProduct.bat"
GOTO OUT
:TWO
call "%CURRDIR%\Organization.bat"
GOTO OUT
:THREE
call "%CURRDIR%\Measure.bat"
:OUT
pause
After it junps to ONE and executes the call, it is just going to continue on the next line (TWO). A label does not change the execution sequence, it is still going to parse the file line by line unless you jump somewhere.
Either jump away to a specific point:
...
:ONE
call "%CURRDIR%\PlanningProduct.bat"
GOTO DONE
:TWO
...
:DONE
pause
or end the batch:
:ONE
call "%CURRDIR%\PlanningProduct.bat"
pause
GOTO :EOF

Resources