2

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.

Foad S. Farimani
  • 12,396
  • 15
  • 78
  • 193
  • 1
    You could specify `< nul program.bat` to provide empty input and not halt. To accept multiple input lines you could put a `:Label` first, then `set /P "piped=" || goto :EOF` (to skip on empty input), then do something with `%piped%`, and then loop back by `goto :Label`. Same loop structure with arguments when using `"%~1"` and `shift` (which is more robust than parsing `%*`), leaving by `if "%~1"=="" goto :EOF`… – aschipfl Oct 22 '20 at 19:55
  • @Gerhard Imagin the `program.bat` as *nix's `grep`. The script I'm working on needs to be as versatile. – Foad S. Farimani Oct 22 '20 at 19:59
  • 1
    I deleted the comment, I think I get what you wanted there. I am working on a better response. – Gerhard Oct 22 '20 at 20:01
  • @aschipfl I can't change the use syntax. It has to be with on single pipe `|`. If there was a way to know if anything is piped to the script or not the problem could be solved. – Foad S. Farimani Oct 22 '20 at 20:01
  • 1
    I think you should refer to [this](https://www.dostips.com/forum/viewtopic.php?p=32615#p32615) topic on dostips by @DBenham – Gerhard Oct 22 '20 at 20:44
  • 1
    @Gerhard Thanks. Will read it. A Tl;dr Eli5 summary would be highly appreciated though. :) – Foad S. Farimani Oct 22 '20 at 20:47
  • maybe @dbenham can help us here? – Foad S. Farimani Oct 22 '20 at 20:59
  • will you actually be piping with linefeeds or just multiple args in the one pipeline to be read as new lines? – T3RR0R Oct 22 '20 at 21:00
  • not sure if I understand, but consider command as dir which outputs multiple lines at once. – Foad S. Farimani Oct 22 '20 at 21:02
  • I can help as well, for now however, the question is a bit broad regarding your expected results. Consider multiple commands as stated. What do you expect the outcome to be? What commands are you wanting and what types of paramaters do you expect to pass? I gave the post by dbenham as a baseline as it seems to suit your scenario. Are you trying to simulate `grep`? – Gerhard Oct 22 '20 at 21:32
  • Sorry, but I have to withdraw the suggestion with `set /P` in a `goto` loop, because [`set /P` fails together with pipes](https://stackoverflow.com/q/41351844) and leads to unexpected results (somehow it seems to mess around with its internal data pointer); seems I totally forgot about that before… – aschipfl Oct 22 '20 at 21:55
  • @aschipfi at least that explains why i cant get that method to read in more than two lines from a pipe – T3RR0R Oct 22 '20 at 23:07
  • 1
    Take a look at [Dostips: How to detect if input comes from pipe or redirected file](https://www.dostips.com/forum/viewtopic.php?t=2371) – jeb Oct 23 '20 at 08:11
  • Thanks @jeb . great to have you here. – Foad S. Farimani Oct 23 '20 at 11:30
  • folks, I wrote a **P.S.** – Foad S. Farimani Oct 23 '20 at 23:24
  • @Gerhard My intention is pure curiosity. I want to learn. – Foad S. Farimani Oct 23 '20 at 23:25

1 Answers1

3

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>
    
aschipfl
  • 33,626
  • 12
  • 54
  • 99
  • Thanks a lot. I will test this and will get back here. – Foad S. Farimani Oct 23 '20 at 06:37
  • would you be kind to check the **P.S**? basically, with this method, the script can not detect if it's being piped to or it is the one piping. – Foad S. Farimani Oct 23 '20 at 23:24
  • 1
    Yes, unfortunately not, because `CmdCmdLine` is the same for either side of the pipe. I am not aware of a method to distinguish between the sides, sorry for that… – aschipfl Oct 24 '20 at 11:13
  • no worries. it is all for fun and you have already been kind to help me learn. one idea might be to measure the maximum timing required for piping and put a timeout. there is the choice command but that only gets the first letter of the piped string / singular character , I suppose. – Foad S. Farimani Oct 25 '20 at 08:47
  • 1
    My current idea was to use `tasklist` (or `wmic Process`) to get the whole command line with the pipe and parse it to find out on what side the batch file was, but without success, unfortunately, because that command line does not seem to be reflected in any of the available fields (like window title, or command line). Also, I do not yet have an idea how to implement your suggestion of `choice`… – aschipfl Oct 25 '20 at 13:24
  • 1
    Eventually I found a way to detect on what side of a pipe the script is involved: the `timeout` command does not accept redirected input and returns an error, which can be detected (by the `||` operator) in case, which occurs when it is on the right side… – aschipfl Oct 26 '20 at 01:33