A batch script that stores optional piped multiline string and optional arguments

Viewed 378

Consider the cmd/batch script program.bat which accepts optional piping and command-line arguments. The script will be executed in four different ways:

  1. program.bat: without piping and arguments
  2. program.bat <arguments>: with arguments
  3. <command> | program.bat: with piped multiline string
  4. <command> | program.bat <arguments>: with both piped multiline string and arguments

It is important to note that the output of <command> is not necessarily a single line (e.g., dir). The script should work with or without any of the piped information and arguments. Case 2 can easily be achieved using the %* to reach the arguments in the form of a list/array (reference). If I were sure that there will always be a one-line string piped I could use: (reference)

SET /p "piped="

However, this will expect the user to enter something in cases 1 and 2, when nothing is piped, which is undesirable. Plus, this only stores the first line of the <command> output to the piped variable. I would appreciate if you could help me know how I can have a script that:

  • works in all 4 above scenarios
  • stores the optional piped multiline string into a variable

P.S. To test the answer by @aschipfl, I created a file program.bat with the content:

@echo %CmdCmdLine%

I ran it in 3 different ways:

  • program.bat resulting

cmd

  • <someRandomCommand> | program.bat resulting

C:\WINDOWS\system32\cmd.exe /S /D /c" program.bat"

  • program.bat | FIND /v ""

C:\WINDOWS\system32\cmd.exe /S /D /c" program.bat"

although it is a brilliant workaround to detect the piping itself, it can not distinguish if the program.bat is the one who is piping or the one which is being piped to. This is not an issue that I had thought about.

1 Answers

The Windows Command Processor cmd.exe features a built-in pseudo-variable called CmdCmdLine that holds the original command line that invoked it.

Given that the Command Prompt is opened just by cmd.exe (without any arguments!), CmdCmdLine holds the (quoted) value of ComSpec, usually "C:\Windows\system32\cmd.exe".

When a batch file is run from such a Command Prompt window by typing its path/name, the value of CmdCmdLine does not change (because the already open cmd.exe instance remains the same).

However, when the script is involved in a pipe, which initiates new cmd.exe instances for either side, CmdCmdLine changes to something like C:\Windows\system32\cmd.exe /S /D /c" …", where … stands for the batch file together with its arguments just as provided.

We can now make use of that and check CmdCmdLine whether it just contains a single item (the ComSpec value) or multiple ones (ComSpec plus some arguments), and if the second one equals /S.

Here is an sample script (named program.bat) to demonstrate what I mean (although this will unfortunately fail when the original Command Prompt window has been invoked by something like %ComSpec% /S …):

@echo off
setlocal EnableExtensions DisableDelayedExpansion

rem // Enable delayed expansion to safely return `CmdCmdLine`:
setlocal EnableDelayedExpansion
>&2 setlocal DisableDelayedExpansion
>&2 echo/
>&2 echo BatFile = ^<%0^>
>&2 echo BatArgs = ^<%*^>
>&2 endlocal
>&2 echo/
>&2 echo ComSpec = ^<!ComSpec!^>
>&2 echo CmdLine = ^<!CmdCmdLine!^>
for %%L in (!CmdCmdLine!) do (
    rem /* `!!` are consumed by delayed expansion, hence the following condition
    rem    is only true for the first iteration because of the `endlocal`: */
    if "!!"=="" (
        endlocal
    ) else (
        rem /* This point is only reached when there are more than one items in
        rem    `CmdCmdLine`, which is the case when the script was involved in
        rem    a pipe (though it does not matter on which side it stood);
        rem    for this to work we assume that the second item, hence the first
        rem    argument, is `/S` as usual for pipes; the hosting Console Window
        rem    must not have been invoked by `cmd.exe /S ...` then though: */
        >&2 echo CmdSwit = ^<%%~L^>
        >&2 set "CmdSwit=%%~L"
        if /I "%%~L"=="/S" (
            >&2 echo/
            >&2 set "CmdSwit="
           rem /* `timeout` does not accept redirected input, which can be used
           rem    to distinguish between the sides of the pipe: */
           > nul 2>&1 timeout /T 0 || call :PIPE
        )
        goto :ARGS
    )
)
:ARGS
>&2 echo/
rem // Parsing each argument individually is more robust as parsing `%*`:
:ARGS_LOOP
if "%~1"=="" goto :NEXT
rem // Do something with `%1` here...
>&2 echo BatArg# = ^<%1^>
shift /1
goto :ARGS_LOOP

:NEXT
>&2 (if defined CmdSwit echo/& pause)
rem // ...

endlocal
exit /B


:PIPE
    setlocal DisableDelayedExpansion
    rem /* The `for /F` loop awaits the whole input text before iterating!
    rem    `more` hangs when there are more than about 64K lines and it converts
    rem    tabs to spaces (a tab becomes one space here due to switch `/T1`);
    rem    use `findstr "^"` instead when you expect more than about 64K lines;
    rem    regard that line lengths are limited to about 8K characters: */
    for /F delims^=^ eol^= %%I in ('more /S /T1') do (
        rem // Do something with `%%I` here...
        >&2 echo PipLin# = ^<%%I^>
    )
    endlocal
    exit /B

Note that lines beginning with >&2 are nothing but debugging lines for illustration.


Here are some usage examples (debugging output):

  1. Script invoked by command line program.bat (no pipe, no arguments):

    BatFile = <program.bat>
    BatArgs = <>
    
    ComSpec = <C:\Windows\system32\cmd.exe>
    CmdLine = <"C:\Windows\system32\cmd.exe" >
    
  2. Script invoked by command line program.bat arg1 arg2 (no pipe, some arguments):

    BatFile = <program.bat>
    BatArgs = <arg1 arg2>
    
    ComSpec = <C:\Windows\system32\cmd.exe>
    CmdLine = <"C:\Windows\system32\cmd.exe" >
    
    BatArg# = <arg1>
    BatArg# = <arg2>
    
  3. Script invoked by command line (echo pip1^&echo pip2^&rem/) | program.bat (pipe with some lines, no arguments):

    BatFile = <program.bat>
    BatArgs = <>
    
    ComSpec = <C:\Windows\system32\cmd.exe>
    CmdLine = <C:\Windows\system32\cmd.exe  /S /D /c" program.bat">
    CmdSwit = </S>
    
    PipLin# = <pip1>
    PipLin# = <pip2>
    
  4. Script invoked by command line (echo pip1^&echo pip2^&rem/) | program.bat arg1 arg2 (pipe with some lines, some arguments):

    BatFile = <program.bat>
    BatArgs = <arg1 arg2>
    
    ComSpec = <C:\Windows\system32\cmd.exe>
    CmdLine = <C:\Windows\system32\cmd.exe  /S /D /c" program.bat arg1 arg2">
    CmdSwit = </S>
    
    PipLin# = <pip1>
    PipLin# = <pip2>
    
    BatArg# = <arg1>
    BatArg# = <arg2>
    
Related