Batch - return value from inside a loop - arrays

I have the following Problem:
I have 2 functions/labels called STARTERand Get_age. The Get_age function stores ages and names in some related variables and shall return the age of the person i passed to it (passed the name).
But the variable which shall store the return value -> means !arr[%%i].age!seems to be empty all the time. I think this is may of the (ENDLOCAL ...) block.
What confuses me too: if i only want to return the %%i inside the (ENDLOCAL ...) block it works fine, but when there are some !exclamation marks! the variable seems to get empty.
How can i now return the age easily without bloating the code? PS: This piece of code is only a small example to give a better view on it.
ThankĀ“s a lot
#echo off
SETLOCAL ENABLEDELAYEDEXPANSION
REM FUNCTION WHICH ASKS STORES A NAME AND ASKS FOR ITS AGE
:STARTER
setlocal
set "person=Niels"
call :Get_age "%person%" "readback"
echo %person% is -%readback%- years old.
pause
exit /b 0
REM FUNCTION WHICH RETURN THE AGE TO THE GIVEN NAME
:Get_age
setlocal
set "searchName=%~1"
set "retVal=%~2"
set "arr[0].name=Niels"
set "arr[0].age=5"
set "arr[1].name=Julia"
set "arr[1].age=2"
set "arr[2].name=Claus"
set "arr[2].age=9"
set "arr_size=2"
REM Go through the arr and return the age to the given name
for /l %%i in (0 1 %arr_size%) do (
if "!arr[%%i].name!" equ "%searchName%" (
(ENDLOCAL
set %retVal%=!arr[%%i].age!
)
)
)
exit /b 0

Perhaps this structure, which enables delayed expansion only when needed, and ends it again as soon as it has no further purpose, will work better for you:
#Echo Off
SetLocal EnableExtensions DisableDelayedExpansion
Rem Stores a name, determines their age and returns it.
Set "person=Niels"
Call :Get_Age "%person%" "readback"
Echo %person% is -%readback%- years of age.
Pause
Exit /B 0
:Get_Age
Set "arr[0].name=Niels"
Set "arr[0].age=5"
Set "arr[1].name=Julia"
Set "arr[1].age=2"
Set "arr[2].name=Claus"
Set "arr[2].age=9"
Set "arr_size=2"
For /L %%G In (0,1,%arr_size%) Do (
SetLocal EnableDelayedExpansion
If /I "!arr[%%G].name!" == %1 (
For %%H In ("!arr[%%G].age!") Do (
EndLocal
Set "%~2=%%~H"
)
) Else EndLocal
)
GoTo :EOF

You could store the value and return it after the loop.
set "found="
for /l %%i in (0 1 %arr_size%) do (
if "!arr[%%i].name!" equ "%searchName%" (
set "found=!arr[%%i].age!"
)
)
(
ENDLOCAL
set "%retVal%=%found%"
)
goto :eof

Related

variable as tokens in for loop

Im trying to make a batch file that loops thru an array containing numbers like this: 1 2 3 4 5.
In the first itteration of the loop I like to pick token 1 and 2. In the second 2 and 3, in the third 3 and 4 and so on.
I do think I should use ! in the variables first and second that I use as tokens. Like in the first FOR /F, but when I do, I get: !first!" was not expected here.
And if I use %, it does not count up.
Everything works except the variable tokens. Any one knowes how to? Any help or suggestions greatly appriciated.
This is the part Im struggeling with:
setlocal EnableDelayedExpansion
set first=1
set second=2
set N=4
set output="1 2 3 4 5"
set output=%output:"=%
for /L %%a in (1,1,%N%) do (
if !counter! equ active (
set /a first+=1
set /a second+=1
)
FOR /F "tokens=!first!" %%a IN ("%output%") DO (
set nr1=%%a
)
FOR /F "tokens=%second%" %%a IN ("%output%") DO (
set nr2=%%a
)
echo nr1 var: !nr1!
echo nr2 var: !nr2!
echo counter f: !first!
echo counter s: !second!
set counter=active
)
You cannot use delayed expanded variables in the options string of for /F. Neither can you use other for variables for that. But you can use normally (immediately) expanded variables. Also you can use argument references like %1, for example.
So a nice work-around for your problem is to place the for /F loop in a sub-routine and use call in the main program with the delayed expanded variables as arguments, like this:
#echo off
setlocal EnableDelayedExpansion
set /A first=1
set /A second=2
set /A N=4
set "output=1 2 3 4 5"
set "counter="
for /L %%a in (1,1,%N%) do (
if defined counter (
set /A first+=1
set /A second+=1
)
call :SUB !first! !second!
echo nr1 var: !nr1!
echo nr2 var: !nr2!
echo counter f: !first!
echo counter s: !second!
set "counter=active"
)
endlocal
exit /B
:SUB val_token1 val_token2
for /F "tokens=%~1,%~2" %%a in ("%output%") do (
if %~1 LSS %~2 (
set "nr1=%%a"
set "nr2=%%b"
) else if %~1 GTR %~2 (
set "nr1=%%b"
set "nr2=%%a"
) else (
set "nr1=%%a"
set "nr2=%%a"
)
)
exit /B
Since you are extracting tokens from the same string, I combined your two for /F loops into a single one. The if block in the for /F loop in the sub-routine :SUB is there just in case the second token number is not always greater than the first one. But if that can guaranteed, the for /F loop needs to contain only set "nr1=%%a" and set "nr2=%%b".

Batch - Decreasing value of a variable in substring is not functioning

I'm trying to reverse "hello" to "olleh". But the output shows "ooooo".
I think !string:~%back%,1! is the problem, because when I use echo to test the value of back is decreasing or not, it works, but it doesn't work in substring, so it always get the last character of the string (-1,1).
#echo off
set string=hello
set temp_string=%string%
set /a string_length=0
:find_length
if defined temp_string (
set temp_string=%temp_string:~1%
set /a string_length+=1
goto :find_length
)
:loop
setlocal enabledelayedexpansion
set /a back=-1
for /l %%a in (1,1,!string_length!) do (
set reverse_string=!string:~%back%,1!!reverse_string!
set /a back-=1
)
echo !reverse_string!
pause >nul
As TripeHound commented, %back% needs to be delayed. What you should do is use the for /L loop value of %%a to in place of %back%. (No sense decrementing a variable manually when one's already being decremented for you as a part of the for /L loop, right?)
for /l %%a in (%string_length%,-1,0) do (
call set "reverse_string=!reverse_string!!string:~%%a,1!"
)
goto loops are not very efficient. If you've got a long string you're going to reverse, there'll be a noticeable pause while you count its length if you goto :label for each character. The fastest way I've found to get the length of a string is based on jeb's answer here:
:length <return_var> <string>
setlocal enabledelayedexpansion
if "%~2"=="" (set ret=0) else set ret=1
set "tmpstr=%~2"
for %%I in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do (
if not "!tmpstr:~%%I,1!"=="" (
set /a ret += %%I
set "tmpstr=!tmpstr:~%%I!"
)
)
endlocal & set "%~1=%ret%"
goto :EOF
Put it all together like this:
#echo off
setlocal
set "string=%*"
call :length string_length "%string%"
setlocal enabledelayedexpansion
for /l %%a in (%string_length%,-1,0) do (
set "reverse_string=!reverse_string!!string:~%%a,1!"
)
echo(!reverse_string!
pause >nul
exit /b 0
:length <return_var> <string>
setlocal enabledelayedexpansion
if "%~2"=="" (set ret=0) else set ret=1
set "tmpstr=%~2"
for %%I in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do (
if not "!tmpstr:~%%I,1!"=="" (
set /a ret += %%I
set "tmpstr=!tmpstr:~%%I!"
)
)
endlocal & set "%~1=%ret%"
goto :EOF
Example output:
command: test.bat The quick brown fox
result: xof nworb kciuq ehT
The problem is that %back% is being used without delayed expansion, so will always have the value -1. Replacing the end of your code with:
set /a back=-1
set /a count=1
:repeat
if %count% gtr %string_length% goto :report
set reverse_string=!string:~%back%,1!!reverse_string!
set /a back-=1
set /a count+=1
goto :repeat
:report
echo !reverse_string!
Will do the trick.
You cannot just use !back! because you need the contrast of !...! and %...% to have a variable index, so you'll have to go back to an old-fashioned :loop construct so %back% gets updated each time around.
As described at this post:
"To get the value of a substring when the index change inside FOR/IF enclose the substring in double percents and precede the command with call. For example:
for /l %%a in (1,1,!string_length!) do (
call set reverse_string=%%string:~!back!,1%%!reverse_string!
set /a back-=1
)
Another way to achieve previous process is using an additional FOR command to change the delayed expansion of the index by an equivalent replaceable parameter, and then use the delayed expansion for the substring. This method run faster than previous CALL:
for /l %%a in (1,1,!string_length!) do (
for %%b in (!back!) do (
set reverse_string=!string:~%%b,1!!reverse_string!
)
set /a back-=1
)
"
However, it is not efficient to first loop thru the string just to count the characters, and then loop again to reverse they. I think the method below should be the fastest one:
#echo off
setlocal EnableDelayedExpansion
set maxLength=80
set string=hello
set "reverse="
for /L %%i in (1,1,%maxLength%) do (
set "reverse=!string:~0,1!!reverse!"
set "string=!string:~1!"
if not defined string goto break
)
:break
echo %reverse%
Here's another algorithm:
#echo off
call :reverse "The quick brown fox"
echo %output%
pause & exit
:reverse
setlocal enableDelayedExpansion
set string=%~1
set index=0
:loopchar
set char=!string:~%index%,1!
if "!char!"=="" endlocal & set output=%output% & exit /b
set output=!char!!output!
set /a index+=1
goto loopchar
"Tricks":
using %index% inside ! to expand its current value instead of call with %% (#Aacini)
export of !output! from the local scope by reassigning it on the same line as endlocal.

strip the file path in Batch script

SourceFilePath="D:\Projects\Code\Site\Beanch\GV\dllfunction\dll_feature\filename.cpp"
How to strip folder names which is just before the file name
From the above example string, i want to get dll_feature,dllfunction,GV into different variables.
#ECHO Off
SETLOCAL
SET "SourceFilePath=D:\Projects\Code\Site\Beanch\GV\dllfunction\dll_feature\filename.cpp"
SET /a count=0
:loop
FOR %%a IN ("%sourcefilepath%") DO SET "part%count%=%%~nxa"&SET "sourcefilepath=%%~dpa"
IF DEFINED part%count% SET /a count +=1&SET "sourcefilepath=%sourcefilepath:~,-1%"&GOTO loop
SET part
SET count
GOTO :EOF
Provided you don'y have weirdo characters in the starting name...
#echo off
setlocal enableextensions disabledelayedexpansion
set "sourceFilePath=D:\Projects\Code\Site\Beanch\GV\dllfunction\dll_feature\filename.cpp"
set "count=10000"
for %%a in ("%sourceFilePath:\=" "%") do (
setlocal enabledelayedexpansion
for %%b in (!count:~-4!) do (
endlocal
set "e_%%b=%%a"
)
set /a "count+=1"
)
rem Show the variables
set e_
Separates each element in the variable replacing the backslash with a space (everything quoted to avoid problems) and for each element, a variable is defined
Numbers in variable names are padded. If not needed just change the initial count value to 0
The one limitation that the MC ND's solution is that you must know the directory depth to get the parts at the end.
Here's an alternate strategy that employs recursion and subroutines. The "subroutine" :SPLIT gets the nth item from the specified path (0 is the right-most) and saves it into the variable specified. The way it's written, if you specify a level that is greater than the number of directory parts, it saves an empty string to the variable. This could be changed easily if that was desirable.
#ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
SET "SourceFilePath=D:\Projects\Code\Site\Beanch\GV\dllfunction\dll_feature\filename.cpp"
FOR /L %%i IN (0,1,10) DO (
CALL :SPLIT "%SourceFilePath%" %%i TGT
#ECHO [%%i] !TGT!
)
EXIT /B
REM Syntax: CALL :SPLIT _path_ _level_
REM Get the _nth_ element of the path (0 is the right-most)
:SPLIT
SETLOCAL ENABLEDELAYEDEXPANSION
SET "PATH=%~1"
SET "LEVEL=%~2"
SET "TARGET_VAR=%~3"
REM Strip off a trailing dir sep (for the %%~ substitution).
IF "%PATH:~-1,1%"=="\" SET "PATH=!PATH:~0,-1!"
FOR %%p IN (%PATH%) DO (
IF /I "%PATH%"=="%%~dp" (
REM BASE CASE
IF "%LEVEL%"=="0" (
SET "_TARGET=%PATH%"
) ELSE (
SET "_TARGET="
)
) ELSE IF "%LEVEL%"=="0" (
REM Return the leaf
SET "_TARGET=%%~nxp"
) ELSE (
REM Recurse on the drive+directory
SET /A SUBLEVEL=LEVEL-1
CALL :SPLIT "%%~dpp" !SUBLEVEL! _TARGET
)
)
ENDLOCAL && SET "%TARGET_VAR%=%_TARGET%"
EXIT /B
The output of this script is:
[0] filename.cpp
[1] dll_feature
[2] dllfunction
[3] GV
[4] Beanch
[5] Site
[6] Code
[7] Projects
[8] D:
[9]
[10]
For the description you give, this invocation would give you something like you want:
CALL :SPLIT "%SourceFilePath%" 0 PART0
#ECHO %PART0%
CALL :SPLIT "%SourceFilePath%" 1 PART1
#ECHO %PART1%
CALL :SPLIT "%SourceFilePath%" 2 PART2
#ECHO %PART2%

length of each line using batch file

I want to read a CSV file line by line and echo something different if the length of the line is 7999.
I manage to do something as below, which reads each line and checks the number of character for each line, but the issue is that I am getting no value in %result% and echo(%result% prints a blank value. Any idea what am I doing wrong here? Thanks
#echo off
setlocal
for /f "tokens=* delims= " %%a in (REPORTS.csv) do (
set "line=%%a"
call :strlen result line
echo(%result%
if %result% EQU 7999 (
echo %%a
echo(short=%result%
) else (
echo %%a
echo(long=%result%
)
pause
)
:strlen <resultVar> <stringVar>
(
setlocal EnableDelayedExpansion
set "s=!%~2!#"
set "len=0"
for %%P in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do (
if "!s:~%%P,1!" NEQ "" (
set /a "len+=%%P"
set "s=!s:~%%P!"
)
)
)
(
endlocal
set "%~1=%len%"
exit /b
)
Put this section into another subroutine, similar to :strlen
echo(%result%
if %result% EQU 7999 (
echo %%a
echo(short=%result%
) else (
echo %%a
echo(long=%result%
)
Note also that your main routine will continue into your subroutine when finished, so at end-of-file(reports.csv) the batch will execute :strlen one final time and exit through the EXIT
I'd recommend adding a
GOTO :EOF
Immediately before the :strlen label. This is understood by the processor to go to end-of-physiacl-file (the colon is required)
When a compound statement enclosed in parentheses is to be executed,
the statement is first parsed from the open parenthesis all of the
way to the matching close-parenthesis.
At this time, any %var% is replaced by that var's value from the
environment AT THE TIME IT IS PARSED (ie its PARSE-TIME value.)
THEN if the statement seems valid, it is executed.
There are three common ways of accessing the RUN-TIME value of the
variable (as a FOR loop executes, for instance.)
1/ SETLOCAL ENABLEDELAYEDEXPANSION which switches to a mode where
!var! may be used to access the runtime value of var
2/ CALL set var2=%%var%% to set the value of var2 from the
runtime value of var
3/ Executing a subroutine, internal or external within which %var%
will be the runtime value.
#ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
FOR %%i IN (1 2 3) DO (
ECHO START of run %%i
ECHO using ^!time^! : !time! - PARSE TIME was %time%
CALL ECHO using CALL %%%%TIME%%%% : %%TIME%%
CALL :report
timeout /t 5
ECHO using ^!time^! : !time!
CALL ECHO using CALL %%%%TIME%%%% : %%TIME%%
CALL :report
ECHO END of run %%i
ECHO.
)
GOTO :eof
:report
ECHO :report says TIME is %TIME%
GOTO :eof
A few items to note:
The instruction
IF ERRORLEVEL n echo errorlevel is n OR GREATER
ALWAYS interprets the RUN-TIME value of ERRORLEVEL
IF SET VAR ALWAYS interprets the RUN-TIME value of VAR
The magic variables like ERRORLEVEL and TIME should never
be SET. If you execute
SET ERRORLEVEL=dumb
then ERRORLEVEL will adopt the value dumb because the current
value in the environment takes priority over the system-assigned value.
You should use DelayedExpansion in if and for loops and take care of the brackets:
#echo off
setlocal enabledelayedexpansion
for /f "tokens=* delims= " %%a in (REPORTS.csv) do (
set "line=%%a"
call :strlen result line
echo.!result!
if !result! EQU 7999 (
echo.%%a
echo.short=!result!
) else (
echo.%%a
echo.long=!result!
)
)
pause
goto:eof
:strlen <resultVar> <stringVar>
setlocal EnableDelayedExpansion
set "s=!%~2!#"
set "len=0"
for %%P in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do (
if "!s:~%%P,1!" NEQ "" (
set /a "len+=%%P"
set "s=!s:~%%P!"
)
)
endlocal &set "%~1=%len%"
exit /b
Your code doesn't ever work in many areas.

.bat script, 4 lists as data, input is an integer i, output is the rank of the list containing i

I need to write a script, in.bat, where
the user inputs a 3 digits number (as a string) i.
the script has 4 predefined groups of numbers, number from 1 to 4
the script tests membership for i in the groups, and return the index of the group containing i.
I'm not familiar with declaring and initializing any kinds of data structure (e.g. list, arrays and the like) for batch files, so can someone help me on this?
Pseudo code:
::Returns 1,2,3,4,5 Depending on testNum passed
group1= <822-824,829,845,851,859,864,867>
group2= <826-828,830-839,843-844,847-850,852-854,860-862,883>
group3= <855-858,861,863,865>
group4= <877-882,884>
if %1 is member of group1
return 1
if %1 is member of group2
return 2
if %1 is member of group3
return 3
if %1 is member of group4
return 4
Thank you!
Batch does not have any formal complex data structures like arrays, lists, or objects. But you can emulate them. Here is an efficient solution that defines the groups with nearly the format as in your question.
#echo off
setlocal enableDelayedExpansion
::Here is a small loop to test the routine
for %%N in (822,823,883,835,856,863,880,884) do (
call :assignGroup %%N
echo %%N is in group !group!
)
exit /b
:assignGroup CaseNumber
:: The returning value is contained in variable GROUP
set group=0
for %%A in (
"822-824,829,845,851,859,864,867"
"826-828,830-839,843-844,847-850,852-854,860-862,883"
"855-858,861,863,865"
"877-882,884"
) do (
set /a group+=1
for %%B in (%%~A) do (
for /f "tokens=1,2 delims=-" %%C in ("%%B") do (
if "%%C"=="%~1" exit /b
if "%~1" gtr "%%C" if "%~1" leq "%%D" exit /b
)
)
)
::no group found so undefine the var
set "group="
exit /b
The above solution is fine for occasional calls. But if you were going to call the routine many thousands of times, then it would be better to initialize an array of valid values with assigned group numbers. Then each test becomes a direct read of the value, instead of having to call a routine. However, it is possible to abuse this technique. Assign enough values and each variable assignment gets slower and slower. You might also spend more time setting up the array than actually testing values.
Note there is no significance to the characters []. in the variable names. They could be stripped out of the variable names and the code would function the same. They are there only to aid in the understanding the intent of the variables.
#echo off
setlocal enableDelayedExpansion
::initialize a sparse "array" that assigns a group to each valid case #
set group=0
for %%A in (
"822-824,829,845,851,859,864,867"
"826-828,830-839,843-844,847-850,852-854,860-862,883"
"855-858,861,863,865"
"877-882,884"
) do (
set /a group+=1
for %%B in (%%~A) do (
for /f "tokens=1,2 delims=-" %%C in ("%%B") do (
if "%%D"=="" (
set case[%%C].group=!group!
) else for /l %%N in (%%C 1 %%D) do (
set case[%%N].group=!group!
)
)
)
)
::Now test some values
for %%N in (822,823,883,835,856,863,880,884,900) do (
if defined case[%%N].group (
echo %%N is in !case[%%N].group!
) else (
echo %%N is not in a group
)
)
exit /b
This will set GROUP to whatever group it finds the code in
set test=822,823,824,829,845,851,859,864,867
echo %test% | findstr %1>nul&&set group=1
set test=826,827,828,830,831,832,833,834,835,836,837,838,839,843,844,847,848,849,850,852,853,854,860,861,862,883
echo %test% | findstr %1>nul&&set group=2
set test=855,856,857,858,861,863,865
echo %test% | findstr %1>nul&&set group=3
set test=877,878,879,880,881,882,884
echo %test% | findstr %1>nul&&set group=4
if you want to use ERRORLEVEL to test the return value, then change the SET GROUP= to EXIT /B
By no means perfect, but a working starting point. Save the following script as group.bat and call it with group 878 to find out to which group 878 belongs to.
#echo off
SET group1=822-824,829,845,851,859,864,867
SET group2=826-828,830-839,843-844,847-850,852-854,860-862,883
SET group3=855-858,861,863,865
SET group4=877-882,884
CALL :IsInGroup %1 "%group1%"
IF Errorlevel 1 echo Group 1 & GOTO :EOF
CALL :IsInGroup %1 "%group2%"
IF Errorlevel 1 echo Group 2 & GOTO :EOF
CALL :IsInGroup %1 "%group3%"
IF Errorlevel 1 echo Group 3 & GOTO :EOF
CALL :IsInGroup %1 "%group4%"
IF Errorlevel 1 echo Group 4 & GOTO :EOF
echo Group not found
GOTO :EOF
:IsInGroup
SETLOCAL ENABLEDELAYEDEXPANSION
FOR %%i IN (%~2) DO (
SET h=%%i
SET g=!h:~3,1!
SET /a lo=!h:~0,3!
IF !g!. == -. (
SET /a hi=!h:~4,3!
IF %1 GEQ !lo! (
IF %1 LEQ !hi! exit /B 1
)
) ELSE (
IF %1 EQU !lo! exit /B 1
)
)
ENDLOCAL
EXIT /B 0
The function :IsInGroup checks whether the first argument is contained in the list passed as the second argument.
If the number of values to check for in each group is small, dbenhams second method (direct read of each array value) is the fastest. Any other method that process the values of each group in a FOR loop to do individual or range comparisons is slower. However, as the number of individual elements grows (counting each element included between a range) array value method get slower, as dbenhams indicated.
There is another approach to solve this problem using an aritmethic expression. For example, you may check if a variable is 829 or 845 with this command:
set /A result=(829-variable)*(845-variable)
If the variable have anyone of these two values the result is zero. To check if the variable is into a range, the expression is this:
set /A aux=(lowerLimit-variable)*(variable-upperLimit), result=(aux-1)/aux
Previous expression requires a small provision in case the variable is anyone of the limits (to manage the division by zero). The program below assemble the appropiate aritmethic expression for each of the groups; after that, checking each value is achieved with a maximum of 4 SET /A commands (one per group). This method is faster than individual testing of each value in a FOR loop and use just one variable per group, not per individual element.
#echo off
setlocal EnableDelayedExpansion
rem Assemble testing expression for each group
set numGroup=0
for %%a in (
"822-824,829,845,851,859,864,867"
"826-828,830-839,843-844,847-850,852-854,860-862,883"
"855-858,861,863,865"
"877-882,884"
) do (
set expr=1
for %%b in (%%~a) do (
for /F "tokens=1,2 delims=-" %%c in ("%%b") do (
if "%%d" equ "" (
rem Individual value: multiply previous expr by direct subtract
set "expr=!expr!*(%%c-n^)"
) else (
rem Range value pair: use range expression at this point, then continue
set "expr=!expr!,a=r,r=0,b=(%%c-n^)*(n-%%d),r=(b-1)/b*a"
)
)
)
set /A numGroup+=1
set expr[!numGroup!]=!expr!
)
rem Now test some values
for %%n in (822,823,883,835,855,856,858,863,880,884,900) do (
call :assignGroup %%n
echo %%n is in group !group!
)
goto :EOF
:assignGroup number
set /A n=%1, group=0
for /L %%i in (1,1,%numGroup%) do (
set /A r=!expr[%%i]! 2> NUL
if !r! equ 0 set group=%%i & exit /B 0
)
exit /B 1

Resources