I'm trying to rename my files to remove any characters that cause problems in scripts. This works well for ampersand and exclamation point but when the file has the percent sign it doesn't show up in the variable to begin with. How do I pass files with special characters via for loop?
for %%v in (*) do call :validate "%%v"
exit /b
:validate
set "original=%~nx1"
set "newtitle=%original:!=%"
set "newtitle=%newtitle:&=and%"
setlocal enabledelayedexpansion
set "newtitle=!newtitle:%%= percent!"
if not "%newtitle%"=="%original%" ren %1 "%newtitle%"
Your problem is the line for %%v in (*) do call :validate "%%v", because thecall` starts the parser a second time and there all percents are evaluated a second time.
You should save the value into a variable and access these variable in your function instead.
setlocal DisableDelayedExpansion
for %%v in (*) do (
set "filename=%%~v"
call :validate
)
exit /b
:validate
setlocal EnableDelayedExpansion
set "original=!filename!"
set "newtitle=%original:!=exclam%"
set "newtitle=!newtitle:&=and!"
set "newtitle=!newtitle:%=percent!"
A possible way is to use delayed expansion:
set "newfilename=%filename:&=and%"
setlocal EnableDelayedExpansion
set "newfilename=!newfilename:%%= percent!"
endlocal
The %-sign must be escaped by doubling it in a batch file.
Note that the variable is no longer available beyond the endlocal command.
I used the quoted syntax, which is the safest way to define variables, even with white-spaces and special characters.
Related
So I know how to do simple string replacing with the SET command.
Example:
set /p a=
set a=%a:<=^<%
echo %a%
(This example will change the prompted variable to the same thing but with the < character to be ^< to be be properly echoed if needed to)
I want to do the same thing but with the % character but I can't get it to work.
#ECHO OFF
SETLOCAL
SET "something=%%he%%llo%%"
ECHO %something%
ECHO ============
SETLOCAL ENABLEDELAYEDEXPANSION
SET "anotherthing=!something:%%=x!"
endlocal&SET "anotherthing=%anotherthing%"
ECHO %something%
ECHO %anotherthing%
GOTO :EOF
Like this, you mean?
There was nothing wrong with your method, short of using doublequotes to protect from poison characters.
The main issue now, is in trying to see the content, because you'd be Echoing poison characters. To see it actually in place I included two methods, a Set command to list all variables beginning with a, (with a findstr thrown in to isolate only the one named a), and by using delayed expansion, which prevents the variable from being expanded when the command line is read/parsed. At the very bottom I included a method of showing it with the Echo command without using delayed expansion or doublequotes, as you can see to escape a caret, ^, you'll need in this case to use two more carets
#Echo Off
SetLocal EnableExtensions DisableDelayedExpansion
Set "a="
Set /P "a="
Set "a=%a:<=^<%"
Echo(%a%
Pause
(Set a) 2> NUL | %SystemRoot%\System32\findstr.exe /B /L /I "a="
Pause
SetLocal EnableDelayedExpansion
Echo(!a!
EndLocal
Pause
Echo(%a:^<=^^^<%
Pause
The full (known) rules for variable replacement can be found at SO:
Percent Expansion Rules
In short:
The search expression in a percent replacement expression can not start with a percent.
The search expression in a delayed replacement expression can not start with a exclamation mark.
But you can replace percent signs with delayed expression
setlocal EnableDelayedExpansion
set /p var=
set "var=!var:%%=REPLACE!"
echo(!var!
The percent sign itself has to be doubled here, because it collapse to a single percent in phase2 (percent expansion)
I have a function that takes a variable number of arguments. Each argument is a reference that the function will modify directly. Here is how I call the function:
set "A=" && set "B=" && set "C="
call :variadic_func A B C
echo [%A%][%B%][%C%]
goto :eof
If I don't use setlocal to limit variable scope, the function works fine. The function creates references X, Y, and Z and assigns them 1, 2, and 3. When the function returns, the caller sees that its variables A, B, and C are 1, 2, and 3. Good. Pretend that this is a variadic function and it figures out how many arguments it has at runtime.
:variadic_func
set "x=%1" && set "y=%2" && set "z=%3"
set "%x%=1" && set "%y%=2" && set "%z%=3"
goto :eof
Output:
C:\scratch\variadic_batch>variadic.bat
[1][2][3]
But I want to limit the scope of my function's variables with setlocal. So that means any values I write to X, Y, and Z get thrown away at the endlocal. How do I get the values out of the function?
:variadic_func
setlocal
set "x=%1" && set "y=%2" && set "z=%3"
set "%x%=1" && set "%y%=2" && set "%z%=3"
endlocal && (
call set "%x%=%%%%x%%%%"
call set "%y%=%%%%y%%%%"
call set "%z%=%%%%z%%%%"
)
goto :eof
Unfortunately, the calling context receives the values %x%, %y%, and %z%. I thought the code above would be expanded like so: 1. Expand %x% first to get call set A=%%A%%. Then the call gets executed and it would evaluate A=%A%. But I just end up assigning the text %A% to the variable A instead of evaluating it.
C:\scratch\variadic_batch>variadic.bat
[%x%][%y%][%z%]
Why is it not working like I expect, and how do I fix it?
(I just thought of doing a setlocal EnableDelayedExpansion before the function call so maybe delayed expansion would still be available when I do the endlocal in the function, but even if that works it'd be nice if the function didn't rely on the caller to be in a delayed expansion block... and I don't even know whether delayed expansion blocks stack)
This is an interesting topic! If you know in advance how many variables the function will get, you can assemble the appropiate line at end to return the values to the caller's environment this way:
:variadic_func
setlocal EnableDelayedExpansion
set "x=%1" & set "y=%2" & set "z=%3"
set "%x%=1" & set "%y%=2" & set "%z%=3"
for /F "tokens=1-3" %%a in ("!%x%! !%y%! !%z%!") do (
endlocal
set "%x%=%%a" & set "%y%=%%b" & set "%z%=%%c"
)
exit /B
However, if the number of variables is unknow, previous method can not be used.
(I used exit /B to terminate subroutines and goto :EOF for the main file only)
Your example is imprecise anyway, because if you don't know how many variables comes, you can NOT use fixed names as "x", "y" or "z". The only way to manage this situation is storing the names in an array and then process the array elements.
This way, before the function ends we could assemble a list of "var=value" pairs that will be executed in a FOR after the endlocal, so the variables will be defined in the caller's environment:
#echo off
call :variadic_func One Two Three
echo THREE VARS: One=[%One%] Two=[%Two%] Three=[%Three%] Four=[%Four%] Five=[%Five%]
call :variadic_func One Two Three Four Five
echo FIVE VARS: One=[%One%] Two=[%Two%] Three=[%Three%] Four=[%Four%] Five=[%Five%]
goto :EOF
:variadic_func
setlocal EnableDelayedExpansion
rem Collect the list of variable names in "var" array:
set i=0
:nextVar
if "%1" equ "" goto endVars
set /A i+=1
set var[%i%]=%1
shift
goto nextVar
:endVars
rem Assign random numbers to the variables (for example):
for /L %%i in (1,1,%i%) do (
set !var[%%i]!=!random!
)
rem Assemble the list of "var=value" assignments that will be executed at end:
set assignments=
for /L %%i in (1,1,%i%) do (
for %%v in (!var[%%i]!) do (
set assignments=!assignments! "%%v=!%%v!"
)
)
rem Execute the list of variable assignments in the caller's environment:
endlocal & for %%a in (%assignments%) do set %%a
exit /B
Output:
THREE VARS: One=[29407] Two=[21271] Three=[5873] Four=[] Five=[]
FIVE VARS: One=[30415] Two=[2595] Three=[22479] Four=[13956] Five=[26412]
EDIT:
I borrowed the method from dbenham's solution to return any number of variables with no limitations, excepting those noted by him. This is the new version:
#echo off
call :variadic_func One Two Three
echo THREE VARS: One=[%One%] Two=[%Two%] Three=[%Three%] Four=[%Four%] Five=[%Five%]
call :variadic_func One Two Three Four Five
echo FIVE VARS: One=[%One%] Two=[%Two%] Three=[%Three%] Four=[%Four%] Five=[%Five%]
goto :EOF
:variadic_func
setlocal EnableDelayedExpansion
rem Assemble the list of variable names in "var" array:
set i=0
:nextVar
if "%1" equ "" goto endVars
set /A i+=1
set var[%i%]=%1
shift
goto nextVar
:endVars
rem Assign random numbers to the variables (for example):
for /L %%i in (1,1,%i%) do (
set !var[%%i]!=!random!
)
rem Complete "var[i]=name" array contents to "var[i]=name=value"
for /L %%i in (1,1,%i%) do (
for %%v in (!var[%%i]!) do (
set "var[%%i]=%%v=!%%v!"
)
)
rem Execute the list of variable assignments in the caller's environment:
for /F "tokens=1* delims==" %%a in ('set var[') do endlocal & set "%%b"
exit /B
Antonio
Very interesting question, and I'm surprised how easy it is to solve :-)
EDIT - My original answer didn't quite answer the question, as Aacini noted in his comment. At the bottom I have a version that does directly answer the question. I've also updated my original answer to include a few more limitations that I discovered
You can return any number of variables very easily if you stipulate that the names of all variables to be returned are prepended with a constant prefix. The return variable prefix can be passed in as one of your parameters.
The following line is all that is needed:
for /f "delims=" %%A in ('set prefix.') do endlocal & set "%%A"
The entire results of the set prefix command is buffered before any iterations take place. The first iteration executes the only ENDLOCAL that is required to get back to the environment state that existed prior to the CALL. The subsequent ENDLOCAL iterations do no harm because ENDLOCAL within a CALLed function only work on SETLOCALs that were issued within the CALL. Additional redundant ENDLOCAL are ignored.
There are some really nice features of this very simple solution:
There is theoretically no limit to the number of variables that are returned.
The returned values can contain almost any combination of characters.
The returned values can approach the theoretical maximum length of 8191 bytes long.
There are also a few limitations:
The returned value cannot contain line feeds
If the final character of a returned value is a carriage return, that final carriage return will be stripped.
Any returned value that contains ! will be corrupted if delayed expansion is enabled when the CALL is made.
I have not figured out an elegant method to set a returned variable to undefined.
Here is a simple example of a variadic function that returns a variable number of values
#echo off
setlocal
set varBeforeCall=ok
echo(
call :variadic callA 10 -34 26
set callA
set varBeforeCall
echo(
call :variadic callB 1 2 5 10 50 100
set callB
set varBeforeCall
exit /b
:variadic returnPrefix arg1 [arg2 ...]
#echo off
setlocal enableDelayedExpansion
set /a const=!random!%%100
:: Clear any existing returnPrefix variables
for /f "delims==" %%A in ('set %1. 2^>nul') do set "%%A="
:: Define the variables to be returned
set "%~1.cnt=0"
:argLoop
if "%~2" neq "" (
set /a "%~1.cnt+=1"
set /a "%~1.!%~1.cnt!=%2*const"
shift /2
goto :argLoop
)
:: Return the variables accross the ENDLOCAL barrier
for /f "delims=" %%A in ('set %1. 2^>nul') do endlocal & set "%%A"
exit /b
And here is a sample run result:
callA.1=40
callA.2=-136
callA.3=104
callA.cnt=3
varBeforeCall=ok
callB.1=24
callB.2=48
callB.3=120
callB.4=240
callB.5=1200
callB.6=2400
callB.cnt=6
varBeforeCall=ok
Here is a version that is safe to CALL when delayed expansion is enabled
With a bit of extra code, it is possible to remove the limitation regarding CALLing the function while delayed expansion is enabled and the return value contains !.
The returned values are manipulated as necessary to protect ! when delayed expansion is enabled. The code is optimized such that the relatively expensive minipulation (particularly the CALL) is only executed when delayed expansion was enabled and the value contains !.
The returned value still cannot contain line feeds. A new limitation is that all carriage returns will be stripped if the returned value contains ! and delayed expansion was enabled when the CALL was made.
Here is a demo.
#echo off
setlocal
set varBeforeCall=ok
echo(
echo Results when delayed expansion is Disabled
call :variadic callA 10 -34 26
set callA
set varBeforeCall
setlocal enableDelayedExpansion
echo(
echo Results when delayed expansion is Enabled
call :variadic callB 1 2 5 10 50 100
set callB
set varBeforeCall
exit /b
:variadic returnPrefix arg1 [arg2 ...]
#echo off
:: Determine if caller has delayed expansion enabled
setlocal
set "NotDelayed=!"
setlocal enableDelayedExpansion
set /a const=!random!%%100
:: Clear any existing returnPrefix variables
for /f "delims==" %%A in ('set %1. 2^>nul') do set "%%A="
:: Define the variables to be returned
set "%~1.cnt=0"
:argLoop
if "%~2" neq "" (
set /a "%~1.cnt+=1"
set /a "%~1.!%~1.cnt!=%2*const"
shift /2
goto :argLoop
)
set %~1.trouble1="!const!\^^&^!%%"\^^^^^&^^!%%
set %~1.trouble2="!const!\^^&%%"\^^^^^&%%
:: Prepare variables for return when caller has delayed expansion enabled
if not defined NotDelayed for /f "delims==" %%A in ('set %1. 2^>nul') do (
for /f delims^=^ eol^= %%V in ("!%%A!") do if "%%V" neq "!%%A!" (
set "%%A=!%%A:\=\s!"
set "%%A=!%%A:%%=\p!"
set "%%A=!%%A:"=\q!"
set "%%A=!%%A:^=\c!"
call set "%%A=%%%%A:^!=^^^!%%" ^^!
set "%%A=!%%A:^^=^!"
set "%%A=!%%A:\c=^^!"
set "%%A=!%%A:\q="!"
set "%%A=!%%A:\p=%%!"
set "%%A=!%%A:\s=\!"
)
)
:: Return the variables accross the ENDLOCAL barrier
for /f "delims=" %%A in ('set %1. 2^>nul') do endlocal & endlocal & set "%%A"
exit /b
And some sample results:
Results when delayed expansion is Disabled
Environment variable callA not defined
callA.1=780
callA.2=-2652
callA.3=2028
callA.cnt=3
callA.trouble1="78\^&!%"\^&!%
callA.trouble2="78\^&%"\^&%
varBeforeCall=ok
Results when delayed expansion is Enabled
Environment variable callB not defined
callB.1=48
callB.2=96
callB.3=240
callB.4=480
callB.5=2400
callB.6=4800
callB.cnt=6
callB.trouble1="48\^&!%"\^&!%
callB.trouble2="48\^&%"\^&%
varBeforeCall=ok
Note how the format of the returned trouble values is consistent whether or not delayed expansion was enabled when the CALL was made. The trouble1 value would have been corrupted when delayed expansion was enabled if it were not for the extra code because of the !.
EDIT: Here is a version that directly answers the question
The original question stipulated that the names of each returned variable are supposed to be provided in the parameter list. I modified my algorithm to prefix each variable name with a dot within the function. Then I slightly modified the final returning FOR statement to strip off the leading dot. There is a restriction that the names of the returned variables cannot begin with a dot.
This version includes the safe return technique that allows CALLs while delayed expansion is enabled.
#echo off
setlocal disableDelayedExpansion
echo(
set $A=before
set $varBeforeCall=ok
echo ($) Values before CALL:
set $
echo(
echo ($) Values after CALL when delayed expansion is Disabled:
call :variadic $A $B
set $
setlocal enableDelayedExpansion
echo(
set #A=before
set #varBeforeCall=ok
echo (#) Values before CALL:
set #
echo(
echo (#) Values after CALL when delayed expansion is Enabled:
call :variadic #A #B #C
set #
exit /b
:variadic arg1 [arg2 ...]
#echo off
:: Determine if caller has delayed expansion enabled
setlocal
set "NotDelayed=!"
setlocal enableDelayedExpansion
:: Clear any existing . variables
for /f "delims==" %%A in ('set . 2^>nul') do set "%%A="
:: Define the variables to be returned
:argLoop
if "%~1" neq "" (
set /a num=!random!%%10
set ^".%~1="!num!\^^&^!%%"\^^^^^&^^!%%"
shift /1
goto :argLoop
)
:: Prepare variables for return when caller has delayed expansion enabled
if not defined NotDelayed for /f "delims==" %%A in ('set . 2^>nul') do (
for /f delims^=^ eol^= %%V in ("!%%A!") do if "%%V" neq "!%%A!" (
set "%%A=!%%A:\=\s!"
set "%%A=!%%A:%%=\p!"
set "%%A=!%%A:"=\q!"
set "%%A=!%%A:^=\c!"
call set "%%A=%%%%A:^!=^^^!%%" ^^!
set "%%A=!%%A:^^=^!"
set "%%A=!%%A:\c=^^!"
set "%%A=!%%A:\q="!"
set "%%A=!%%A:\p=%%!"
set "%%A=!%%A:\s=\!"
)
)
:: Return the variables accross the ENDLOCAL barrier
for /f "tokens=* delims=." %%A in ('set . 2^>nul') do endlocal & endlocal & set "%%A"
exit /b
And sample results:
($) Values before CALL:
$A=before
$varBeforeCall=ok
($) Values after CALL when delayed expansion is Disabled:
$A="5\^&!%"\^&!%
$B="5\^&!%"\^&!%
$varBeforeCall=ok
(#) Values before CALL:
#A=before
#varBeforeCall=ok
(#) Values after CALL when delayed expansion is Enabled:
#A="7\^&!%"\^&!%
#B="2\^&!%"\^&!%
#C="0\^&!%"\^&!%
#varBeforeCall=ok
Remove a pair of the percents from the value. call set "%x%=%%%%x%%%%" into call set "%x%=%%%x%%%"
Currently it is evaluating as follows:
:: Here is the base command
call set "%x%=%%%%x%%%%"
:: The single percents are evaluated and the doubles are escaped.
set "A=%%x%%"
:: The doubles are escaped again leaving literal % signs
"A=%x%"
You want as follows:
:: Here is the base command
call set "%x%=%%%x%%%"
:: The single percents are evaluated and the doubles are escaped.
set "A=%A%"
:: The single percents are evaluated.
"A=1"
When doing variable expansion using the call command, single percents % get evaluate first, then double percents %% seconds due to batch escaping.
Batch commands are read from left to right. So when there are an even number of % signs such as %%%%, the first and third percent signs will be utilized as escape characters for the second and fourth leaving no percent signs left to be used for variable evaluation.
I am trying to get a batch to file read a text file from dropbox and execute it as variables in the batch file.
this is what i am trying, but it does not work, please help!
SetLocal EnableDelayedExpansion
set content=
for /F "delims=" %%i in (DROPBOX-LINK-HERE) do set content=!
content! %%i
%content%
EndLocal
I'm not sure what you mean by DROPBOX-LINK-HERE, but I am using an ordinary text file for content.
You must separate each line with & or else enclose the content in parentheses and separate each line with <linefeed>. The linefeed solution is more complicated, but has fewer limitations on the content.
Any ! characters in the content will be corrupted during expansion of a FOR variable if delayed expansion is enabled. But delayed expansion is needed to preserve unquoted special characters. So delayed expansion needs to be creatively toggled on and off.
Here is code that I think does what you want.
#echo off
setlocal disableDelayedExpansion
::Define a carriage return variable
for /f %%a in ('copy /Z "%~dpf0" nul') do set "CR=%%a"
::Create a newline variable
set LF=^
::The above 2 blank lines are critical - do not remove
::Both CR and LF should be expanded using delayed expansion only.
::Load the content into a variable.
::We want to separate lines with linefeed, but FOR /F won't preserve linefeeds.
::So use carriage return as a place holder for now.
set "content=("
setlocal enableDelayedExpansion
for /f %%C in ("!CR! ") do (
endlocal
for /f "delims=" %%A in (test.txt) do (
setlocal enableDelayedExpansion
for /f "delims=" %%B in ("!content!") do (
endlocal
set "content=%%B%%C%%A"
)
)
)
::Now replace carriage returns with newline and append terminating )
setlocal enableDelayedExpansion
for /f %%C in ("!CR! ") do for %%N in ("!LF!") do set "content=!content:%%C=%%~N!%%~N)"
::Execute the content
endlocal&%content%
The code works, but there are limitations to the type of code that can be executed from a variable.
Variables cannot be expanded by using normal expansion unless you use CALL. For example, a line like echo %var% will not work, but call echo %var% will work. Another option is to use delayed expansion. SETLOCAL EnableDelayedExpansion and ENDLOCAL can be included in the content as needed.
You cannot CALL or GOTO a :LABEL within the content.
That's all I can remember at the moment, but there may be (probably are) other restrictions.
I have one question though:
If the content is already in a text file, then why not simply give the text file a .BAT extension and execute it?
I have a script that has a lot of use of the SETLOCAL ENABLEDELAYEDEXPANSION command, so I start the script off that way (less headaches). However, it does not allow you to use the ! character without escaping each instance of it (and I want to create a long line of !s for an error logging section =D ) and I don't want to escape every one of them.
Is there a way to temporarily break out of SETLOCAL, then reenter it keeping all previously created variables within the original SETLOCAL?
For example:
SETLOCAL ENABLEDELAYEDEXPANSION
set var=HELLO
ECHO %var%
ENDLOCAL
SETLOCAL ENABLEDELAYEDEXPANSION
ECHO %var%
The 2nd ECHO will not give you the previous value of var
EDIT: ^ will not allow you to escape the ! inside SETLOCAL ENABLEDELAYEDEXPANSION
setlocal DisableDelayedExpansion
set var=Value! with! many! Bangs!
setlocal EnableDelayedExpansion
echo !var!
You can nest it like Aacini shows it.
Or you can use the return technic or escape ! inside a EnableDelayed block with ^^!
Setlocal EnableDelayedExpansion
echo 1^^!^^!^^!^^!^^!^^!^^!
REM *** Or use a self removing quote
echo !="! 2^!^!^!^!^!^!
Setlocal DisableDelayedExpansion
echo 3 !!!!!!!!
set var=Hello
(
endlocal
rem Bring the value behind the endlocal barrier
set var=%var%
)
echo var is still there, %var%
The return technic can also be used for exclamation marks, but then it is a bit more complex.
It can be found at Make an environment variable survive ENDLOCAL
I have a batch file that computes a variable via a series of intermediate variables:
#echo off
setlocal
set base=compute directory
set pkg=compute sub-directory
set scripts=%base%\%pkg%\Scripts
endlocal
%scripts%\activate.bat
The script on the last line isn't called, because it comes after endlocal, which clobbers the scripts environment variable, but it has to come after endlocal because its purpose is to set a bunch of other environment variables for use by the user.
How do I call a script who's purpose is to set permanent environment variables, but who's location is determined by a temporary environment variable?
I know I can create a temporary batch file before endlocal and call it after endlocal, which I will do if nothing else comes to light, but I would like to know if there is a less cringe-worthy solution.
The ENDLOCAL & SET VAR=%TEMPVAR% pattern is classic. But there are situations where it is not ideal.
If you do not know the contents of TEMPVAR, then you might run into problems if the value contains special characters like < > & or|. You can generally protect against that by using quotes like SET "VAR=%TEMPVAR%", but that can cause problems if there are special characters and the value is already quoted.
A FOR expression is an excellent choice to transport a value across the ENDLOCAL barrier if you are concerned about special characters. Delayed expansion should be enabled before the ENDLOCAL, and disabled after the ENDLOCAL.
setlocal enableDelayedExpansion
set "TEMPVAR=This & "that ^& the other thing"
for /f "delims=" %%A in (""!TEMPVAR!"") do endlocal & set "VAR=%%~A"
Limitations:
If delayed expansion is enabled after the ENDLOCAL, then the final value will be corrupted if the TEMPVAR contained !.
values containing a lineFeed character cannot be transported
If you must return multiple values, and you know of a character that cannot appear in either value, then simply use the appropriate FOR /F options. For example, if I know that the values cannot contain |:
setlocal enableDelayedExpansion
set "temp1=val1"
set "temp2=val2"
for /f "tokens=1,2 delims=|" %%A in (""!temp1!"|"!temp2!"") do (
endLocal
set "var1=%%~A"
set "var2=%%~B"
)
If you must return multiple values, and the character set is unrestricted, then use nested FOR /F loops:
setlocal enableDelayedExpansion
set "temp1=val1"
set "temp2=val2"
for /f "delims=" %%A in (""!temp1!"") do (
for /f "delims=" %%B in (""!temp2!"") do (
endlocal
set "var1=%%~A"
set "var2=%%~B"
)
)
Definitely check out jeb's answer for a safe, bullet proof technique that works for all possible values in all situations.
2017-08-21 - New function RETURN.BAT
I've worked with DosTips user jeb to develop a batch utility called RETURN.BAT that can be used to exit from a script or called routine and return one or more variables across the ENDLOCAL barrier. Very cool :-)
Below is version 3.0 of the code. I most likely will not keep this code up-to-date. Best to follow the link to make sure you get the latest version, and to see some example usage.
RETURN.BAT
::RETURN.BAT Version 3.0
#if "%~2" equ "" (goto :return.special) else goto :return
:::
:::call RETURN ValueVar ReturnVar [ErrorCode]
::: Used by batch functions to EXIT /B and safely return any value across the
::: ENDLOCAL barrier.
::: ValueVar = The name of the local variable containing the return value.
::: ReturnVar = The name of the variable to receive the return value.
::: ErrorCode = The returned ERRORLEVEL, defaults to 0 if not specified.
:::
:::call RETURN "ValueVar1 ValueVar2 ..." "ReturnVar1 ReturnVar2 ..." [ErrorCode]
::: Same as before, except the first and second arugments are quoted and space
::: delimited lists of variable names.
:::
::: Note that the total length of all assignments (variable names and values)
::: must be less then 3.8k bytes. No checks are performed to verify that all
::: assignments fit within the limit. Variable names must not contain space,
::: tab, comma, semicolon, caret, asterisk, question mark, or exclamation point.
:::
:::call RETURN init
::: Defines return.LF and return.CR variables. Not required, but should be
::: called once at the top of your script to improve performance of RETURN.
:::
:::return /?
::: Displays this help
:::
:::return /V
::: Displays the version of RETURN.BAT
:::
:::
:::RETURN.BAT was written by Dave Benham and DosTips user jeb, and was originally
:::posted within the folloing DosTips thread:
::: http://www.dostips.com/forum/viewtopic.php?f=3&t=6496
:::
::==============================================================================
:: If the code below is copied within a script, then the :return.special code
:: can be removed, and your script can use the following calls:
::
:: call :return ValueVar ReturnVar [ErrorCode]
::
:: call :return.init
::
:return ValueVar ReturnVar [ErrorCode]
:: Safely returns any value(s) across the ENDLOCAL barrier. Default ErrorCode is 0
setlocal enableDelayedExpansion
if not defined return.LF call :return.init
if not defined return.CR call :return.init
set "return.normalCmd="
set "return.delayedCmd="
set "return.vars=%~2"
for %%a in (%~1) do for /f "tokens=1*" %%b in ("!return.vars!") do (
set "return.normal=!%%a!"
if defined return.normal (
set "return.normal=!return.normal:%%=%%3!"
set "return.normal=!return.normal:"=%%4!"
for %%C in ("!return.LF!") do set "return.normal=!return.normal:%%~C=%%~1!"
for %%C in ("!return.CR!") do set "return.normal=!return.normal:%%~C=%%2!"
set "return.delayed=!return.normal:^=^^^^!"
) else set "return.delayed="
if defined return.delayed call :return.setDelayed
set "return.normalCmd=!return.normalCmd!&set "%%b=!return.normal!"^!"
set "return.delayedCmd=!return.delayedCmd!&set "%%b=!return.delayed!"^!"
set "return.vars=%%c"
)
set "err=%~3"
if not defined err set "err=0"
for %%1 in ("!return.LF!") do for /f "tokens=1-3" %%2 in (^"!return.CR! %% "") do (
(goto) 2>nul
(goto) 2>nul
if "^!^" equ "^!" (%return.delayedCmd:~1%) else %return.normalCmd:~1%
if %err% equ 0 (call ) else if %err% equ 1 (call) else cmd /c exit %err%
)
:return.setDelayed
set "return.delayed=%return.delayed:!=^^^!%" !
exit /b
:return.special
#if /i "%~1" equ "init" goto return.init
#if "%~1" equ "/?" (
for /f "tokens=* delims=:" %%A in ('findstr "^:::" "%~f0"') do #echo(%%A
exit /b 0
)
#if /i "%~1" equ "/V" (
for /f "tokens=* delims=:" %%A in ('findstr /rc:"^::RETURN.BAT Version" "%~f0"') do #echo %%A
exit /b 0
)
#>&2 echo ERROR: Invalid call to RETURN.BAT
#exit /b 1
:return.init - Initializes the return.LF and return.CR variables
set ^"return.LF=^
^" The empty line above is critical - DO NOT REMOVE
for /f %%C in ('copy /z "%~f0" nul') do set "return.CR=%%C"
exit /b 0
#ECHO OFF
SETLOCAL
REM Keep in mind that BAR in the next statement could be anything, including %1, etc.
SET FOO=BAR
ENDLOCAL && SET FOO=%FOO%
The answer of dbenham is a good solution for "normal" strings, but it fails with exclamation marks ! if delayed expansion is enabled after ENDLOCAL (dbenham said this too).
But it will always fail with some tricky contents like embedded linefeeds,
as the FOR/F will split the content into multiple lines.
This will result into strange behaviour, the endlocal will executed multiple times (for each line feed), so the code isn't bullet proof.
There exists bullet proof solutions, but they are a bit messy :-)
A macro version exists SO:Preserving exclamation ..., to use it is easy, but to read it is ...
Or you could use a code block, you can paste it into your functions.
Dbenham and I developed this technic in the thread Re: new functions: :chr, :asc, :asciiMap,
there are also the explanations for this technic
#echo off
setlocal EnableDelayedExpansion
cls
for /f %%a in ('copy /Z "%~dpf0" nul') do set "CR=%%a"
set LF=^
rem TWO Empty lines are neccessary
set "original=zero*? %%~A%%~B%%~C%%~L!LF!one&line!LF!two with exclam^! !LF!three with "quotes^&"&"!LF!four with ^^^^ ^| ^< ^> ( ) ^& ^^^! ^"!LF!xxxxxwith CR!CR!five !LF!six with ^"^"Q ^"^"L still six "
setlocal DisableDelayedExpansion
call :lfTest result original
setlocal EnableDelayedExpansion
echo The result with disabled delayed expansion is:
if !original! == !result! (echo OK) ELSE echo !result!
call :lfTest result original
echo The result with enabled delayed expansion is:
if !original! == !result! (echo OK) ELSE echo !result!
echo ------------------
echo !original!
goto :eof
::::::::::::::::::::
:lfTest
setlocal
set "NotDelayedFlag=!"
echo(
if defined NotDelayedFlag (echo lfTest was called with Delayed Expansion DISABLED) else echo lfTest was called with Delayed Expansion ENABLED
setlocal EnableDelayedExpansion
set "var=!%~2!"
rem echo the input is:
rem echo !var!
echo(
rem ** Prepare for return
set "var=!var:%%=%%~1!"
set "var=!var:"=%%~2!"
for %%a in ("!LF!") do set "var=!var:%%~a=%%~L!"
for %%a in ("!CR!") do set "var=!var:%%~a=%%~3!"
rem ** It is neccessary to use two IF's else the %var% expansion doesn't work as expected
if not defined NotDelayedFlag set "var=!var:^=^^^^!"
if not defined NotDelayedFlag set "var=%var:!=^^^!%" !
set "replace=%% """ !CR!!CR!"
for %%L in ("!LF!") do (
for /F "tokens=1,2,3" %%1 in ("!replace!") DO (
ENDLOCAL
ENDLOCAL
set "%~1=%var%" !
#echo off
goto :eof
)
)
exit /b
I want to contribute to this too and tell you how you can pass over an array-like set of variables:
#echo off
rem clean up array in current environment:
set "ARRAY[0]=" & set "ARRAY[1]=" & set "ARRAY[2]=" & set "ARRAY[3]="
rem begin environment localisation block here:
setlocal EnableExtensions
rem define an array:
set "ARRAY[0]=1" & set "ARRAY[1]=2" & set "ARRAY[2]=4" & set "ARRAY[3]=8"
rem `set ARRAY` returns all variables starting with `ARRAY`:
for /F "tokens=1,* delims==" %%V in ('set ARRAY') do (
if defined %%V (
rem end environment localisation block once only:
endlocal
)
rem re-assign the array, `for` variables transport it:
set "%%V=%%W"
)
rem this is just for prove:
for /L %%I in (0,1,3) do (
call echo %%ARRAY[%%I]%%
)
exit /B
The code works, because the very first array element is queried by if defined within the setlocal block where it is actually defined, so endlocal is executed once only. For all the successive loop iterations, the setlocal block is already ended and therefore if defined evaluates to FALSE.
This relies on the fact that at least one array element is assigned, or actually, that there is at least one variable defined whose name starts with ARRAY, within the setlocal/endlocal block. If none exist therein, endlocal is not going to be executed. Outside of the setlocal block, no such variable must be defined, because otherwise, if defined evaluates to TRUE more than once and therefore, endlocal is executed multiple times.
To overcome this restrictions, you can use a flag-like variable, according to this:
clear the flag variable, say ARR_FLAG, before the setlocal command: set "ARR_FLAG=";
define the flag variable inside of the setlocal/endlocal block, that is, assign a non-empty value to it (immediately before the for /F loop preferrably): set "ARR_FLAG=###";
change the if defined command line to: if defined ARR_FLAG (;
then you can also do optionally:
change the for /F option string to "delims=";
change the set command line in the for /F loop to: set "%%V";
Something like the following (I haven't tested it):
#echo off
setlocal
set base=compute directory
set pkg=compute sub-directory
set scripts=%base%\%pkg%\Scripts
pushd %scripts%
endlocal
call .\activate.bat
popd
Since the above doesn't work (see Marcelo's comment), I would probably do this as follows:
set uniquePrefix_base=compute directory
set uniquePrefix_pkg=compute sub-directory
set uniquePrefix_scripts=%uniquePrefix_base%\%uniquePrefix_pkg%\Scripts
set uniquePrefix_base=
set uniquePrefix_pkg=
call %uniquePrefix_scripts%\activate.bat
set uniquePrefix_scripts=
where uniquePrefix_ is chosen to be "almost certainly" unique in your environment.
You could also test on entry to the bat file that the "uniquePrefix_..." environment variables are undefined on entry as expected - if not you can exit with an error.
I don't like copying the BAT to the TEMP directory as a general solution because of (a) the potential for a race condition with >1 caller, and (b) in the general case a BAT file might be accessing other files using a path relative to its location (e.g. %~dp0..\somedir\somefile.dat).
The following ugly solution will solve (b):
setlocal
set scripts=...whatever...
echo %scripts%>"%TEMP%\%~n0.dat"
endlocal
for /f "tokens=*" %%i in ('type "%TEMP%\%~n0.dat"') do call %%i\activate.bat
del "%TEMP%\%~n0.dat"
For surviving multiple variables: If you choose to go with the "classic"
ENDLOCAL & SET VAR=%TEMPVAR% mentioned sometimes in other responses here (and are satisfied that the drawbacks shown in some of the responses are addressed or are not an issue), note that you can do multiple variables, a la ENDLOCAL & SET var1=%local1% & SET var2=%local2%.
I share this because other than the linked site below, I have only seen the "trick" illustrated with a single variable, and like myself some may have incorrectly assumed that it only "works" for a single variable.
Docs: https://ss64.com/nt/endlocal.html
To answer my own question (in case no other answer comes to light, and to avoid repeats of the one I already know about)...
Create a temporary batch file before calling endlocal that contains the command to call the target batch file, then call and delete it after endlocal:
echo %scripts%\activate.bat > %TEMP%\activate.bat
endlocal
call %TEMP%\activate.bat
del %TEMP%\activate.bat
This is so ugly, I want to hang my head in shame. Better answers are most welcome.
How about this.
#echo off
setlocal
set base=compute directory
set pkg=compute sub-directory
set scripts=%base%\%pkg%\Scripts
(
endlocal
"%scripts%\activate.bat"
)