I have been successfully using the CALL mechanism to allow one batch file to CALL another to setup environment variables. This code has been working well for over a year on Windows XP.
However, it does not appear to be working in the same way on Windows 7. The variables exist in the second batch file just before the EXIT /B statement. But, they do not exist upon the return to the first batch file.
Some trivial examples seem to work as expected, but the large batch scripts do not.
Has anyone had difficulties with this or know any workarounds?
In years of advanced batch scripting, I have never seen a CALL fail to preserve environment variables unless the called script (or label) set the variable when SETLOCAL was still active. There is an implicit ENDLOCAL for every active SETLOCAL from the within the CALL upon termination of the CALL.
It sounds like you have put in diagnostic messages prior to your EXIT /B to confirm that your variables are defined. I would take it one step further and add multiple ENDLOCAL statements prior to your diagnostic messages. I suspect you will then see your values dissapear prior to EXIT /B. You can add as many ENDLOCAL as you want. ENDLOCAL will never affect SETLOCAL that occurred prior to the CALL.
The most likely explanation is that either your script has somehow changed from XP to Win 7, or else there is some context change in your Win 7 environment that is exercising some aspect of the code that hadn't been exposed before.
Try this:
(
ENDLOCAL
SET "_Var1=Some Variable You want to exist"
SET "_Var2=Some Other Variable You want to exist"
EXIT /b 0
)
Also make sure you call Batch 2 from batch 1 like this:
CALL "\\PathToBatch2\Batch2.cmd"
ALTERNATELY you can do this:
CMD One:
REM Script: Batch1
#(
SETLOCAL
ECHO OFF
SET "_CallBatch2=C:\PathToBatch2\Batch2.cmd"
SET "_SetCmd=CALL :SetCMD "
SET "_RecievedVarList="
SET "_RecievedVar1=" & REM -- Note only done to show this is being created, normally you won't know or care what variables are being returned.
SET "_eLvL=0"
)
CALL :Main
(
ENDLOCAL
EXIT /b %_eLvl%
)
:Main
FOR %%A IN (CALL "%_CallBatch2%") DO (
IF /I "%%~A" EQU "SET" (
REM CALL %%A "%%~B" would work too
%_SetCmd% %%~B
) ELSE (
REM Looks like this was intended to be some output, show it.
ECHO.%%A %%B
)
)
FOR /F "Tokens=1*" %%A IN (%_RecievedVarList%) DO (
REM ECHO the Variable's name and it's contents:
CALL ECHO."%%~A" = "%%%%~A%%"
)
GOTO :EOF
:SetCMD
SET "%*"
FOR /F "Tokens=1 Delims==" %A IN ("%*") DO (
REM Store vars to output later to check their values.
SET "_RecievedVarList=%_RecievedVarList% "%A""
)
GOTO :EOF
.
CMD Two:
REM Script: Batch2
#(
SETLOCAL
ECHO OFF
)
CALL :Main
(
ENDLOCAL
EXIT /b %_eLvl%
)
:Main
ECHO.SET "_RecievedVar1=This is Recieved Var 1"
ECHO.SET "_RecievedVar2=This is Recieved Var 2"
GOTO :EOF
Related
This question already has answers here:
How can I call a function that is defined in another batch file, in my current batch?
(4 answers)
Closed 8 months ago.
I have a Batch file that's a library of functions like
:findmsbuild
if exist msbuildpath.txt (
for /f %%i in (msbuildpath.txt) do set MSBUILD="%%i\MSBuild.exe"
) else (
set VSWHERE="%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
for /f "delims=" %%i in ('!VSWHERE! -latest -prerelease -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe') do set MSBUILD="%%i"
)
exit /b
And the official documentation for call says that I should be able to call directly to a label in another file like
call library.bat :findmsbuild
but what happens instead is, the interpreter executes library.bat from the first line. What am I doing wrong? The calling Batch file is approximately
setlocal enableextensions enabledelayedexpansion
cd "%~dp0"
call library.bat :findmsbuild
echo %MSBUILD%
What are you doing wrong? Trusting Microsoft's documentation.
You can either call externalbatch or call :internalroutine.
To do what you want, you'd need to put a line like goto %1 at the start of your library file - and remember then that the parameter list has the label at the start, so you'd need to shift it, and %* would retain the label as the first parameter.
Preface: job / function is used interchangeably below.
It is possible to call functions in this way, however you need to script it into your library.bat program. That means assessing provided arguments, determining if they correspond to a valid job, and determining the action to take such as calling a job with %* arguments or providing help output.
Note: using Call %* to execute a label supplied as %1 when calling library.bat will:
desirable behaviour:
Allow arguments provided to a called job to reside in their intended positions as :jobname becomes %0 when called.
Potentially undesirable behaviours:
Result in the doubling of any carets present in the arguments.
If relevent to any jobs, you'll need to account for that in the relevant job.
Expand any %variables% present in the argument string supplied to library.bat (defined or undefined)
An example of executing the desired functionality:
#Echo off
Echo(
Set "ExitCode=0"
2> nul (
Rem // search this file for labels matching Argument 1 at line beginning.
%SystemRoot%\System32\Findstr.exe /BLI "%~1" "%~f0" > nul && (
Call %* || (
Call Set "ExitCode=%%Errorlevel%%"
(Call )
)
) || (
Echo("%~1"|%SystemRoot%\System32\Findstr.exe /R "[:][.]*" && (
Rem // No labels matching Argument 1 at line beginning. Argument 1 begins with a ":"
Echo(Invalid job. Valid jobs:
For /f "tokens=1" %%J in ('%SystemRoot%\System32\Findstr.exe /R "^[:][.]*" "%~f0"')Do Echo(%%J
Exit /b 1
) || If not "%~1" == "" (
Rem // Argument 1 does not begin with a ":"
Echo(No job call attempt. Args: %*
Echo(
Echo(To call a job:
Echo(Call "%~f0" :jobname %%*
)
)
)
Exit /b %ExitCode%
:demo
Echo(Jobname %0 JobArgs: [%*]
Exit /b 0
:job
Echo(Jobname %0 JobArgs: [%*]
REM // demostrate return of errorlevel in ExitCode variable facilitate && || logic
set /A ExitCode=%random% %% 2
Exit /b %ExitCode%
I'm feeling a bit constrained by batch's limitations and unsure the best way to code some logic. I have a hybrid Batch/VBScript for printing and decided the best way to confirm the VBScript had finished was by utilizing an external file as a placeholder.
The logic is like this:
Batch Script --> Create tmpfile --> Run VBScript
VBScript --> Print Job --> Delete File
That part works, but in my "call script" I have an array of files that I use a FOR LOOP to iterate over and print, but I can't figure out how to code the logic WHILE FILE EXISTS DON'T RUN NEXT PRINT JOB. I have figured out how to "loop" until the file doesn't exist, but this uses GoTo statements and they don't work inside FOR LOOPS. I tried using "EXIT /B" which I thought just exits a function, but it just closed the main script all together.
Here is my "call script" which needs some help~
::This is a placeholder file to check if VBScript is running
SET File="%TEMP%\jobrunning.txt"
del /F /Q %File%
::Print Each File in Array
for /l %%n in (0,1,%arrsize%) do (
SET FILEPATH="FILEPATH=!file[%%n]!
GoTo file_exists
)
::Is Job Still Running?
:file_exists
IF EXIST %File% (
GoTo :file_exists
) ELSE (
ECHO %PFUNC% %FILEPATH%
%PFUNC% %FILEPATH%
EXIT /B
)
The issue was using GoTo w/ EXIT /B. GoTo just jumps to a LINELABEL, and EXIT /B exits PROCESS IE: The Script. CALL opens a SUBPROCESS and EXIT /B returns to the main PROCESS allow further code execution. (It's the same as calling an external script and closing it).
This is what I ended up using to test and just deleted the file manually after each iteration of the array.
#ECHO OFF
setlocal enabledelayedexpansion
::This is a placeholder file to check if VBScript is running
SET File="%TEMP%\jobrunning"
del /F /Q %File%
::Set arrsize -1
set arrsize=4
set arrayline[0]=1A
set arrayline[1]=2A
set arrayline[2]=3A
set arrayline[3]=4A
set arrayline[4]=5A
::read it using a FOR /L statement
for /l %%n in (0,1,%arrsize%) do (
copy /y NUL %TEMP%\jobrunning.txt >NUL
TIMEOUT 2
CALL :file_exists
echo !arrayline[%%n]!
)
PAUSE
GoTo :EOF
:Is Job Still Running?
:file_exists
IF EXIST %File% (
GoTo :file_exists
) ELSE (
EXIT /B
)
I need to make delayed expansion inside of delayed expansion, i.e. smth like:
!PARAMS[!BEFORE_LAST!]!
Of course, the above is not valid, so I tried to workaround it with for-loop, but with no success:
SETLOCAL EnableExtensions EnableDelayedExpansion
SET PARAM_COUNT=0
FOR %%P IN (%*) DO (
SET /A PARAM_COUNT+=1
SET PARAMS[!PARAM_COUNT!]=%%P
IF !PARAM_COUNT! GTR 1 (
SET /A BEFORE_LAST = !PARAM_COUNT!-1
FOR /L %%G IN (!BEFORE_LAST!) DO SET BEFORE_LAST_PARAM=!PARAMS[%%G]!
IF "!BEFORE_LAST_PARAM!"=="--buildroot" (
REM perfrom some actions here
)
)
)
ENDLOCAL
How could I achieve the explained behavior?
delayed expansion inside delayed expansion doesn't work, try this:
CALL SET "myvar=%%PARAMS[!BEFORE_LAST!]%%"
This is not so much of an answer, but using #Enduro's Answer, I solved my problem. I am just giving an example here, to see how to use it.
This answer was the only one that worked for me! After a LOT of searching; Basically I was trying to generate a file with all android app package names, without version codes. We have used a gradle script to generate names in a particular format also, something like: com.package.appname-releaseVersion.apk.
Here is the original code for generating file (apps.ls) with required package names, which I mainly use like this, inside android shell: for app in $(cat < apps.ls ); do monkey -p $app 1; done;. It just launches all those apps to make sure their services start running and registered.
My Original Script
#echo off
setlocal EnableDelayedExpansion
cd "C:\remoteUpdate"
>apps.ls (
for /r %%v in (*.apk) do (
set "name=%%~nv"
call :indexof "!name!" "-" idx
call echo %%name:~0,!!idx!!%%
)
)
C:\dos2unix\bin\dos2unix.exe apps.ls
#echo Done generating apps.ls
:indexof [%1 - string ; %2 - find index of ; %3 - if defined will store the result in variable with same name]
::http://ss64.org/viewtopic.php?id=1687
#echo off
setlocal enableDelayedExpansion
set "str=%~1"
set "s=!str:%~2=&rem.!"
set s=#%s%
if "%s%" equ "#%~1" endlocal& if "%~3" neq "" (set %~3=-1&exit /b 0) else (echo -1&exit /b 0)
set "len=0"
for %%A in (2187 729 243 81 27 9 3 1) do (
set /A mod=2*%%A
for %%Z in (!mod!) do (
if "!s:~%%Z,1!" neq "" (
set /a "len+=%%Z"
set "s=!s:~%%Z!"
) else (
if "!s:~%%A,1!" neq "" (
set /a "len+=%%A"
set "s=!s:~%%A!"
)
)
)
)
endlocal & if "%~3" neq "" (set %~3=%len%)
exit /b 0
Now the issue was that I needed to filter out certain packages out of my apps.ls file,
Changes
All I needed to do was change the code inside the for loop like so:
CALL SET "truncname=%%name:~0,!!idx!!%%"
if NOT "!truncname!"=="com.blhealthcare.package1" if NOT "!truncname!"=="com.android.package2" echo !truncname!
Everything I tried before that, using only set was not working. However using call with set worked really well. I cannot find any documentation on this as well, as it's difficult to even search this issue. What I noticed was that call echo %%name:~0,!!idx!!%% was printing correctly, whereas echo %%name:~0,!!idx!!%% was printing something like name:~0,28 and not working. I couldn't understand it then, but today I can understand from #Endoro's answer that
delayed expansion inside delayed expansion doesn't work
for BAT file when I write following script I don't get the name of the .jpg files in the folder. What's the error and how it can be achieved?
for /f "tokens=*" %%f in ('dir /b *.jpg') do (
SET newname=%%f
SET front=%newname:~0,6%
echo %front%
)
When a compound statement enclosed in parentheses is to be executed,
the statement is first parsed from the open parenthesis all of the
way to the matching close-parenthesis.
At this time, any %var% is replaced by that var's value from the
environment AT THE TIME IT IS PARSED (ie its PARSE-TIME value.)
THEN if the statement seems valid, it is executed.
There are three common ways of accessing the RUN-TIME value of the
variable (as a FOR loop executes, for instance.)
1/ SETLOCAL ENABLEDELAYEDEXPANSION which switches to a mode where
!var! may be used to access the runtime value of var
2/ CALL set var2=%%var%% to set the value of var2 from the
runtime value of var
3/ Executing a subroutine, internal or external within which %var%
will be the runtime value.
#ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
FOR %%i IN (1 2 3) DO (
ECHO START of run %%i
ECHO using ^!time^! : !time! - PARSE TIME was %time%
CALL ECHO using CALL %%%%TIME%%%% : %%TIME%%
CALL :report
timeout /t 5
ECHO using ^!time^! : !time!
CALL ECHO using CALL %%%%TIME%%%% : %%TIME%%
CALL :report
ECHO END of run %%i
ECHO.
)
GOTO :eof
:report
ECHO :report says TIME is %TIME%
GOTO :eof
A few items to note:
The instruction
IF ERRORLEVEL n echo errorlevel is n OR GREATER
ALWAYS interprets the RUN-TIME value of ERRORLEVEL
IF SET VAR ALWAYS interprets the RUN-TIME value of VAR
The magic variables like ERRORLEVEL and TIME should never
be SET. If you execute
SET ERRORLEVEL=dumb
then ERRORLEVEL will adopt the value dumb because the current
value in the environment takes priority over the system-assigned value.
This is one option - but ! characters in the filenames will cause issues.
#echo off
setlocal enabledelayedexpansion
for /f "delims=" %%f in ('dir /b *.jpg') do (
SET "newname=%%f"
SET "front=!newname:~0,6!"
echo !front!
)
pause
I am developing a batch file to collect websphere products information, it seems to work fine except for some cases.
For some reason under some circumstances versionInfo.bat -maintenancePackages is called but the following code (check for manageprofiles.bat), it seems like it's returning from the :check section after calling versionInfo.
My Windows batch writing skills are very rusty, other improvements are welcome.
#echo off
SetLocal EnableDelayedExpansion
set tmpfile=%TEMP%\tmpdone.txt
echo. > %tmpfile%
For /F "eol= delims=| tokens=13" %%a in (%windir%\vpd.properties) Do (
set check=%%a
call :check
)
goto eof
:check
Set skip=No
For /F "eol= delims=|" %%a in (%tmpfile%) Do (
if "%%a" == "%check%" set skip=YES
)
if %skip% == YES goto eof
echo %check%>>%tmpfile%
if exist "%check%\bin\versionInfo.bat" "%check%\bin\versionInfo.bat" -maintenancePackages
echo %check%\bin\manageprofiles.bat
if exist "%check%\bin\manageprofiles.bat" "%check%\bin\manageprofiles.bat" -listProfiles
goto eof
:del
echo Done
del %tmpfile%
:eof
You need to use call to run batch files from another batch file. Otherwise cmd won't return from the called one. So your code should read:
if exist "%check%\bin\versionInfo.bat" call "%check%\bin\versionInfo.bat" -maintenancePackages
echo %check%\bin\manageprofiles.bat
if exist "%check%\bin\manageprofiles.bat" call "%check%\bin\manageprofiles.bat" -listProfiles
goto :eof
(Also no need for a :eof jump label, you can just use the goto :eof special syntax to exit the batch file directly. I usually only use such a jump label if I need some cleanup to do first, but I name it differently then, to avoid confusion :-))