I wrote the following code that works exactly the way I want:
#echo off
setlocal enabledelayedexpansion
set /a i=0
set in="This is line 1", "This is line 2"
for %%h in (%in%) do (
set /a i+=1
set "val[!i!]=%%h"
)
set out=
for /l %%n in (1,1,!i!) do (
set out=!out! !val[%%n]! ^& vbcrlf ^& _)
echo !out:~1,-12!
Which takes the value of the %in% variable and reads each comma separated line into an element of an array and then does some string concatenation on it and spits out a new string. Now, when I try to turn it into a function, it fails because of how %2 is being parsed as an argument. I need %2 to be parsed as a single, comma separated string with a variable amount of values. This simple test doesn't work:
call :Test Title "This is line 1","This is line 2" "arg3"
exit /b
:Test arg1 arg2 arg3
set /a i=0
for %%h in (%2) do (
set /a i+=1
set "val[!i!]=%%h"
)
set out=
for /l %%n in (1,1,!i!) do (
set out=!out! !val[%%n]! ^& vbcrlf ^& _)
echo %1 !out:~1,-12! %3
exit /b
The only thing I can think of is to use %* and change the delimiter to something unique but I'd rather avoid that if possible.
1. Shift through the Middle parameters
This will leave %1 alone and shift though all the middle parameters stopping when there are no more and leaving the last param in %3.
#echo off
setlocal
call :Test One "Two","Three" Four
endlocal
exit /b 0
:Test <Title> <Lines...> <LastArg>
echo %2
set "Temp=%4"
if defined Temp shift /2 & goto Test
echo %1
echo %3
exit /b 0
Output
"Two"
"Three"
One
Four
2. Put the string in a variable and pass the variable name
#echo off
setlocal EnableDelayedExpansion
set "String="Two","Three""
call :Test One String Four
endlocal
exit /b 0
:Test <a> <b> <c>
echo %1
echo !%2!
echo %3
exit /b 0
Output
One
"Two","Three"
Four
These are the first two solutions that come to my mind.
Update
Here is the shift method applied to your code using an inner loop :__Test
#echo off
setlocal EnableDelayedExpansion
call :Test Title "This is line 1","This is line 2" "arg3"
endlocal
exit /b 0
:Test <arg1> <arg2[,...]> <arg3>
set "i=0"
:__Test
set /a "i+=1"
set "val[!i!]=%2"
set "tempvar=%4"
if defined tempvar shift /2 & goto __Test
set "out="
for /l %%n in (1,1,!i!) do (
set out=!out! !val[%%n]! ^& vbcrlf ^& _)
echo %1 !out:~1,-12! %3
exit /b 0
For a general solution, passing values by reference (store value in a variable and pass variable name) is the best option. This is the same as David Ruhmann's second option.
There is another way, but it requires more work by the caller. You can require that all quotes in the parameter value be doubled up, and then enclose the entire parameter in one more set of quotes. Within the function, replace all "" with " to get the desired value. I used to use this method until I learned about passing values by reference.
#echo off
setlocal enableDelayedExpansion
call :Test Title """This is line 1"",""This is line 2""" "arg3"
exit /b
:Test arg1 arg2 arg3
set "arg2=%~2"
set "arg2=%arg2:""="%"
echo arg1=%1
echo arg2=%arg2%
echo arg3=%3
UPDATE
Passing values by reference is the best option to pass complex values that contain token delimiters and quotes.
But the OP isn't really interested in the list of values as a single parameter, since a FOR loop is used to split them up. The FOR loop can run into trouble if any of the values contain * or ?.
I now see that for this particular case, it is better to move the list to the end such that all arguments from 3 on are part of the list. Then use SHIFT /3 and a GOTO loop to read in the list values. This is basically David Ruhmann's option 1.
Related
In the following script I want to pass a string via variable and the variable name for an array which should contain substrings to a subroutine.
The subroutine puts substrings of the passed string into an array/list which then should get "returned" by setting it as the value of the 2. passed parameter.
#ECHO OFF
SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
SET testString=Hello World
REM Pass testString and substrings to subroutine
CALL :get_substrings testString substrings
REM For testing. Echo substrings. DOESN'T WORK. substrings is empty!
FOR /L %%s IN (0,1,2) DO (
ECHO !substrings[%%s]!
)
ENDLOCAL
EXIT /B 0
:get_substrings
SETLOCAL ENABLEDELAYEDEXPANSION
SET "string=!%~1!"
REM Alternative approach: Make a connection to %2 rightaway
REM SET "substrings=!%~2!"
REM Process string: Put substrings into indexed array. This works as expected!
FOR /L %%s IN (0,1,2) DO (
SET substrings[%%s]=!string:~0,5!
SET string=!string:~5!
)
REM For testing. Echo the substrings. Works as expected!
FOR /L %%s IN (0,1,2) DO (
ECHO !substrings[%%s]!
)
REM For alternative approach
REM ENDLOCAL
REM End the local the set 2.param = substringsArray
ENDLOCAL & SET %2=%substrings%
EXIT /B 0
Processing the string by creating a array with substrings in the subroutine works as expected. But setting 2. parameters value and keeping the value after subroutine doesn't work...
Notes: The processing of the string is just a dummy. The real process is slightly different but the core with the substrings array is the same. The script is executable right away.
So, how can I get the value substrings back?
This does what you want:
#ECHO OFF
SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
SET testString=Hello World
REM Pass testString and substrings to subroutine
CALL :get_substrings testString substrings
REM For testing. Echo substrings.
FOR /L %%s IN (0,1,2) DO (
ECHO !substrings[%%s]!
)
ENDLOCAL
EXIT /B 0
:get_substrings
SETLOCAL ENABLEDELAYEDEXPANSION
SET "string=!%~1!"
REM Process string: Put substrings into indexed array. This works as expected!
FOR /L %%s IN (0,1,2) DO (
SET substrings[%%s]=!string:~0,5!
SET string=!string:~5!
)
REM For testing. Echo the substrings. Works as expected!
FOR /L %%s IN (0,1,2) DO (
ECHO !substrings[%%s]!
)
REM End the local the set 2.param = substringsArray
set SubEnviron=1
for /F "tokens=2* delims=[]=" %%a in ('set substrings[') do (
if defined SubEnviron ENDLOCAL
set "%2[%%a]=%%b"
)
EXIT /B 0
I wasn't able to understand your counting of characters so here's how I'd probably do it:
#Echo Off
SetLocal EnableDelayedExpansion
Set "TestString=Hello World"
For /F "Delims==" %%A In ('Set SubString[ 2^>Nul') Do Set "%%A="
Set "i=1"
Set "SubString[%i%]=%TestString: ="&Set/A i+=1&Set "SubString[!i!]=%"
Set SubString[
Pause
Example Output:
SubString[1]=Hello
SubString[2]=World
Press any key to continue . . .
For the purposes of testing you probably don't need the For loop, its purpose is to ensure there are no existing variables whose name begins with SubString[
Edit
This uses three parameters:
The string to cut%string%
A number of how long each substring should be%chrnum%
The substring parameter%strvar%
#Echo Off
SetLocal EnableDelayedExpansion
Set "string=montuewedthufrisatsun"
Set "chrnum=3"
Set "strvar=substring"
Set "i=1"
Set "_=%string%"
:Loop
Set "!strvar![%i%]=!_:~,%chrnum%!"
If "!_:~%chrnum%!"=="" GoTo Write
Set "_=!_:~%chrnum%!"
Set /A i+=1
GoTo Loop
:Write
Set !strvar![ 2>Nul
Pause
Yes, I fully understand that you are not going to change this code to PowerShell. But, it might be worth considering for the next time given how easy it is. get_substrings is a lambda.
PS C:\src\t\selarr> type .\lamb002.ps1
$teststring = 'hello cruel world'
$get_substrings = { param($t) foreach ($s in $t.split()) { $s.Substring(0,4) } }
$a = & $get_substrings $teststring
$a.length
$a[0]
$a[1]
$a[2]
PS C:\src\t\selarr> .\lamb002.ps1
3
hell
crue
worl
The FOR loop only outputs the first item of the list and I'm trying to go over the whole list.
#ECHO OFF
set list=this,is,a,list
FOR /f "tokens=1* delims=," %%a IN ("%list%") DO echo %%a
pause
The FOR /F command split a line in several tokens, that must be referenced individually via separate letters:
#ECHO OFF
set list=this,is,a,list
FOR /f "tokens=1-4 delims=," %%a IN ("%list%") DO (
echo %%a
echo %%b
echo %%c
echo %%d
)
pause
The plain FOR command process a series of elements separated by space, or by the standard Batch file delimiters: comma, semicolon or equal-sign:
#ECHO OFF
set list=this,is,a,list
FOR %%a IN (%list%) DO echo %%a
pause
Open a command prompt window, run for /? and read the output help.
With tokens=1* the first string delimited by 1 or more commas (,,, is like 1 comma!) is assigned to loop variable a which is here the word this. And the rest of the string being is,a,list is assigned to loop variable b (next after a in ASCII table) which is not referenced at all in provided code snippet.
Here is a batch code demonstrating how to process each substring of a comma separated string:
#echo off
set "List=this,is,a,list"
set ItemCount=0
:NextItem
if "%List%" == "" pause & goto :EOF
set /A ItemCount+=1
for /F "tokens=1* delims=," %%a in ("%List%") do (
echo Item %ItemCount% is: %%a
set "List=%%b"
)
goto NextItem
The output is:
Item 1 is: this
Item 2 is: is
Item 3 is: a
Item 4 is: list
There are of course also other solutions possible. This is just an example.
For understanding the used commands and how they work, open a command prompt window, execute there the following commands, and read entirely all help pages displayed for each command very carefully.
echo /?
for /?
goto /?
if /?
pause /?
set /?
Since Aacini's first solution can only be used with lists of known length and the second cannot be used for lists containing spaces, special characters, etc., here is a modified version of the Mofi's best approach used with a procedure call with parameters.
Cheers
#echo off&cls
:: CREATING PROCEDURE MESSAGES
set "ms1Item= List of All Items:"
set "ms2Item= Item Number"
set "ms3Item= Number of Total Items"
:: DEFINE PROCEDURE DELIMITER
set "dlmItem=,"
:: CREATING LISTS
set "lst1=this,is,a,list,a b,c"
set "lst2=c:\#WebNet\WebSite Watcher,e:\#WebNet Tools\Bandwidth Monitor\"
set "lst3=this-is-an-other-list-aaaa, bbbb-with spaces-and comma"
:: PROCEDURE CALLS
call:getItem "%lst1%" "echo. Item Value is:" x x x x
echo.&pause&cls
call:getItem "%lst2%" "echo. Item Value is" x x x x
echo.&pause&cls
call:getItem "%lst2%" "dir /o:en" "echo.&pause&cls" x
echo.
call:getItem "%lst2%" "dir /b /o:en" "echo.&timeout /t 10>nul" "" "" x
cls
:: REDEFINE PROCEDURE DELIMITER
set "dlmItem=-"
call:getItem "%lst3%" "echo. Item Value is:" x x x x
echo.&pause&goto:eof
:: END OF PROCEDURE CALLS
:getItem
:: PROCEDURE PARAMETERS
:: %1 COMPLETE ITEMS SEPARATED BY %dlmItem%
:: %2 A COMMAND TO BE EXECUTED WITH THE ITEM
:: %3 A COMMAND TO BE EXECUTED AFTER OR X
:: %4 IF SET DISPLAY THE LIST OF ALL ITEMS
:: %5 IF SET DISPLAY THE ITEMS COUNT
:: %6 IF SET DISPLAY THE ITEMS TOTAL
if "%~3" EQU "" echo.&goto:getItemEND
set /A cntItem=0
set "newItem=%~1"
set "cmdItem=%~2"
set "nxtItem=%~3"
if "%~4" NEQ "" echo.%ms1Item% %newItem%&echo.
:oneItem
if "%newItem%" EQU "" goto:oneItemEND
set /A cntItem+=1
for /F "tokens=1* delims=%dlmItem%" %%a in ("%newItem%") do (
set "oneItem=%%a"
set "newItem=%%b"
)
if "%~5" NEQ "" echo.%ms2Item% %cntItem%
%cmdItem% "%oneItem%"
if /I "%nxtItem%" NEQ "x" %nxtItem%
goto oneItem
:oneItemEND
if "%~6" NEQ "" echo.&echo.%ms3Item% %cntItem%
:getItemEND
exit/b
:: END OF PROCEDURE
NB: echo.bell string ALT007
and change set "lst2=..." with your values
I have a batch-script with multiple arguments. I am reading the total count of them and then run a for loop like this:
#echo off
setlocal enabledelayedexpansion
set argCount=0
for %%x in (%*) do set /A argCount+=1
echo Number of processed arguments: %argCount%
set /a counter=0
for /l %%x in (1, 1, %argCount%) do (
set /a counter=!counter!+1 )
What I want to do now, is to use my running variable (x or counter) to access the input arguments. I am thinking aobut something like this:
REM Access to %1
echo %(!counter!)
In an ideal world this line should print out my first command line argument but obviously it doesn't. I know I am doing something wrong with the % operator, but is there anyway I could access my arguments like this?
//edit: Just to make things clear - the problem is that %(!counter!) provides me with the value of the variable counter. Meaning for counter=2 it gives me 2 and not the content of %2.
#echo off
setlocal enabledelayedexpansion
set argCount=0
for %%x in (%*) do (
set /A argCount+=1
set "argVec[!argCount!]=%%~x"
)
echo Number of processed arguments: %argCount%
for /L %%i in (1,1,%argCount%) do echo %%i- "!argVec[%%i]!"
For example:
C:> test One "This is | the & second one" Third
Number of processed arguments: 3
1- "One"
2- "This is | the & second one"
3- "Third"
Another one:
C:> test One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve etc...
Number of processed arguments: 13
1- "One"
2- "Two"
3- "Three"
4- "Four"
5- "Five"
6- "Six"
7- "Seven"
8- "Eight"
9- "Nine"
10- "Ten"
11- "Eleven"
12- "Twelve"
13- "etc..."
:loop
#echo %1
shift
if not "%~1"=="" goto loop
here's one way to access the second (e.g.) argument (this can be put in a for /l loop, see below.):
#echo off
setlocal enableDelayedExpansion
set /a counter=2
call echo %%!counter!
endlocal
so:
setlocal enableDelayedExpansion
set /a counter=0
for /l %%x in (1, 1, %argCount%) do (
set /a counter=!counter!+1
call echo %%!counter!
)
endlocal
If to keep the code short rather than wise, then
for %%x in (%*) do (
echo Hey %%~x
)
#ECHO OFF
SETLOCAL
SET nparms=0
FOR /l %%i IN (1,1,20) DO (
SET myparm=%%i
CALL :setparm %*
IF DEFINED myparm SET nparms=%%i&CALL ECHO Parameter %%i=%%myparm%%
)
ECHO there were %nparms% parameters in %*
GOTO :EOF
:setparm
IF %myparm%==1 SET myparm=%1&GOTO :EOF
shift&SET /a myparm -=1&GOTO setparm
GOTO :eof
This should show how to extract random parameters by position.
For simple iteration can't we just check for additional arguments with "shift /1" at the end of the code and loop back? This will handle more than 10 arguments, upper limit not tested.
:loop
:: Your code using %1
echo %1
:: Check for further batch arguments.
shift /1
IF [%1]==[] (
goto end
) ELSE (
goto loop
)
:end
pause
sample input in cmd:
test.bat /p 1,3,4
expected result:
1
3
4
my codes so far:
#echo off
set arg = %1
set var = %2
if (%1)==(/p) (
... need code that will read and print each character of var
)
There is a potential problem with your question. If test.bat is:
#echo %1%
Then
test 1,2,3
Prints:
1
Because, in this context, the comma is treated as an argument delimiter.
So you either need to enclose in quotes:
test "1,2,3"
Or use a different internal delimiter:
test 1:2:3
Unless you want the parts to be placed in %2, %3, etc., in which case you problem is solved by a trivial use of SHIFT.
For my solution I have elected to require quotes around the group parameter, "1,2,3" (though this is easily adapted for a different delimiter by changing delims=, to specify the character you want to use).
#echo off
setlocal ENABLEDELAYEDEXPANSION ENABLEEXTENSIONS
set args=%~2
if "%1"=="/p" (
:NextToken
for /F "usebackq tokens=1* delims=," %%f in ('!args!') do (
echo %%f
set args=%%g
)
if defined args goto NextToken
)
Call like:
readch.bat /p "1,2,3"
%~2 is used to remove the quotes.
The FOR statement parses args, puts the first token in %f and the remainder of the line in %g.
The `goto NextToken' line loops until there are no more tokens.
#echo off
if "%1"=="/p" (
:LOOP
echo %2
shift
set arg=%2
if defined arg goto :LOOP else exit >nul
)
set params=%%~2
if (%1)==(/p) (
set params=%params:,= %
)
if "%params%" NEQ "" (
call :printer %params%
)
goto :eof
:printer
:shifting
if "%%1" NEQ "" (
echo %%1
) else (
goto :eof
)
shift
goto :shifting
goto :eof
as discussed in an other thread How to avoid cmd.exe interpreting shell special characters like < > ^
it is not easy to get all parameters from the command line.
A simple
set var=%1
set "var=%~1"
are not enough, if you have a request like
myBatch.bat abc"&"^&def
I have one solution, but it needs a temporary file, and it is also not bullet proof.
#echo off
setlocal DisableDelayedExpansion
set "prompt=X"
(
#echo on
for %%a in (4) do (
rem #%1#
)
) > XY.txt
#echo off
for /F "delims=" %%a in (xy.txt) DO (
set "param=%%a"
)
setlocal EnableDelayedExpansion
set param=!param:~7,-4!
echo param='!param!'
It fails with something like myBatch.bat %a, it display 4 not the %a
in this situation a simple echo %1 would work.
It's obviously the for-loop but I don't know how to change this.
Perhaps there exists another simple solution.
I don't need this to solve an actual problem, but I like solutions that are bullet proof in each situation, not only in the most cases.
I don't think anyone found any holes in this, except for the inability to read newlines in the parameters:
#echo off
setlocal enableDelayedExpansion
set argCnt=1
:getArgs
>"%temp%\getArg.txt" <"%temp%\getArg.txt" (
setlocal disableExtensions
set prompt=#
echo on
for %%a in (%%a) do rem . %1.
echo off
endlocal
set /p "arg%argCnt%="
set /p "arg%argCnt%="
set "arg%argCnt%=!arg%argCnt%:~7,-2!"
if defined arg%argCnt% (
set /a argCnt+=1
shift /1
goto :getArgs
) else set /a argCnt-=1
)
del "%temp%\getArg.txt"
set arg
The above comes from a lively DosTips discussion - http://www.dostips.com/forum/viewtopic.php?p=13002#p13002. DosTips user Liviu came up with the critical SETLOCAL DisableExtensions piece.
The code below is based on the rambling Foolproof Counting of Arguments topic on DosTips and this answer by jeb:
#echo off & setLocal enableExtensions disableDelayedExpansion
(call;) %= sets errorLevel to 0 =%
:: initialise variables
set "paramC=0" & set "pFile=%tmp%\param.tmp"
:loop - the main loop
:: inc param counter and reset var storing nth param
set /a paramC+=1 & set "pN="
:: ECHO is turned on, %1 is expanded inside REM, GOTO jumps over REM,
:: and the output is redirected to param file
for %%A in (%%A) do (
setLocal disableExtensions
set prompt=#
echo on
for %%B in (%%B) do (
#goto skip
rem # %1 #
) %= for B =%
:skip - do not re-use this label
#echo off
endLocal
) >"%pFile%" %= for A =%
:: count lines in param file
for /f %%A in ('
find /c /v "" ^<"%pFile%"
') do if %%A neq 5 (
>&2 echo(multiline parameter values not supported & goto die
) %= if =%
:: extract and trim param value
for /f "useBack skip=3 delims=" %%A in ("%pFile%") do (
if not defined pN set "pN=%%A"
) %= for /f =%
set "pN=%pN:~7,-3%"
:: die if param value is " or "", else trim leading/trailing quotes
if defined pN (
setLocal enableDelayedExpansion
(call) %= OR emulation =%
if !pN!==^" (call;)
if !pN!=="" (call;)
if errorLevel 1 (
for /f delims^=^ eol^= %%A in ("!pN!") do (
endLocal & set "pN=%%~A"
) %= for /f =%
) else (
>&2 echo(empty parameter values (""^) not supported & goto die
) %= if errorLevel =%
) else (
:: no more params on cmd line
set /a paramC-=1 & goto last
) %= if defined =%
:: die if param value contains "
if not "%pN:"=""%"=="%pN:"=%" (
>&2 echo(quotes (^"^) in parameter values not supported & goto die
) %= if =%
:: assign nth param, shift params, and return to start of loop
set "param%paramC%=%pN%" & shift /1 & goto loop
:last - reached end of params
:: no param values on cmd line
if %paramC% equ 0 (
>&2 echo(no parameter values found & goto die
) %= if =%
:: list params
set param
goto end
:die
(call) %= sets errorLevel to 1 =%
:end
:: exit with appropriate errorLevel
endLocal & goto :EOF
The following conditions will terminate the program immediately:
no parameters found
multiline parameter
empty parameter (""", or " is permitted for the last parameter)
one or more quotes (") in a parameter value
To ease these restrictions, simply comment out the relevant lines. Read the inline comments for more information. Do not attempt to turn off the multiline parameter trap!
I invented the syntax-error-technic to solve the problem (partially).
With this solution it's even possible to receive multiline parameters and also carriage return characters.
There is no known parameter which fails!
BUT the drawback of this solution, the main process exits and only a child process continues.
That is a consequence of the capture trick, a syntax error is created by using an invalid parenthesis block ( Prepare ) PARAMS....
But the syntax error itself outputs the complete block, including the expanded value of %*.
The output is redirected to a file by the permanent redirect technic.
And the child process can retrieve the complete parameter from the file.
This solution can be useful, when the batch file only handles the parameter and always exit afterwards.
#echo off
REM *** Thread redirector
for /F "tokens=3 delims=:" %%F in ("%~0") do goto %%F
REM *** Clear params.tmp
break > params.tmp
start "" /b cmd /k "%~d0\:StayAlive:\..\%~pnx0 params.tmp"
(set LF=^
%=empty=%
)
REM *** Change prompt for better recognition
prompt #PROMPT#
REM *** Change streams permanently
REM *** stream1 redirects to params.tmp
REM *** stream2 redirects to nul
echo on >nul 2>nul 0>nul 3>params.tmp 4>nul 5>&3
#REM *** This is the magic part, it forces a syntax error, the error message itself shows the expanded %asterix without ANY modification
( Prepare ) PARAMS:%LF%%*%LF%
echo Works
exit /b
REM *** Second thread to fetch and show the parameters
:StayAlive
:__WaitForParams
if %~z1 EQU 0 (
goto :__WaitForParams
)
REM *** Show the result
findstr /n "^" %1
It's up to the user who types the command to escape any special characters. Your program cannot do anything about what the shell does before your program even runs. There is no other "bullet proof" solution to this.