Reading the output of a command into a batch file variable


It’s Day Two of Batch File Week. Don’t worry, it’ll be over in a few days.

There is no obvious way to read the output of a command into a batch file variable. In unix-style shells, this is done via backquoting.

x=`somecommand`

The Windows command processor does not have direct backquoting, but you can fake it by abusing the FOR command. Here’s the evolution:

The /F flag to the FOR command says that it should open the file you pass in parentheses and set the loop variable to the contents of each line.

for /f %%i in (words.txt) do echo [%%i]

The loop variable in the FOR command takes one percent sign if you are executing it directly from the command prompt, but two percent signs if you are executing it from a batch file. I’m going to assume you’re writing a batch file, so if you want to practice from the command line, remember to collapse the double percent signs to singles.

I’m cheating here because I know that words.txt contains one word per line. By default, the FOR command sets the loop variable to the first word of each line. If you want to capture the entire line, you need to change the delimiter.

for /f "delims=" %%i in (names.txt) do echo [%%i]

There are other options for capturing say the first and third word or whatever. See the FOR /? online help for details.

Now, parsing files is not what we want, but it’s closer. You can put the file name in single quotes to say “Instead of opening this file and reading the contents, I want you to run this command and read the contents.” For example, suppose you have a program called printappdir which outputs a directory, and you want a batch file that changes to that directory.

for /f "delims=" %%i in ('printappdir') do cd "%%i"

We ask the FOR command to run the printappdir program and execute the command cd "%%i" for each line of output. Since the program has only one line of output, the loop executes only once, and the result is that the directory is changed to the path that the printappdir program prints.

If you want to capture the output into a variable, just update the action:

for /f %%i in ('printappdir') do set RESULT=%%i
echo The directory is %RESULT%

If the command has multiple lines of output, then this will end up saving only the last line, since previous lines get overwritten by subsequent iterations.

But what if the line you want to save isn’t the last line? Or what if you don’t want the entire line?

If the command has multiple lines of output and you’re interested only in a particular one, you can filter it in the FOR command itself…

for /f "tokens=1-2,14" %%i in ('ipconfig') do ^
    if "%%i %%j"=="IPv4 Address." set IPADDR=%%k

The above command asked to execute the ipconfig command and extract the first, second, and fourteenth words into loop variable starting with %i. In other words, %i gets the first word, %j gets the second word, and %k gets the fourteenth word. (Exercise: What if you want to extract more than 26 words?)

The loop then checks each line to see if it begins with “IPv4 Address.“, and if so, it saves the fourteenth word (the IP address itself) into the IPADDR variable.

How did I know that the IP address was the fourteenth word? I counted!

   IPv4 Address. . . . . . . . . . . : 192.168.1.1
   ---- -------- - - - - - - - - - - - -----------
     1      2    3 4 5 6 7 8 9  11  13      14
                              10  12

That’s also why my test includes the period after Address: The first dot comes right after the word Address without an intervening space, so it’s considered part of the second “word”.

Somebody thought having the eye-catching dots would look pretty, but didn’t think about how it makes parsing a real pain in the butt. (Note also that the above script works only for US-English systems, since the phrase IPv4 Address will change based on your current language.)

Instead of doing the searching yourself, you can have another program do the filtering, which is important if the parsing you want is beyond the command prompt’s abilities.

for /f "tokens=14" %%i in ('ipconfig ^| findstr /C:"IPv4 Address"') do ^
  set IPADDR=%%i

This alternate version makes the findstr program do the heavy lifting, and then saves the fourteenth word. (But this version will get fooled by the line Autoconfiguration IPv4 Address.)

Yes I know that you can do this in PowerShell

foreach ($i in Get-WmiObject Win32_NetworkAdapterConfiguration) {
  if ($i.IPaddress) { $i.IPaddress[0] }
}

You’re kind of missing the point of Batch File Week.

Comments (28)
  1. Rick C says:

    "Exercise: What if you want to extract more than 26 words?"

    You use the SHIFT command, of course.

  2. Adam Rosenfield says:

    Backquoting is all well and good for simple Unix shell scripts, but if you're doing any complicated nesting, it's preferred to use $(command) instead of command, since parens nest but backquotes don't.

  3. Dan Bugglin says:

    shift command is for %0-9 (and beyond), won't work on any other variables AFAIK.

    I did not know the for command could do this.  I figured you were going to tell us to > to a file and then use for on the file but no, you can cut out that middleman.  Good to know.

  4. Matt says:

    How about

    echo "set foo=" > foo.bat

    COMMAND > foo.bat

    call foo.bat

  5. 1 click bat file to share with "Authenticated Users":

    for /f "delims=" %%A in ('cd') do (set foldername=%%~nxA)

    net share "%foldername%"="%cd%" /grant:"Authenticated Users",Read

    Life's good with batch files amidst horrid GUIs. Thankfully, support hasn't been removed yet from cmd.exe citing lower telemetry usage or availability of PSH.

  6. jps says:

    Love Batch Week! This is very useful info. Thanks

  7. Ari says:

    You can also use the delims to your advantage. In this case, if you use ':' as a delimiter you can avoid all the counting.

    [No, it just shifts the counting. Now you have to count the dots so you include the right number in your comparison string. -Raymond]
  8. Maurits says:

    But this version will get fooled by the line Autoconfiguration IPv4 Address

    'ipconfig ^| findstr /C:"IPv4 Address" ^| findstr /v "Autoconfiguration"' will filter that out.

  9. Hello71 says:

    Or you do this:

       ipconfig ^| findstr /R "^IPv4 Address"

    Some escaping may be needed.

  10. Cesar says:

    The way I have seen people do these kinds of thing back in the DOS days (so, with a more limited batch language) was something like:

    1. Write the output of the command (in this example, ipconfig, even though AFAIK ipconfig did not exist in the DOS days) to a .bat file.
    2. Have a IPv4.bat in the current directory.

    3. Run the output of step 1, which will call the IPv4.bat (perhaps after a few "command or filename not found" error messages caused by the preceding lines in the generated file). Execution of the generated .bat file stops here (because it did not use CALL).

    4. The IPv4.bat received the value as a parameter. It creates another .bat file with a SET command to set the output to a variable.

    5. Run the output of step 4 (execution of IPv4.bat stops here).

    6. Run the original .bat file again, passing it a parameter to tell it to skip (with a GOTO) the above steps (execution of the second generated .bat file stops here).

    7. (optional) Do a CLS to clear the junk in the screen.

    8. (optional) Erase the temporary .bat files created in steps 1 and 4.

    Yeah, self-modifying batch file scripting. When all you have is a hammer…

  11. Antonio Rodríguez says:

    Preemptive snarky comment: the word "PowerShell" is forbidden during Batch File Week :-P .

  12. Skyborne says:

    @Adam Rosenfield, backquotes nest with the appropriate, tricky number of backslashes.  It's just a pain to change the nesting levels.

    Unix people like to make it possible to do everything, even if you want to claw your eyes out afterwards, than create an impossible case ("what if I want to send a literal backtick into a printf string run from my zsh prompt?").  This is also why you can technically embed interesting things like 07, 7f, or 1f5b376d in filenames.

  13. Tim! says:

    @Matt: your resulting batch file looks like

    set foo=

    [output from command]

    which will unset foo then run the output as a command, most likely resulting in "… is not recognized…"

    I'm not aware of any way to tell the redirection operator to omit the newline.

  14. Henrik Jensen says:

    Suggestion to solution for exercise.

    Reserve process the first 25 tokens, reserve the %%z for the reminder and run for /f again on the reminder.


    @echo off

    setlocal

    for /F "tokens=1-25,*" %%a in ('echo a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9') do ^

    set _first25= %%a %%b %%c %%d %%e %%f %%g %%h %%i %%j %%k %%l %%m %%n %%o %%p %%q %%r %%s %%t %%u %%v %%w %%x %%y && ^

    for /F "tokens=1-11" %%a in ('echo %%z') do set _last10= %%a %%b %%c %%d %%e %%f %%g %%h %%i %%j

    echo _first25=%_first25%

    echo _last10=%_last10%


  15. Henrik Jensen says:

    Ups! 2nd call to echo not needed

    for /F "tokens=1-11" %%a in ('echo %%z') -> for /F "tokens=1-11" %%a in ("%%z")

  16. JM says:

    Batch file programming is an exercise in constrained writing that makes Unix shell scripting look positively sane and appropriate. Personally, I'd rather be forced to write everything without the letter "t" than ever go back to systems that don't have PowerShell installed.

    Even when PowerShell didn't exist, I usually just gave up and copied over Win32 ports of Unix tools. The only dragon left to wrestle with then is figuring out which combination of sigils will get you the desired manner of escaping for your input (if the escaping you're looking for is even possible, that is, and doesn't mandate intermediate files).

    My mother always told me that if you have nothing nice to say, you shouldn't say anything at all, so I'd better stop here before I really unload.

  17. Jim says:

    For the exercise: do you have to use single character variable names? Instead of %%A, %%B, etc can't you use %ANOTHERVAR%?

    In any case, maybe parsing something that complicated with a batch file is a mistake :-)

  18. chentiangemalc says:

    hmm well I love batch file week! must have been growing up on MS-DOS. fun to see the younger IT crowd look in awe as you actually make a batch file do something useful. :) i got bored once and figured out how to take this IP address stuff a bit further, and do full binary conversion, calculate the network address. this allowed me to check if a computer was in a specific subnet. an example of the code i put on my blog here chentiangemalc.wordpress.com/…/how-to-do-binary-conversion-using-command-line-only

  19. M Hotchin says:

    Ah, memories.  I remember a similar question came up when I was working as Large Multination Corp, and at the time there were problems with bugs in CMD handling of 'delayed environment variable expansion'.

    And here's my part of the thread!  I knew keeping old mail around was, um, useful.

    ——

    There are games you can play with delayed env variable expansion, but there are some problems:

    • No good way to put a newline into the value of a env variable, so the 'contents' all ends up as one long line

    • You need to enable a special ability, using 'cmd /V' (but you can enable it by default).

    • Delayed env expansion is broken on 2195 SP1 (and maybe others).

    The example given in the help for 'set' is thus:

    set LIST=

    for %i in (*) do set LIST=!LIST! %i

    However, the final result is thus:

    E:someplace>set LIST

    LIST=!LIST! buildd.log coffbase.ast coffbase.dbg coffbase.rtl dirs gohist.dat srcbase

    CMD both interpreted '!LIST!', and added it to the value of the variable.  Even better:

    E:someplace>set LIST=foo

    E:someplace>for %i in (*) do set LIST=!LIST! %i

    E:someplace>set LIST

    LIST=foo buildd.log coffbase.ast coffbase.dbg coffbase.rtl dirs gohist.dat srcbase

    So, if the variable is empty to begin with, you get the wrong answer.  Whee.

    But just for grins:

    >set FOO=qwerty

    >type foo.txt

    foo bar

    foo

    bar

    baz

    >for /F "delims=" %i in (foo.txt) do set FOO=!FOO!^|%i

    >set FOO

    FOO=qwerty|foo bar|foo|bar|baz

    >set "FOO=%FOO:~7%"

    >set FOO

    FOO=foo bar|foo|bar|baz

    Voila, a env variable with the contents of a file, each line delimited by '|'

  20. ender says:

    Speaking of batch files, here's something I ran into a few years ago:

    eternallybored.org/…/mandel.cmd

    Yes, it does exactly what it's name implies.

  21. Andrew T says:

    I'm reminded of this cunnining one liner from a previous role – we were storing custom information in the comment attribute (needed for nt4 compadibility):

    for /f %%a in ('dsquery user -name e* -limit 1000^|dsget user -samid') do for /f "tokens=2,*" %%m in ('net user %%a /domain^|find /i "user's comment"') do for /f "tokens=1,2 delims=#" %%p in ("%%n") do net user %%a /usercomment:"%%p#%%q"

  22. Someone says:

    @Example by Andrew T

    This is nearly unreadable. And even on Unix where I have written a good amount of shell and awk scripts myself, I found that using scripts as part of tools makes this tools nearly unmaintenable. At all places command arguments and environment variables get expanded, but most of this breaks with spaces in filenames or directory names because its a real pain to correctly escape / quote all this cases.

    Using shell scripts a lot is nearly as bad as using symbolic links to simulate some kind of version control: One or two may be good, but as soon as there are 20 or more, its becomes very hard to understand.

    Its just try-and error because you do not have a debugger. You cannot single-step. You cannot inspect variables during the execution (only by putting "echo" everywhere). You do not have "compiler warnings" to catch typos.

    But the crown of all this mess goes to the classic Unix makefiles. To control the correct expansion of variables from the make script as also from the environment when using them as part of make rules or as part of commands is a very weird task.

  23. Cheong says:

    There's a reason why I always install Cygwin in my WinXP days. So many neat tools that'll save the day at your finger tips.

  24. Neil says:

    @Raymond I think Ari means this:

    for /f "tokens=2 delims=:" %%a in ('ipconfig ^| find "  IP Address"') do @for /f %%a in ("%%a") do @echo %%a

    The second for is necessary to trim the white space around the IP address found by the first for.

  25. Jonathan says:

    I learned about the ^ line continuation char immediately after I switched from a several-yaes position that involved many cmd file maintenance. It makes long commands so much nicer:

    msbuild bla.sln^

       /t:clean;build^

       /clp:verbosity=minimal

    etc.

  26. pcooper says:

    You should have Batch File Week every year. It's kind of like those obfuscated code contents or contrived languages like Whitespace. You can do everything, it's just a pain, and watching people use it is like watching some twisted type of art…

  27. Cesar says:

    @Someone: There is set -u, which warns of typos in variable names (sort of; it is actually "treat undefined variables as an error"), and set -e, which aborts the script in case any command fails (unless you catch it with || true). There is also set -x, which traces the command execution, and can be very useful for finding errors in your script.

    Just today we found an error where a sourced file was with CRLF line endings, resulting in a stray CR at the end of the variables set by that file; bash with set -x nicely showed the value (being passed as a parameter to a command) as $'asdfr' instead of asdf, making it obvious what the problem was.

  28. Someone says:

    @Cesar: I did know this all back then :) But when there are many scripts calling each other, each several hundred lines long, some of them containing very long awk/sed/whatever inline scripts (up to the HP UX maximum of 64k text), calling native custom EXEs in between to retrieve various infos from a database etc etc, then its time to throw it all away and use a real program.

    Some general remark: Sometimes, you see some command line "cmd arg1 arg2…." executed but you cannot see how this is broken up into the argv[] argument of cmd's main(). Also, with filename expansion done by the shell vs. filenames containing whitespace or special characters, you cannot easely see from the trace what parts of the text line are handled  by the interpreter as one or several tokens.

Comments are closed.