Date: | July 31, 2012 / year-entry #175 |
Tags: | code |
Orig Link: | https://blogs.msdn.microsoft.com/oldnewthing/20120731-00/?p=7003 |
Comments: | 28 |
Summary: | 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'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
The for /f %%i in (words.txt) do echo [%%i]
The loop variable in the
I'm cheating here because I know that 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
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
for /f "delims=" %%i in ('printappdir') do cd "%%i"
We ask the 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 /f "tokens=1-2,14" %%i in ('ipconfig') do ^ if "%%i %%j"=="IPv4 Address." set IPADDR=%%k
The above command asked to execute the
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 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 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)
Comments are closed. |
"Exercise: What if you want to extract more than 26 words?"
You use the SHIFT command, of course.
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.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.
How about
echo "set foo=" > foo.bat
COMMAND > foo.bat
call foo.bat
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.
Love Batch Week! This is very useful info. Thanks
You can also use the delims to your advantage. In this case, if you use ':' as a delimiter you can avoid all the counting.
'ipconfig ^| findstr /C:"IPv4 Address" ^| findstr /v "Autoconfiguration"' will filter that out.
Or you do this:
ipconfig ^| findstr /R "^IPv4 Address"
Some escaping may be needed.
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:
Have a IPv4.bat in the current directory.
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).
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.
Run the output of step 4 (execution of IPv4.bat stops here).
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).
(optional) Do a CLS to clear the junk in the screen.
(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…
Preemptive snarky comment: the word "PowerShell" is forbidden during Batch File Week :-P .
@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.
@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.
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%
Ups! 2nd call to echo not needed
for /F "tokens=1-11" %%a in ('echo %%z') -> for /F "tokens=1-11" %%a in ("%%z")
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.
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 :-)
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
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 '|'
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.
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"
@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.
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.
@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.
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.
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…
@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.
@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.