escaping "!" in a for-loop with delayed variable expansion - batch-file

I need to escape "!" (and other special chars) in a for-loop where delayed variable expansion is enabled
I've tried manually escaping the loop-varible with string substitution ^^! in the loop-variable, %%a but with no luck at all. Is it already too late after the for has read them? If so, how the heck can I even accomplish this?
Below is a short function. The only relevant part here is the for-loop and the echo statement. That is printing out whole lines from a file every X'th line, and those lines are file-paths. They (sometimes) contain characters like "!" and other troublesome special characters. I just want echo here to pass it without interpreting it at all - but instead it ends up deleting my "!" chars.
For my use they need to be exactly correct or they are useless as they must correlate to actual files later on in what I use them for.
setlocal EnableDelayedExpansion
:SplitList
if [%3] == [] goto :SplitListUsage
set inputfile=%~1
set outputfile=%~2
set splitnumber=%~3
set skipLines=0
set skipLines=%~4
if %skipLines% GTR 0 (
set skip=skip=%skipLines%
) else (
set skip=
)
#echo off > %outputfile%
set lineNumber=0
for /f "tokens=* %skip% delims= " %%a in (%inputfile%) do (
set /a modulo="!lineNumber! %% !splitnumber!"
if "!modulo!" equ "0" (
echo %%a >> !outputfile!
)
set /a lineNumber+=1
)
exit /B 0

Quick solution:
if !modulo! equ 0 (
setlocal DisableDelayedExpansion
(echo %%a)>> "%outputfile%"
endlocal
)
Better solution:
Based on your code sample, there is no need to have delayed expansion enabled for the entire code,in fact you should keep it disabled to not mess with the file names or input strings which may contain !, and enabled it when necessary:
setlocal DisableDelayedExpansion
REM Rest of the code...
for /f "tokens=* %skip% delims= " %%a in (%inputfile%) do (
set /a "modulo=lineNumber %% splitnumber"
setlocal EnableDelayedExpansion
for %%m in (!modulo!) do (
endlocal
REM %%m is now have the value of modulo
if %%m equ 0 (
(echo %%a)>> "%outputfile%"
)
)
set /a lineNumber+=1
)
Side notes:
There are some other issues with your code which as you might have noticed, some of them are corrected in the above solutions. But to not distract you from main issue you had, I covered them here separately.
There is also room for improving the performance of the code when writing to the outputfile.
Here is the re-written code which covers the rest:
#echo off
setlocal DisableDelayedExpansion
:SplitList
if "%~3"=="" goto :SplitListUsage
set "inputfile=%~1"
set "outputfile=%~2"
set "splitnumber=%~3"
set "skipLines=0"
set /a "skipLines=%~4 + 0" 2>nul
if %skipLines% GTR 0 (
set "skip=skip=%skipLines%"
) else (
set "skip="
)
set "lineNumber=0"
(
for /f "usebackq tokens=* %skip% delims= " %%a in ("%inputfile%") do (
set /a "modulo=lineNumber %% splitnumber"
setlocal EnableDelayedExpansion
for %%m in (!modulo!) do (
endlocal
REM %%m is now have the value of modulo
if %%m equ 0 echo(%%a
)
set /a lineNumber+=1
)
)>"%outputfile%"
You didn't protect the variable assignments with double qoutes " e.g. set inputfile=%~1. If the now naked batch parameter %~1 contains spaces or special characters like & your batch files fails, either fatally with a syntax error or at execution time with incorrect data. The recommended syntax is to use set "var=value" which does not assign the quotes to the variable value but provides protection against special characters. It also protects the assignment against the accidental trailing spaces.
The %inputfile% may contain special characters or spaces, so it should be protected by double quoting it when using in the FOR /F's IN clause. When double quoting the file name in FOR /F the usebackq parameter must also be used.
With SET /A there is no need to expand the variable values: set /a modulo="!lineNumber! %% !splitnumber!". The variable names can be used directly, and it will work correctly with or without delayed expansion.
Using (echo %%a) >> "%outputfile%" inside a FOR loop introduces a severe performance penalty specially with a large number of iterations, because at each iteration the output file will be opened, written to and then closed. To improve the performance The whole FOR loop can redirected once. Any new data will be appended to the already opened file.
The odd looking echo( is to protect against the empty variable values, or when the variable value is /?. Using echo %%a may print the `ECHO is on/off' message if the variable is empty or may print the echo usage help.
In the main solutions, The reason I've used (echo %%a)>> "%outputfile%" instead of echo %%a >> "%outputfile%" is to prevent outputting the extra space between %%a and >>. Now you know the reason for using echo(, it is easy to understand the safer alternative: (echo(%%a)>> "%outputfile%"

Related

How to remove spaces in the File name of the text file?

The file is not added in the destination path if it has spaces in the file name.
For example, if the filename is textfile1.txt -> it will be added in the destination path. However, if the directory filename has space like this text file4.txt it will not be added.
Is there a way to remove the spaces of the filename?
Here is the image:
Here is my main concern:
Here is my script:
#ECHO off
TITLE (c) ASDG
SETLOCAL EnableDelayedExpansion
SET locationPath=C:\Textfiles\
SET destinationPath=E:\Textfiles\
SET status=success
SET countMetadata=0
SET countPDF=0
SET countJPEG=0
ECHO Executing the program...
FOR /R %locationPath% %%g IN (*.txt) DO (
CD %%~dpg
IF EXIST *.txt* (
FOR /F "skip=1 tokens=1* delims=|" %%a IN (%%~nxg) DO (
SET /a countMetadata+=1
ECHO %%~dpa%%a^| %%b >> %destinationPath%^%%~nxg
)
IF %status% == success (
ECHO %%g has been successfully added
ECHO %%g >> %destinationPath%^logs.txt
)
)
)
After reformatting to use indentation to show code blocks (parenthesised code that is parsed, substituting %variables%, then executed), I then applied various recommendations:
Use set "var=value" for setting string values - this avoids problems caused by trailing spaces. Don't assign " or a terminal backslash or Space. Build pathnames from the elements - counterintuitively, it is likely to make the process easier. If the syntax set var="value" is used, then the quotes become part of the value assigned.
Use set /a to assign numeric values. No quotes required.
THIS CODE WILL NOT FULLY PERFORM THE REQUIRED TASK
Without comments to explain why some code is used, it's difficult to provide guidance.
#ECHO OFF
SETLOCAL EnableDelayedExpansion
TITLE (c) ASDG
SET "locationPath=C:\Textfiles"
SET "destinationPath=E:\Textfiles"
SET "status=success"
SET /a countMetadata=0
SET /a countPDF=0
SET /a countJPEG=0
ECHO Executing the program...
FOR /R "%locationPath%" %%g IN (*.txt) DO (
CD %%~dpg
IF EXIST *.txt* (
FOR /F "usebackq skip=1 tokens=1* delims=|" %%a IN ("%%~nxg") DO (
SET /a countMetadata+=1
ECHO %%~dpa%%a^| %%b >> %destinationPath%\%%~nxg
)
IF !status! == success (
ECHO %%g has been successfully added
ECHO %%g >> %destinationPath%\logs.txt
)
)
)
GOTO :EOF
Modifications explanation
I prefer the setlocal to be the second line. YMMV.
Set commands modified as explained above
locationpath in the for /R quoted - it may contain separators like spaces.
%%~nxg quoted in for...%%a as it may contain spaces
usebackq added to for...%%a as %%~nxg is now quoted
I prefer to avoid ADFNPSTXZ (in either case) as metavariables (loop-control variables)
ADFNPSTXZ are also metavariable-modifiers which can lead to difficult-to-find bugs
(See `for/f` from the prompt for documentation)
Not sure what is going on with the following echo, but destinationPath no longer has the terminal \. Same comment applies to the following ECHO %%g.
!staus! replaces %status% (logically) as logically, status may change within the block - the delayed expansion trap
BUT status is not being varied within the block, so its value will always be its initial value, success.
Using Boolean
So - a few things for OP to clean up...

How to make a random chance "function" in Batch

I'm making a small little game I want a random chance to get certain items (e.g. if the random number is greater than 10 but less than 15 then you will get a certain item). Here's what I've already tried which resulted in a crash.
set /a chance= %random% %%30+1
if %chance% gtr 10 && lss 30 (
set /a %nails%+1
echo You got nails! %chance%
)
This piece right here was just a test, but should give you an idea of what I am going for. This is really the only way I can think of doing it. If you could help, please do! :)
I see a number of problems in that code:
set /a chance= %random% %%30+1
if %chance% gtr 10 && lss 30 (
set /a %nails%+1
echo You got nails! %chance%
)
Going through them:
The if statement is not valid, && is the "execute next command if previous command worked" conjunction, not a general "and" operator. To do what you want would be:if %chance% gtr 10 if %chance% lss 30.See here for a way to do and and or in cmd language.
The command set /a %nails%+1 does not actually change nails in any way, it just evaluates an expression and throws it away. You need an assignment to assign a value, and you don't need the variable markers in this case:set /a "nails += 1".
If you're using delayedexpansion to print out nails (and you should be), you need a ! both before and after the variable name:echo You got !nails! %chance%.
As an aside, you'll probably notice I have a penchant for quoting my set /a expressions and spacing them nicely - I find this aids readability.
That will fix some specific problems but, to be honest, you're probably better off making a generic function that can give you a yes/no answer for some probability of an event happening. That way, you can reuse it anywhere you need it.
You can use a function like chance, shown below in a complete program, to decide whether something should happen based on a percentage:
#echo off
goto :main
:chance
setlocal enableextensions enabledelayedexpansion
set retcode=1==0
set /a "value = %random% %% 100"
rem echo %value% rem uncomment for debugging
if %value% lss %2 set retcode=1==1
endlocal && set %1=%retcode%
goto :eof
:main
call :chance result 50
echo %result%
It should be called with both a variable name to put the result into, and the percentage level you want to use. For example, if you wanted to set a variable hasdied based on a 5% chance, you would call it with:
call :chance hasdied 5
if %hasdied% goto :handlebeingdead
The function contains a number of features which probably bear explanation:
The setlocal command ensures that no variables escape the scope of this function (but see below), useful for proper encapsulation.
The value variable is set to some random value between 0 and 99 inclusive. It's not perfectly distributed since %random% will give you a value up to 32767 so will be slightly skewed toward numbers less than 68. Said skew is probably not enough to concern yourself with.
This value is then compared with the threshold you provided (the second argument) to decide the return value true or false.
The return value is rather sneaky in that it gives you an expression that you can put into an if statement without having to do an explicit comparison like:if %hasdied%==1By returning such an equality comparison directly, you can just use the return value as if it was boolean.
The endlocal then cleans up any variable changes that have been made in this function, including the return code. However, the fact that the substitutions on this line take place before any of it is executed means that the set part of it will already have the correct value of retcode substituted before the endlocal cleans it up. This is a way to have specific variables "escape" the scope bounded by setlocal/endlocal. The retcode value is therefor placed in the parameter whose name you provided as the first argument.
The set %1= part of that command is a way to allow you to specify what variable should receive the value in the call itself, akin to myvar = function(). That stops you from having to allocate a hard-coded variable name to each function and then assign it to another variable after the call.
And, of course, the goto :eof is simply a return instruction.
I'm pretty sure the && does not exist in batch. Nested if statements work:
set /a chance= %random% %%30+1
echo %chance%
IF %chance% GTR 10 (IF %chance% LSS 15 (
echo You got nails! %chance%
))
You cannot use && like that. You need to run the if statement twice to match both gtr and lss you can put them one after the other:
#echo off
set /a chance=%random% %%30+1
if %chance% gtr 10 if %chance% lss 30 (
set /a nails+=1
echo You got nails! %chance%
)
Also note the correct way of increasing a variable set /a nails+=1
the if condition approach works and all, but is somewhat clunky if your going to be scripting in many loot situations. it is by far easier to use an array setup with a macro that can access ranges within the array to allow you to simply and easily script loot boxes that roll different items by using substring modification to change the index of the array the random number can access. a demonstration:
#Echo off
:new
::: -------------------------------------------------------------------|| MACRO DEFINITIONS
Setlocal DisableDelayedExpansion
(Set \n=^^^
%=DNR=%
)
rem ********************* Display any existing character names for continuation or deletion of characters
If Exist "%TEMP%\%~n0_*_save.bat" (Echo/Your Characters:&Echo/&(For /F "Delims=" %%G in ('Dir "%TEMP%\%~n0_*_save.bat" /B')Do For /F "Tokens=2 Delims=_" %%o in ("%%~nG") Do < Nul Set /P "=[%%o] ")&Echo/)
:character
Set /P "Name=Name: "
If Exist "%TEMP%\%~n0_%Name%_save.bat" (Echo/[C]ontinue / [D]elete?&For /F "Delims=" %%O in ('Choice /N /C:cd')Do If /I "%%O"=="C" (Goto :playon)Else (Del /P "%TEMP%\%~n0_%Name%_save.bat" & Goto :character))
If "%Name%"=="" Goto :character
:playon
rem *** Inventory Macro. Displays all elements for the given group and their current values.
rem ::: Usage: %INV:#=$varname[%
Set "INV=Echo/&(For /F "Tokens=2 Delims==" %%i in ('Set #') Do (Set "VN=%%i"&^< Nul Set /P"=[!VN:$=!:!%%i!] "))&Echo/"
rem *** Autosave macro. Can be incorperated into other macro's
rem ::: Usage: %Save%
Set SAVE=(For /F "Tokens=1 Delims==" %%i in ('Set $') Do (If not "!%%i!"=="" Echo/Set "%%i=!%%i!"))^>"%TEMP%\%~n0_!name!_save.bat"
rem *** Location Display Macro with autosave macro included
rem ::: Usage: %Loc:#=LocationLABEL%
Set "Loc=(Set "$Loc=#"&Title !$Loc:_= !)&%Save%"
rem *** Loot box Macro to generate random loot from specified range of an indexed array
rem *** !random! %%4 + Index# will access an index range between the index # and 4 above the index number.
rem ::: Usage: %Loot:#=index#%
Set "LOOT=(For /F "UsebackQ Delims=" %%i in (`"Set /A i#=!Random! %%4 + #"`) Do For /F "UsebackQ Delims=" %%v in (`"Set /A v#=!Random! %%3 + 1"`) Do (Set "VN=!$Loot[%%i]:$=!"&Echo/You got %%v !VN!&Set /A "!$Loot[%%i]!+=%%v")) 2> Nul & %SAVE%"
rem *** the below macros /I /V and /P are not used in this example. - They are an optional method for defining
rem *** variables prefixed with $ that automatically saves them for reloading
rem ::: usage: %/I:V=Varname%Input Prompt String:
Set "/I=For %%n in (1 2)Do If %%n==2 (Set /P "$V=!$PromptStr:$=!: "&%Save%)Else Set $PromptStr="
rem ::: usage: %/P:V=Varname%VariableValue
Set "/V=For %%n in (1 2)Do If %%n==2 (Set "$V=!str!"&%Save%)Else Set str="
rem ::: usage: %/A:V=Varname%=Equation
Set "/A=For %%n in (1 2)Do If %%n==2 (Set /A "$V!sum!"&%Save%)Else Set sum="
rem *** Wait prompt Macro
rem ::: usage: %Wait:#=Integer value for time in seconds%Wait Prompt String
Set "Wait=For %%n in (1 2)Do If %%n==2 (Timeout # /Nobreak > Nul & (Pause | Echo/!Output!) 2> Nul )Else Set Output="
rem *** Array definition macro. Asigns the element names to an indexed Groupname (Array), With each element being assigned an initial 0 value
Rem ::: Usage: %DefArray%{VarGroupName}{Element names as list}
Set DefArray=For %%n in (1 2) Do if %%n==2 (%\n%
Set "i#=0"%\n%
For /F "Tokens=1,2 Delims={}" %%G in ("!List!") Do (%\n%
For %%i in (%%~H) Do (%\n%
Set "$%%~G[!i#!]=$%%i"%\n%
Set "$%%i=0"%\n%
Set /A i#+=1 ^> Nul%\n%
)%\n%
)%\n%
) Else Set List=
Set Menu=CLS^&Set "Copt="^&For %%n in (1 2) Do if %%n==2 (%\n%
Echo/[E]xit%\n%
For %%G in (!OPTS!)Do (%\n%
Set "opt=#%%~G"%\n%
Set "opt=!opt:_= !"^&Set "Opt=!Opt:~,-1!"%\n%
Set "Copt=!Copt!%%~G"%\n%
Echo/!Opt! [%%~G]%\n%
)%\n%
(For /F "Delims=" %%O in ('Choice /N /C !Copt!E')Do If "%%O"=="E" (Endlocal^&Endlocal^&Set "Name="^&Goto :New) Else (CLS^&Goto :#%%O))%\n%
) Else Set OPTS=
::: -------------------------------------------------------------------|| END MACRO DEFINITIONS
::: -------------------------------------------------------------------|| Example Script
REM // required to be enabled PRIOR to macro Use, AFTER definition.
Setlocal EnableDelayedExpansion
%DefArray%{Loot}{Wood Nails Ore Leather Gold Silver Bronze Jade}
IF Exist "%TEMP%\%~n0_!name!_save.bat" (
Call "%TEMP%\%~n0_!name!_save.bat"
Goto :!$Loc!
)
:Menu
%Loc:#=Menu%
%Menu:#=Loot_Box_% "1" "2"
Goto :Menu
:Loot_Box_1
%Loc:#=Loot_Box_1%
%Loot:#=0%
%INV:#=$Loot[%
%Wait:#=1%
Goto :Menu
:Loot_Box_2
%Loc:#=Loot_Box_2%
%Loot:#=4%
%INV:#=$Loot[%
%Wait:#=1%Demo wait prompt
Goto :Menu

Batch File Count all occurrences of a character within a string

I have a version number 17.06.01.01 and I would like to know how many entries there are split by a period.
My last piece of code was;
setlocal ENABLEDELAYEDEXPANSION
for /F "tokens=1-10 delims=." %%a in ("17.09.01.04.03") do (
set /a "iCount+=1"
echo %%a, !iCount!
)
endlocal
I've tried a host of 'For' commands but seem to be getting further away each time.
replace every dot with a space (as a delimiter). Then count number of tokens:
#echo off
set "string=17.09.01.04.03"
set count=0
for %%a in (%string:.= %) do set /a count+=1
echo %count%
(May give false results, if there are other spaces, commas or tabs in the string, but should work nice for your example of version strings containing only numbers and dots)
#treintje:
echo off
set "string=1>7.0 9.0&1.04.0!3"
set count=0
:again
set "oldstring=%string%"
set "string=%string:*.=%"
set /a count+=1
if not "%string%" == "%oldstring%" goto :again
echo %count%
For the sample string you provided, Stephan's approach is perfectly sufficient.
However, if the string contains token separators (white-spaces, ,, ;, =), quotation marks ("), wild-cards (*, ?) or other special characters (^, &, (, ), >, <, |) it might probably fail. You could of course replace most of such characters by others in advance, and use delayed expansion rather than immediate one, but this still does not resolve all issues (for instance, you cannot replace all = or * characters due to the sub-string replacement syntax).
The following approach however can deal with every arbitrary combination of all such characters. Basically it replaces every period (.) with a new-line (line-feed) character and lets find /C /V "" count the total number of lines.
#echo off
set "string=17.09.01.04.03"
setlocal EnableDelayedExpansion
if defined string (set ^"strtmp=!string:.=^
%= empty string =%
!^") else set "strtmp="
for /F %%C in ('cmd /V /C echo(^^!strtmp^^!^| find /C /V ""') do set /A "count=%%C-1"
echo "!string!" contains %count% periods.
endlocal
The periods become replaced by line-feeds in advance, using delayed expansion in order not to fail if any special characters occur. The for /F loop executes the command line cmd /V /C echo(^^!string^^!| find /C /V "" and captures its output in a variable, reduced by one as find /C /V "" actually returns the number of lines, which are separated by one less line-feeds (hence periods), originally. The double-escaped exclamation marks ^^! are needed in order to ensure that the variable strtmp is actually expanded within the explicitly invoked inner-most cmd instance, because otherwise, the contained multi-line string is not correctly transported into the pipe (|).
A different approach comparing strLen before and after replacing the dots with nothing.
#Echo off&SetLocal EnableExtensions EnableDelayedExpansion
set "string=17.09.01.04.03"
set "str2=%string:.=%"
call :strlen string ret
call :strlen str2 ret2
Set /A "Dots=ret-ret2"
echo Number of dots in %string% is %Dots%
goto :Eof
:strLen string len
:$source http://www.dostips.com/?t=Function.strLen
(SETLOCAL ENABLEDELAYEDEXPANSION
set "str=A!%~1!"
set "len=0"
for /L %%A in (12,-1,0) do (set /a "len|=1<<%%A"
for %%B in (!len!) do if "!str:~%%B,1!"=="" set /a "len&=~1<<%%A")
)
ENDLOCAL&IF "%~2" NEQ "" SET /a %~2=%len%
EXIT /b
Test with strings from comments:
Pass: set "string=Tim says:"There is a cat and a dog""
Fail: set "string=1>7.0% 9.0&1.04.%0!3"
Pass: set "string=Tim says:"There is a cat. And a dog""

How to split a variable into an array in batch

I'm trying to write a text adventure in batch, so I want to know how I can split variable like set userinput=take book and turn it into an array. I want to be able to write a program that will split strings into array items at each space. I've done this in a lot of other languages. There are several other questions like this, but I don't feel like they answer my question.
This task is probably much more difficult than you realize. There are lots of "simple" methods, but none of the simple ones are robust.
For example, a simple FOR loop can parse out individual tokens, but it will be broken by *, ?, and possibly ".
A robust solution requires a significant amount of code.
Here is my solution, with extensive comments:
#echo off
:: Start out with delayed expansion disabled, and define "user input"
:: I intentionally include "problem" characters
setlocal disableDelayedExpansion
set "userinput=take book! & ;MustPreserve EmptyLinesAreIgnored"
:: Initialize count
set "cnt=0"
:: Enable and use delayed expansion to protect against poison characters
setlocal enableDelayedExpansion
:: Substitute <LineFeed> for each <space>
(set parsed=!userinput: =^
%= Do not remove or alter this line =%
!)
:: Iterate each line, setting delims and eol to <space> to preserve all tokens
for /f "eol= delims= " %%A in ("!parsed!") do (
%= Return to delayed expansion disabled on first iteration, else ! is lost =%
if "!" equ "" endlocal
%= Increment the count =%
set /a cnt+=1
%= Temporarily enable delayed expansion to capture current count in FOR variable =%
setlocal enableDelayedExpansion
for %%N in (!cnt!) do (
endlocal
%= Save the array value =%
set "token.%%N=%%A"
)
)
::Print the results. Safe array access requires delayed expansion
setlocal enableDelayedExpansion
for /l %%N in (1 1 !cnt!) do echo token.%%N=!token.%%N!
It looks a bit better without all the comments
#echo off
setlocal disableDelayedExpansion
set "userinput=take book! & :MustPreserveColon ;MustPreserve EmptyLinesAreIgnored"
set "cnt=0"
setlocal enableDelayedExpansion
(set parsed=!userinput: =^
%= Do not remove or alter this line =%
!)
for /f "eol= delims= " %%A in ("!parsed!") do (
if "!" equ "" endlocal
set /a cnt+=1
setlocal enableDelayedExpansion
for %%N in (!cnt!) do (
endlocal
set "token.%%N=%%A"
)
)
setlocal enableDelayedExpansion
for /l %%N in (1 1 !cnt!) do echo token.%%N=!token.%%N!
And here is the output:
token.1=take
token.2=book!
token.3=&
token.4=;MustPreserve
token.5=EmptyLinesAreIgnored

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

Resources