Use a for loop variable to call a predefined variable - batch-file

I'm looking to use a variable which gets defined inside a for loop and use it to call a predefined variable.
For example: To start the program "Slack" I need to call the variable %Slack%.
The batch file will take your input, so If I input "Slack", the variable !filename[%%i]! will be assigned "Slack". Once that is done, I wish to call %Slack% using !filename[%%i]!. As per logic: %!filename[%%i]!%
Using %!filename[%%i]!% will make the batch file crash and close.
Setting set file=!filename[%%i]! and running it with %file% will return nothing.
Any smart trick?
#echo off
setlocal EnableDelayedExpansion
:: Predefined variables
for /f "delims== tokens=1,2" %%G in (D:\programs.txt) do set %%G=%%H
set /P "filenames=Enter programs to install: "
:: Puts input into array
set n=0
for %%a in (%filenames%) do (
set /A n+=1
set "filename[!n!]=%%~a"
)
:: Run variables imported from programs.txt
for /L %%i in (1,1,%n%) do (
:: This is where I want !filename[%%i]! to be called as %!filename[%%i]!%
)
pause
endlocal
program.txt example
Slack=C:\Users\username\AppData\Local\slack\slack.exe
Text=C:\textfile.txt
Program=C:\program.bat & C:\program_license.txt

I don't like to use the CALL hack described in the comments because 1), it is relatively slow, and 2) it causes an extra round of parsing that can require additional escaping (not an issue here). Even worse, it will corrupt quoted ^ - "^" becomes "^^", and there is no escape sequence that can prevent that.
My solution is to add an additional FOR loop so that the expansion can be staged.
for /L %%i in (1,1,%n%) do for %%A in ("!filename[%%i]!") do echo !%%~A!
This is certainly more verbose than the CALL hack, but it is a good code pattern to learn because it is more universally applicable.

Related

How to use variables set in a sub called in a FOR loop, with delayed expansion

This code loops through the contents of "Fire" folder on PC, which contains several subfolders. For each subfolder it runs another FOR to look for a match to directories on a tablet. If no match found it calls subroutine :missing to set variables about the missing directory.
When I get back to main routine I'm unable to echo the vars set in the sub. I've tried many permutations of !name[%CNT%]! (doubling/tripling the % and !) but can't get it right. Hope someone can lend a hand.
#Echo off
SETLOCAL EnableDelayedExpansion
:main
Set /A CNT=0
:: Fire folder contains dirs that may have been deleted from tablet.
For /F %%G IN ('dir /b %Fire%') DO (
SET thisdir=%%G
SET /A CNT+=1
:: set command that will look for %%G on the tablet
set cmd="adb shell ls /system/priv-app/!thisdir!"
:: This loop returns nul if dir exists on device
For /F "tokens=1* delims=:" %%g in ('!cmd!') do (set _N=%%h)
If NOT "!_N!"=="" call :missing !thisdir!
:: HERE'S THE PROBLEM: With this syntax I get "CNT" "CNT"
Echo AFTER THE CALL: "!name[!CNT!]!" "!pkg[!CNT!]!"
:: this displays: "pkg[3]]
Echo AFTER THE CALL: "%!name[!CNT!]%!" [!pkg[!CNT!]!]
)
:missing
:: Both %CNT% and !CNT! work here
Set pkg[%CNT%]=%1
:: set friendly name for this dir
If "%1"=="com.name.video" set "name[%CNT%]=Video"
If "%1"=="com.name.games" set "name[%CNT%]=Games"
. . .
:: THIS WORKS HERE IN THE SUB. HOW TO DISPLAY IN MAIN?
Echo MISSING: !name[%CNT%]! (!pkg[%CNT%]!)
:: Output: MISSING: Video (com.name.video)
exit /b
To display in main loop:
call Echo AFTER THE CALL: "%%name[!CNT!]%%" [%%pkg[!CNT!]%%]
Iassume you know not to use ::comments in a block and that your maun will run into your sub...
In your first AFTER THE CALL line, your script will try to evaluate !name[! and then a string literal CNT and then !]! and so on. Since both !name[! and !]! are undefined, they evaluate to nothing and you're left with the string literal, CNT. Instead, you should convert the inner !CNT! to percent notation, so it can peacefully coexist with the outer delayed variable. You can accomplish this with a for loop.
for %%I in ("!CNT!") do echo !name[%%~I]!
... to avoid the conflicting exclamation marks. In your second AFTER THE CALL line, you attempt to begin a variable with %! while also ending it with %!. When using that sort of syntax, the ! has to be outside, and the % inside (or possibly vice-versa -- see Magoo's answer for an example). But since it's inside a parenthetical code block, it won't work like you want anyway. You need the delayed expansions. Use the for loop I described above.
By the way, don't use those :: pseudo-labels as comments within a parenthetical code block. Use rem instead. The :: can break things.
Maybe you can try this:
#echo off&SetLocal EnableDelayEdexpansion
set "name[1]=something"
set "a=1"
echo !name[%a%]!
rem echo %name[!a!]%
call echo !name[%a%]!
call echo %%name[!a!]%%
call echo %%name[%a%]%%
for %%a in (!a!) do (echo !name[%%a]!)
pause

batch %var:*\% to eliminate all before last \ not first

set var=C:\Users\user\Desktop\bla\bla.exe
set var=%var:*\%
echo %var%
this returns Users\user\Desktop\bla\bla.exe - is there any way to make it focus on the last \ and not the first one so that it would just return bla.exe? bear in mind that this will be used on multiple files and folders so i won't always know how many sub-folders there are.
#ECHO OFF
SETLOCAL
set "var=C:\Users\user\Desktop\blah blah\bla.exe"
FOR %%a IN ("%var%") DO (
SET "filename=%%~nxa"
FOR %%b in ("%%~dpa.") DO SET "lastleaf=%%~nxb"
)
ECHO filename is "%filename%"
ECHO lastleaf is "%lastleaf%"
GOTO :EOF
Normally, the next question is about how to obtain the last leaf of the directory-tree. No subroutines required...
Note positioning of quotes to minimise problems with separators. ALso minor directory name-change to exhibit differences.
The following snippet shows one way to do it.
#setlocal enableextensions enabledelayedexpansion
#echo off
:main
set var=C:\Users\user\Desktop\blah blah\yada yada.exe
call :basename result "%var%"
echo %result%
endlocal
goto :eof
:basename
set %1=%~nx2
goto :eof
It basically calls a function basename (named after the UNIX utility), passing the full name and the variable you want to assign the base name to, and you need to make sure you quote it properly lest filenames containing spaces will cause you problems.
The full set of variable modifiers can be seen in the call /? help output.
Alternatively, you can use the same basename functionality in a one-liner for statement:
for /f "delims=" %%I in ("%var%") do set result=%%~nxI
This allows you to get the base name without having to call a function. I tend to prefer the function myself since it's more readable but you could probably alleviate that by just including a comment:
rem Get base name of var into result:
rem eg: var = C:\Users\user\Desktop\blah blah\yada yada.exe
rem result = yada yada.exe
for /f "delims=" %%I in ("%var%") do set result=%%~nxI

Is there such a thing as a batch code optimizer?

So you know , when people write code they do it neat, best look and most understandable. but Is there an optimizer for batch? Could someone make one?
example-
it takes existing variables and replaces them with the shortest possible variants
set whatever=whatever
echo question
set /p answer=:
if %answer%=%whatever% whatever
and turns it into
set a=whatever
echo question
set /p b=:
if %b%=%a% whatever
so it basically shortens the variables , flags (or tags or whatever like :top) and does other things I cannot thing of to basically optimize everything.
There are certain constructs in Batch programs that slow down the execution. The sole construct that have a major impact for this point is assemble a loop via a GOTO instead of any type of FOR command. If a large program with many GOTO's is rewritten with FOR commands, an important time saving is expected. Another aspect that affect this point is the number of commands/lines a program have, that is, a program that get the same result than another one with less lines, will run faster. The way to achieve the same things with less commands is making good use of Batch file capabilities.
For example, this code:
set /A b=a+8
set /A c=b*2
set /A d=c+e
... run slower than this one:
set /A b=a+8, c=b*2, d=c+e
This code:
command-that-return-errorlevel
if %errorlevel% equ 1 goto label-1
if %errorlevel% equ 2 goto label-2
if %errorlevel% equ 3 goto label-3
... run slower than this one:
command-that-return-errorlevel
for %%e in (1 2 3) do if %errorlevel% equ %%e goto label-%%e
... and previous one run slower than this one:
command-that-return-errorlevel
goto label-%errorlevel%
Shorten the variable names have a very little impact in the execution speed.
This way, the best option is to write Batch files using these techniques from the very beginning. There is no easy way to develop a program that read a Batch file and perform the previous changes, that is, replace GOTO's with FOR and "compress" several lines in less ones.
The program below is just for fun!
#echo off
setlocal EnableDelayedExpansion
rem Get a list of current variable names
(for /F "delims==" %%v in ('set') do (
echo %%v
)) > varnames.txt
rem Call the Batch file, so it creates its variables
call "%~1"
rem Create an array with new variable names
set "_numVars="
set "_nextVar="
< varnames.txt (for /F "delims==" %%v in ('set') do (
if not defined _nextVar set /P _nextVar=
if "%%v" neq "!_nextVar!" (
set /A _numVars+=1
set "var[!_numVars!]=%%v"
) else (
set "_nextVar="
)
))
del varnames.txt
rem Rename the variables in a new file
setlocal DisableDelayedExpansion
(for /F "delims=" %%a in ('findstr /N "^" "%~1"') do (
set "line=%%a"
setlocal EnableDelayedExpansion
set "line=!line:*:=!"
if defined line (
for /L %%i in (1,1,%_numVars%) do (
for /F "delims=" %%v in ("!var[%%i]!") do set "line=!line:%%v=v%%i!"
)
)
echo(!line!
endlocal
)) > "%~1.new"
This program get all variables from the Batch file given in the parameter and renames they as "v#" with a sequential number. To use this program you must remove any setlocal command from the Batch file before pass it to this program. Of course, this detail may cause several pitfalls: although the called program have Delayed Expansion already Enabled, there is no way to activate any Disable/Enable delayed expansion that the Batch file may require in order to correctly run. Besides, the program changes all strings in the Batch file that are equal to any variable name, but even if this point could be fixed (and this is a big IF, because it requires to emulate the cmd.exe Batch file parser!) this program would still be useless: as I said in my other answer, renaming the variables in a Batch file have a very little effect on its performance. I just wrote it for fun!

Delayed expansion doesn't work inside delayed expansion

I've got a script that does everything I expect it to do, apart from one line.
I've done similar before, but I can't get this one to work.
The code I've got is here
#echo off
SETLOCAL ENABLEDELAYEDEXPANSION
::Set Path to be folder of Sage Files
SET PATH=C:\Welcome\Progs\SitesDataSetups\GeorgeYarmouth
::set date variables
for /f "tokens=1" %%i in ('date /t') do set thedate=%%i
set mm=%thedate:~3,2%
set dd=%thedate:~0,2%
set yyyy=%thedate:~6,4%
::Set T_DAY variable to date in ddmmyy format
set T_DAY=%dd%%mm%%yyyy:~2%
c:
cd\
cd %path%
for /f "usebackq tokens=* delims= " %%P in (`dir sage*.csv /od /b`) do (
set SAGE=%%P
set SAGE2=!SAGE:~0,8!_EDITED
set EODNUM=!SAGE:~4,4!
for /f "tokens=* delims= " %%A in (%%P) do (
echo %EODNUM%
set S=%%A
***This line is the problem***
set S=!S:%T_DAY%=%EODNUM%!
echo.!S! >> %PATH%\TEST\!SAGE2!.csv
)
)
endlocal
I was expecting that is would take each line of the csv file and replace it with itself, except with a string replace of the current date with the variable EODNUM (which it does... only the variable is expanded before it is set, so is nothing)... The delayed expansion should solve this, but I can use this line of code
set S=!S:%T_DAY%=!EODNUM!!
because I think its too many !'s for CMD.
Am I missing something, or is there a better way to code this?? (I'm not a programmer of any kind, and most of the code I write comes from trial and error, and 'borrowing' from other scripts, so this may be a very messy way to do this).
Transfer the the value of !EODNUM! to a FOR variable, and then use your FOR variable as the replace string.
echo !EODNUM!
set "S=%%A"
for /f "delims=" %%E in ("!EODNUM!") do set "S=!S:%T_DAY%=%%E!"
echo.!S!>> %PATH%\TEST\!SAGE2!.csv
By way of explanation...
CMD reads (and does env var substitution), parses, and executes one top-level command at a time.
In your example, it reads the "for /f..." command all at once parsing and performing %var% substitution.
Once this is complete, it then executes the for loop, performing delayed !var! substitution.
Unfortunately, !var! substitution is not a do-substitution-until-none-left. This makes it hard (as in the answerer's solution) to perform the substitution into the !var:src=dst! value.
You will need a way that during execution you can get guaranteed substitution. This requires a for-statement, or something that involves reading and %var% substituting again. One way of doing this is to use the CALL :LABEL form where you can call to a specific label in your .cmd file and have this section do what you want:
...
call :GenS
...
and then:
:GenS
set S=!S:%T_DAY%=%EODNUM%!
goto :eof
BTW: I'm perplexed that you didn't notice the ECHO %EODNUM% not working in the loop as during the reading of the for loop all %var% substitutions are made.

dos batch extract characters from string

I am reading, in a file, the first column which contains 0002C1, 0002C2, 0003C1, 0004C1
Extracting only the first 4 digits and put them in a variable.
FOR /F "tokens=1" %i IN (export.txt) DO (
echo %i
set s=%i:~0,4%
echo %s%
)
in output, the result of echo %i is correct, extracting the digits seems to be working fine also (when I try it for one entry, the result is correct) but the value of s seems to not change!
Can somebody see what the problem is?
Here is the output that I receive:
0002C1
%s%
0002C2
%s%
0003C1
%s%
0004C1
%s%
First: You need yo expand the variable using the SETLOCAL ENABLEDELAYEDEXPANSION command...
Second: you are trying to "cut" a special var (%i:~0,1%), you can't.
My solution:
#Echo OFF
:: By Elektro H#cker
FOR /F %%# IN (export.txt) DO (
Call Set "Token=%%#"
Call Set "Token=%%Token:~0,4%%"
Call Echo %%Token%%
)
Pause&Exit
There are a few issues with your script, only a few adjustments needed and it works great.
You need to use delayed expansion to access a variable that you create in a for loop.
setlocal enabledelayedexpansion
FOR /F "tokens=1" %%i IN (export.txt) DO (
echo %%i
set x=%%i
set s=!x:~0,4!
echo !s!
)
The only difference being you replace the %'s with !'s to tell cmd to use delayed expansion instead to read the variables.
When you are using a batch file you need to use double %'s for the for variables.
You also need to assign %%i to something so you can use a variable sign either side of it, in this case I have used x.

Resources