42

I have a delimited list of IPs I'd like to process individually. The list length is unknown ahead of time. How do I split and process each item in the list?

@echo off
set servers=127.0.0.1,192.168.0.1,10.100.0.1

FOR /f "tokens=* delims=," %%a IN ("%servers%") DO call :sub %%a

:sub
    echo In subroutine
    echo %1
exit /b

Outputs:

In subroutine
127.0.0.1
In subroutine
ECHO is off.

Update: Using Franci's answer as reference, here's the solution:

@echo off
set servers=127.0.0.1,192.168.0.1,10.100.0.1

call :parse "%servers%"
goto :end


:parse
setlocal
set list=%1
set list=%list:"=%
FOR /f "tokens=1* delims=," %%a IN ("%list%") DO (
  if not "%%a" == "" call :sub %%a
  if not "%%b" == "" call :parse "%%b"
)
endlocal
exit /b

:sub
setlocal
echo In subroutine
echo %1
endlocal
exit /b

:end

Outputs:

In subroutine
127.0.0.1
In subroutine
192.168.0.1
In subroutine
10.100.0.1
bjaxbjax
  • 1,351
  • 2
  • 15
  • 24

8 Answers8

100

The for command handles a number of delimiters by default. In this case you can do

@echo off

set servers=127.0.0.1,192.168.0.1,10.100.0.1

for %%i in (%servers%) do (
  echo %%i
)

If you run into a delimiter that is not natively supported, you could do a replace to first prepare the string so it is in the right format

@echo off

set servers=127.0.0.1@192.168.0.1@10.100.0.1
set servers=%servers:@=,%

for %%i in (%servers%) do (
  echo %%i
)

Using recursive calls has the chance to run out of stack space once you go over a certain number of items in the list. Also testing it, the recursion pattern seems to run a bit slower.

Matthew Benedict
  • 1,071
  • 2
  • 7
  • 4
31

Update: If the number of items in the list is not known, it is still possible to parse all items with simple recursion on the head of the list. (I've changed the IPs to simple numbers for simplicity of the list)

Finally that Lisp class I took 18 years ago paid off...

@echo off
setlocal

set servers=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24

echo %servers%

call :parse "%servers%"

goto :eos

:parse

set list=%1
set list=%list:"=%

FOR /f "tokens=1* delims=," %%a IN ("%list%") DO (
  if not "%%a" == "" call :sub %%a
  if not "%%b" == "" call :parse "%%b"
)

goto :eos

:sub

echo %1

goto :eos

:eos
endlocal
Franci Penov
  • 74,861
  • 18
  • 132
  • 169
  • 2
    This doesn't work -- it just prints "b" and "c" after the first IP address. Besides, the OP stated that the number of inputs would be unknown ahead of time. This may not be possible in a pure DOS batch file -- but I"m glad to know that I'm not the only one still using them! – harpo Mar 26 '10 at 16:47
  • my mistake, forgot to change the * to the token numbers. And you are right - if the number of tokens is not known ahead of time, it's not possible in standard Windows shell scripting. – Franci Penov Mar 26 '10 at 17:12
  • Exactly harpo. The servers variable serves simple as an example. In practice (in my case, this script will be used as part of the deployment scheme), the servers variable is read as a user parameter. Knowing I ask the impossible is also good so I can move on to alternative approaches. – bjaxbjax Mar 26 '10 at 17:14
  • 2
    Now see what you guys made me do. I actually pulled out my ages old copy of Windows NT Shell Scripting (the most used book in my technical library) to refresh my rusty shell scripting skills... :-) A little bit of recursion goes a long way to solving problems like this. – Franci Penov Mar 26 '10 at 17:35
  • 1
    Excellent. Recursion to the rescue! – bjaxbjax Mar 26 '10 at 17:53
  • Sure enough -- pretty amazing! – harpo Mar 26 '10 at 18:01
  • @harpo: That batch already uses features that didn't exist back in DOS times. I'm pretty sure the OP wanted a Windows batch file anyway. – Joey Mar 26 '10 at 18:02
  • 1
    Franci: You can probably make the `call :parse` in the loop a `goto parse` which should exit the loop instead of recursing. You don't really need recursion here anyway and stack space is limited. – Joey Mar 26 '10 at 18:04
  • @Johannes is right, this can be easily transformed into an iterative solution. – Franci Penov Mar 26 '10 at 23:20
7

The difficulty is that the for command is intended to work on multiline text strings that you would typically find in files. The best solution I've found to this problem is to modify your string to be multiline.

rem You need to enable delayed expansion, otherwise the new line will interfere
rem with the for command.
setlocal ENABLEDELAYEDEXPANSION
rem The following three lines escape a new line and put it in a variable for
rem later.
rem The empty lines are important!
set newline=^


set list=jim alf fred
for /F %%i in ("%list: =!newline!%") do (echo item is %%i)

This final for loop will iterate over the items in the space separated variable. It does it by replacing the space in the list variable with newlines, then doing the normal for loop.

kybernetikos
  • 8,281
  • 1
  • 46
  • 54
  • Just to add, while the for /? documentation says that (set) is a set of files, it does work fine for it to be something else: e.g. for %i in (%data%) do echo %i will process comma separated lists in data perfectly well. The answer above is handy if you want to do more complex processing based on tokens with for /F, but most of the time doing a straight for is fine. – kybernetikos Apr 24 '12 at 22:25
5

Yes, I know this is a very old topic, but I recently discovered and interesting trick that can perform the requested split in a much simpler way. Here it is:

set n=1
set "server[!n!]=%servers:,=" & set /A n+=1 & set "server[!n!]=%"

These two lines split the servers string into the server array and also define n variable with the number of created elements. This way, you just need to use a for /L command from 1 to %n% to process all elements. Here it is the complete code:

@echo off
setlocal EnableDelayedExpansion

set servers=127.0.0.1,192.168.0.1,10.100.0.1

set n=1
set "server[!n!]=%servers:,=" & set /A n+=1 & set "server[!n!]=%"

for /L %%i in (1,1,%n%) do echo Process server: !server[%%i]!

This method is not just simpler and faster than any other method based on for command, but it also allows to directly use a multi-character string as delimiter.

You may read further details on the splitting method at this post.

Community
  • 1
  • 1
Aacini
  • 65,180
  • 12
  • 72
  • 108
5

I can't believe this is so complicated! Seems like everyone would want to split a string frequently without knowing how many tokens are in it and without parsing it by lines. Typically, it seems people are writing to a file and then step through it or using a multi sub method like these others. What a mess!

Here's my solution. It's easier to follow and works inline, i.e. no sub routine. It is nice to build a list of filenames or whatever and then step through them without having to write to them to a temp file, ensure there is no file writing collision, delete the temp file etc. Plus I don't know how to return a value from a sub like it was a function - I don't think you can. So you'd have to keep writing 2 subs with the other answers I see here each time you wanted to do something like this - one that feeds the other. My method lets you easily create lists and step through them as many times as you want throughout the script.

setlocal enableextensions enabledelayedexpansion

set SomeList=
set SomeList=!SomeList!a;
set SomeList=!SomeList!b;
set SomeList=!SomeList!c;
set SomeList=!SomeList!d;
set SomeList=!SomeList!e;
set SomeList=!SomeList!f;
set SomeList=!SomeList!g;

set List=!SomeList!
:ProcessList
FOR /f "tokens=1* delims=;" %%a IN ("!List!") DO ( 
  if "%%a" NEQ "" ( 
      echo %%a
  )
  if "%%b" NEQ "" (
      set List=%%b
      goto :ProcessList
  )
)

Output:

a
b
c
d
e
f
g

Obviously you can just exchange the echo with something useful.

Jonathan
  • 51
  • 1
  • 1
3

This is a generalisation of Franci's solution, so that you can perform any job on each item in your list, without having copies of the list iteration code.

:list_iter
    rem Usage: call :list_iter :do_sub_for_each_item "CSV of items"
    set foo=%1
    set list=%~2
    for /f "tokens=1* delims=," %%i in ("%list%") do (
        call %foo% %%i
        if not "%%j" == "" ( call :list_iter %foo% "%%j" )
    )
    exit /b

Eg, you can call it by:

call :list_iter :my_foo "one,two,three"
call :list_iter :my_bar "sun,poo,wee"
azhrei
  • 2,303
  • 16
  • 18
0

If PowerShell is available you could:

C:>set servers=127.0.0.1,192.168.0.1,10.100.0.1
C:>powershell -NoProfile -Command "('%servers%').Split(',')"
127.0.0.1
192.168.0.1
10.100.0.1

Another use to display a readable PATH variable.

@powershell -NoProfile -Command "($Env:PATH).Split(';')"

or

powershell -NoProfile -Command "$Env:PATH -split ';'"
lit
  • 14,456
  • 10
  • 65
  • 119
0

Let me try to improve Franci Penov's method, maybe a little better.

    @echo off

    :Main
        setlocal
        set servers=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24
        echo %servers%
        call :func_split_string "%servers%" "," "split_string_callback"
        endlocal & echo on & goto :eof

    :split_string_callback [%1/aItem]
        echo %1
        goto :eof

    :func_split_string [%1/to_split_str] [%2/delims_char] [%3/callback]
        setlocal
        set to_split_str=%~1
        set to_split_str=%to_split_str:"=%
        set _token_exp="tokens=1* delims=%~2"
        for /f %_token_exp% %%a in ("%to_split_str%") do (
            if not "%%a" == "" call :%~3 %%a
            if not "%%b" == "" call :func_split_string "%%b" "%~2" "%~3"
        )
        endlocal & goto :eof
Jac08in
  • 19
  • 5