A script making it easy to set a variable with the output of a command

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Post Reply
Message
Author
jfl
Posts: 226
Joined: 26 Oct 2012 06:40
Location: Saint Hilaire du Touvet, France
Contact:

A script making it easy to set a variable with the output of a command

#1 Post by jfl » 22 Jan 2021 10:47

Unix shells, and Windows own PowerShell, allow nesting commands inside each other using the $(subcommand) syntax.
This syntax is extremely convenient for capturing the output of a subcommand, and passing it as an argument to an outer command.
For example, using Docker, you can find containers that have a given property, stop them, and delete them, all in a single command line:

Code: Select all

docker rm $(docker stop $(docker ps -a -q --filter='some_criteria'))
In Windows cmd.exe shell, the only equivalent would be to use the “for /f … (‘command’) … do …” syntax to capture the output of an inner command, and pass it on to an outer command. But this syntax is overly complex, and practically impossible to use for a two-level nesting like in the example above!

Looking for a workaround for the cmd.exe shell, I eventually developed a $.bat script that makes it easy to capture the output of a command into a variable. (I hope I'm not reinventing the wheel!)
So with successive calls to that script, it's easy to pass on the output of commands to other commands, through intermediate variables. All that without having to do any copying and pasting in the cmd console.

The simplest way of using $.bat is to pass it the variable name, then the command to capture, and its arguments:

Code: Select all

C:\JFL\Temp>set HOST
Environment variable HOST not defined

C:\JFL\Temp>$ HOST hostname
set HOST=JFLZB

C:\JFL\Temp>set HOST
HOST=JFLZB

C:\JFL\Temp>
To see the actual for /f command that runs under the hood, use the -X option:

Code: Select all

C:\JFL\Temp>$ -X HOST hostname
for /f "delims=" %v in ('hostname ^| findstr /R "^."') do set HOST=%v

C:\JFL\Temp>
Now I was not satisfied with this, as this did not allow capturing the output of a complex pipeline of commands.
So I added a second way of operating the $.bat script: If no command is passed in, it captures the data on its standard input:

Code: Select all

C:\JFL\Temp>set HOST
Environment variable HOST not defined

C:\JFL\Temp>hostname | $ HOST

C:\JFL\Temp>set HOST=JFLZB

C:\JFL\Temp>set HOST
HOST=JFLZB

C:\JFL\Temp>
Note that the command "set HOST=JFLZB" was not typed by me. It was generated by $.bat itself.
This is due to a batch limitation: When run in a pipe, a batch is actually run in a sub-shell. So if it sets a variable, that change is lost when that sub-shell exits at the end of the pipe.
I worked around that limitation by using a JScript subroutine, that uses the WScript.Shell.SendKeys() routine to generate keystrokes in the parent shell.
Still, it's a dirty trick, and its performance is poor. If anybody has an idea on how to do that in pure batch, I'm interested!

Finally note that this $.bat script is still a work in progress.
I know that the usual tricky batch characters will cause problems. I need to add some careful escaping.
Another known shortcoming is that $.bat only captures the last non-empty line in the subcommand output.
In the Docker scenario I used as an example in the beginning, there might be several containers that match the selection criteria, and you'd want to capture them all.
But there are cases where this is an advantage:

Code: Select all

C:\JFL\Temp>$ DT wmic os get LocalDateTime
set DT=LocalDateTime
set DT=20210122170704.345000+060

C:\JFL\Temp>
Use '$ -?' to get a help screen with the available options.

Any improvement idea welcome!

Squashman
Expert
Posts: 4486
Joined: 23 Dec 2011 13:59

Re: A script making it easy to set a variable with the output of a command

#2 Post by Squashman » 22 Jan 2021 11:49

The very first comment you make is about the nesting of commands to use as input to another command. This really doesn't solve that problem unless I am not understanding how to use your program to do that.

jfl
Posts: 226
Joined: 26 Oct 2012 06:40
Location: Saint Hilaire du Touvet, France
Contact:

Re: A script making it easy to set a variable with the output of a command

#3 Post by jfl » 22 Jan 2021 12:45

No, you still can't nest them, but you can get the same effect very easily:

Code: Select all

$ VAR1 innermost_command
$ VAR2 middle_command %VAR1%
outer_command %VAR2%
So, yes, three commands instead of one. But barely more characters to type overall than in Bash or PowerShell.

jfl
Posts: 226
Joined: 26 Oct 2012 06:40
Location: Saint Hilaire du Touvet, France
Contact:

Re: A script making it easy to set a variable with the output of a command

#4 Post by jfl » 27 Jan 2021 12:14

I've updated $.bat. It now creates a list with one entry for each non-empty line in the command output.
This makes it really usable for the Docker scenarios that motivated me to begin this effort.
Entries that contain spaces or special characters are quoted. This allows looping on the list. Ex:

Code: Select all

C:\JFL\Temp>$ DIRS dir /b /ad C:\p*
set DIRS=.pytest_cache PerfLogs "Program Files" "Program Files (x86)" ProgramData Python27amd64

C:\JFL\Temp>for %d in (%DIRS%) do @echo %~d
.pytest_cache
PerfLogs
Program Files
Program Files (x86)
ProgramData
Python27amd64

C:\JFL\Temp>
If your command outputs just one line, you can come back to the initial $.bat behavior, storing the last output line without quoting, using the -l option:

Code: Select all

C:\JFL\Temp>$ PF echo %ProgramFiles%
set PF="C:\Program Files"

C:\JFL\Temp>$ -l PF echo %ProgramFiles%
set PF=C:\Program Files

C:\JFL\Temp>
All this also works when using the pipe mode, capturing data from stdin: (While still automatically entering the set command at the next command prompt.)

Code: Select all

C:\JFL\Temp>dir /b /ad C:\p* | $ DIRS
set DIRS=.pytest_cache PerfLogs "Program Files" "Program Files (x86)" ProgramData Python27amd64

C:\JFL\Temp>set DIRS=.pytest_cache PerfLogs "Program Files" "Program Files (x86)" ProgramData Python27amd64

C:\JFL\Temp>
I now begin to think about further extending the script to recursively process $() blocks in its argument line. This would fulfil the initial vision of a single command recursively expanding itself like in Bash or PowerShell. :)

Post Reply