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
Related
I'm trying to make sure the user of the batchfile is inputing a 6 digit numbers only.
No more no less and only numbers.
I found the code bellow in this thread but can't figure how to modify it for my need.
Reference code
:Prompt
SET /P "UserInput=Please Enter Version as X.X.X.X: "
FOR /F "TOKENS=1-4 DELIMS=." %%i IN ("%UserInput%") DO (
SET /A n1=1*%%i
SET /A n2=1*%%j
SET /A n3=1*%%k
SET /A n4=1*%%l
)
IF NOT "%UserInput%" == "%n1%.%n2%.%n3%.%n4%" GOTO :Prompt
ECHO %UserInput%
I thought that modifying the code like so would do the trick but it does not
My modification to the reference code
:Prompt
SET /P "UserInput=Please Enter Version as XXXXXX: "
FOR /F "TOKENS=1-6" %%i IN ("%UserInput%") DO (
SET /A n1=1*%%i
SET /A n2=1*%%j
SET /A n3=1*%%k
SET /A n4=1*%%l
SET /A n4=1*%%m
SET /A n4=1*%%n
)
IF NOT "%UserInput%" == "%n1%%n2%%n3%%n4%%n5%%n6%" GOTO :Prompt
ECHO %UserInput%
What i'm I doing wrong and i'm I on the right track?
Thank you and have a nice day!
You could avoid labels or verfication by simply restricting the input to allowed characters and length, then just append any 'fixed' characters to the string.
#Echo off &Setlocal EnableDelayedExpansion
Set nVar=
Echo/Enter 6 Digit Integer:&(For /L %%# in (1 1 6)Do For /F "Delims=" %%G in ('Choice /N /C 0123456789')Do (<Nul Set /P"=%%G"&Set "nvar=!nVar!%%G"))&Echo/
Echo/Var [!nVar!] Entered
Endlocal
If you need numerical strings of varying lengths, then you can turn the above into a macro and use substring modification to supply the number of digits required.
#Echo off & Setlocal EnableDelayedExpansion
Set "Input=(Set "nVar="&Echo/Enter # Digit Integer:&(For /L %%. in (1 1 #)Do For /F "Delims=" %%G in ('Choice /N /C 0123456789')Do (<Nul Set /P"=%%G"&Set "nvar=^^!nVar^^!%%G"))&Echo/)"
%Input:#=6% & Echo/Var [!nVar!] Entered
Endlocal
You could do the following:
:INPUT
rem // Clear variable to not use previous value if user just presses {Enter}:
set "UserInput="
rem // Prompt for user input:
set /P UserInput="Please enter version as XXXXXX: "
rem // Evaluate user input:
cmd /V /C echo(!UserInput!| > nul findstr "^[0-9][0-9][0-9][0-9][0-9][0-9]$" || goto :INPUT
echo Entered version is %UserInput%.
To avoid a few other characters, like 2 and 3 (available on German keyboards), or also 1, depending on the current code page, to be unintentionally accepted, change each expression [0-9] to [0123456789].
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%"
Inside the for loop I'm trying to access the element at index count in CLs (this line of code: echo !!CLs[!count!]!!) , but I'm not sure how to do this. I don't really understand how expansion works in this case, so what you see below it me trying something out of no where.
#ECHO off
setlocal enableextensions enabledelayedexpansion
SET CLs[0]=#
SET /A count = 0
FOR /F "tokens=5" %%I IN ('some command') DO (
echo !!CLs[!count!]!! :: THIS LINE
IF NOT %%I == CLs[!count!] (
SET /A count += 1
SET CLs[!count!]=%%I
)
)
echo The item is %CLs[10]%
endlocal
Thanks
According to the post How does the Windows Command Interpreter (CMD.EXE) parse scripts? (see phase 5), the line echo !!CLs[!count!]!! cannot work, because the opening !! are collapsed to a single !, then !CLs[! is expanded to an empty string (assuming such variable is not defined), then count is returned literally, then !]! is expanded to an empty string and the final ! is dismissed. Or in other words, delayed expansion cannot be nested.
You can use call though to introduce another parsing phase, like this:
call echo %%CLs[!count!]%%
The line IF NOT %%I == CLs[!count!] ( ... ) is wrong, you must expand the right value too. However, call if will not help unfortunately, because if (like for and rem) is a special command that is recognised by the parser earlier than others, like call.
To work around that you can store the value of !count! in a for meta-variable, like %%J, for instance, to introduce another parsing phase, and use !CLs[%%J]! then, like this:
set /A "count=0"
for /F "tokens=5" %%I in ('some command') do (
for %%J in (!count!) do (
echo !CLs[%%J]!
if not "%%I" == "!CLs[%%J]!" (
set /A "count+=1"
set "CLs[!count!]=%%I"
)
)
)
Another yet slower possibility is to put the relevant code into a sub-routine:
set /A "count=0"
for /F "tokens=5" %%I in ('some command') do (
call :SUB !count!
)
goto :EOF
:SUB
echo !CLs[%~1]!
if not "%%I" == "!CLs[%~1]!" (
set /A "count+=1"
set "CLs[%~1]=%%I"
)
goto :EOF
You may also take a look at the post Arrays, linked lists and other data structures in cmd.exe (batch) script about how to deal with such pseudo-arrays.
ECHO ------------- START AT %time%
REM <!-- language: lang-dos -->
#ECHO Off
setlocal enableextensions ENABLEDELAYEDEXPANSION
SET "sourcedir=U:\sourcedir"
SET "filename1=%sourcedir%\q58209698.txt"
SET CLs[0]=#
SET /a clscnt[0]=0
SET /A count = 0
FOR /F "tokens=*" %%I IN ('type %filename1%') DO (
SET "processed="
FOR /f "tokens=1,2,3delims=[]=" %%a IN ('set cls[') DO IF /i "%%a"=="cls" (
IF "%%I"=="%%c" (SET /a clscnt[%%b]+=1&SET "processed=y")
)
IF not DEFINED processed SET /a count+=1&SET "cls[!count!]=%%I"&SET /a clscnt[!count!]=1
)
FOR /L %%a IN (0,1,%count%) DO ECHO !clscnt[%%a]! times !cls[%%a]!
ENDLOCAL
ECHO -------------------------Second way -----------------
#ECHO Off
setlocal enableextensions ENABLEDELAYEDEXPANSION
SET "sourcedir=U:\sourcedir"
SET "filename1=%sourcedir%\q58209698.txt"
SET CLs[0]=#
SET /a clscnt[0]=0
SET /A count = 0
FOR /F "tokens=*" %%I IN ('type %filename1%') DO (
SET "processed="
FOR /L %%a IN (0,1,!count!) DO (
IF "%%I"=="!cls[%%a]!" (SET /a clscnt[%%a]+=1&SET "processed=y")
)
IF not DEFINED processed SET /a count+=1&SET "cls[!count!]=%%I"&SET /a clscnt[!count!]=1
)
FOR /L %%a IN (0,1,%count%) DO ECHO !clscnt[%%a]! times !cls[%%a]!
ENDLOCAL
GOTO :EOF
I used a file named q58209698.txt containing some dummy data for my testing and chose to use the entire data line, having no suitable files where token 5 existed.
Note that as a bonus, I've added clscnt - an array of occurence-counts.
Shown: two separate ways of achieving the aim of finding/counting the unique tokens. Naturally, if the cls array is pre-loaded with the required tokens, then it's basic-programmer's-play to adjust the code to detect/report occurrences of those tokens.
The two methods are similar. In the first, set is used to list the established variables starting cls[. The first if ensures processing only the array-name cls, then either it's a repeat (set prcoessed to a value and increment the occurrences-counter) or it's a new value (when the for...%%a loop ends, processed is still undefined) so record it.
The second way is more direct, using the value of count to specifically interrogate the values in the cls array.
Here is my code for storing the words extracted from a log file into an array, which I want to use in the batch file later on.
cls
#echo off
set /a i=0
TIMEOUT 2
REM I want to save the words from newlog.txt into an array for later use in batch file.
SETLOCAL EnableDelayedExpansion
FOR /F "tokens=1,2,3 delims= " %%G IN (newlog.txt) DO ( set a[%i%]=%%H
& set /a i+=1 #echo !a[%i%]! )
#echo %i%
#echo a[%i%]
TIMEOUT 200
I just want to use them as global variables.
#echo off
set /a i=0
REM I want to save the words from newlog.txt into an array for later use in batch file.
SETLOCAL EnableDelayedExpansion
FOR /F "tokens=1,2,3 delims= " %%G IN (q48428047.txt) DO (
set a[!i!]=%%H&set /a i+=1)
SET a[
pause
GOTO :EOF
I used a file named q48428047.txt containing some dummy data for my testing.
Note that %i% will be replaced by the value of i *as it was when the *forwas encountered that is, 0.
Your second set within your parentheses is incorrect. It's missing the & before the #echo. Also, you appear to be attempting to increment i and then show the corresponding array entry, which makes no sense because you haven't yet stored anything in that array-entry, you've attempted to install it in the previous position. It won't work anyway, because %i% will be replaced by 0, not the varying value of i.
And whereas you may get the count-of-lines appearing on the screen, a[numlines] will not be defined.
Use pause to stop the batch for perusal. Preferably, run the command from the prompt, not by clicking.
The modified code reads each line of the file putting the first word in %%G, second in %%H and third in %%I. it then assigns %%H to a[currentlinenumber] and increments the line number in i.
The set a[ will display all environment variables whose names start with a[.
In addition to the answer already provided, if you really have a need to Echo each variable value immediately after it is Set then you would need to invoke a Call command:
#Echo Off
Rem Undefine any existing variables which begin with a[
For /F "Delims==" %%A In ('"(Set a[) 2>Nul"') Do Set "%%A="
Set "i=0"
ClS
SetLocal EnableDelayedExpansion
For /F "UseBackQ Tokens=2 Delims= " %%A In ("newlog.txt") Do (
Set "a[!i!]=%%A"
Call Echo "%%a[!i!]%%"
Set /A i+=1
)
If %i% GEq 1 Echo(&Echo %i% variables were defined&Echo(
(Set a[) 2>Nul
Timeout 200
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.