A script making it easy to set a variable with the output of a command
Posted: 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:
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:
To see the actual for /f command that runs under the hood, use the -X option:
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:
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:
Use '$ -?' to get a help screen with the available options.
Any improvement idea welcome!
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'))
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>
Code: Select all
C:\JFL\Temp>$ -X HOST hostname
for /f "delims=" %v in ('hostname ^| findstr /R "^."') do set HOST=%v
C:\JFL\Temp>
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>
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>
Any improvement idea welcome!