windowspowershellcmd

Passing through arguments from a .cmd to a .exe


I have an executable that receives a number of command line arguments and I want to write a .cmd that performs a few checks and then passes on all the arguments to the executable.

As an example, take the executable echo_args.exe which is this Rust app:

use std::env;

fn main() {
    // Collect the command line arguments into a vector
    let args: Vec<String> = env::args().collect();

    // Iterate over the arguments and print each one
    for arg in args.iter() {
        println!("{}", arg);
    }
}

The fact that it's Rust has no bearing on the question, but this way you can reproduce my situation.

Calling this app directly from PowerShell:

echo_args.exe "hello `"`"world`"`""

Prints, as expected:

echo_args.exe
hello ""world""

Calling this app directly from Command Prompt:

echo_args.exe "hello """"world"""""

Prints, as expected:

echo_args.exe
hello ""world""

In both cases, a correct way of escaping double quotes is used, appropriate to the command processor.

However, if I write a test.cmd file, I run into the problem that it behaves differently when called from PowerShell or from Command Prompt.

The naive solution is:

@echo off
set exe=echo_args.exe
"%exe%" %*

And although that works when called from Command Prompt:

test "hello """"world"""""

It does not from PowerShell because:

test "hello `"`"world`"`""

Now prints:

echo_args.exe
hello "world"

Note that the double quotes get 'eaten' by the command processor running the .cmd (i.e. cmd.exe). So, Powershell passes on the hello ""world"" to cmd.exe (like it would to echo_args.exe) and cmd.exe dedupes the double quotes and passes hello "world" to the .exe.

Now, I realise that I can avoid this by also writing a test.ps1 that performs the same function, so that the .cmd does not get called from PowerShell, and for practical purposes that is fine, but my question is this: is there a way to write the .cmd so that it behaves correctly, regardless if it is started with cmd.exe automatically with PowerShell, or if it is found and executed from Command Prompt directly?

Detecting whether cmd.exe was launched from PowerShell seems error-prone and complicated. And I don't see a straightforward way of (re)constructing the arguments in the .cmd that avoids this problem (since the problem essentially happens before that code even runs). What am I missing? I've asked a few LLMs (ChatGPT 4o and Claude Sonnet 3.5), but they insist on proving their uselessness for problems that require some nuance and come up with an endless slew of non-solutions.


Solution

  • The sad reality in Windows PowerShell (the legacy, ships-with-Windows, Windows-only edition of PowerShell whose latest and last version is 5.1) as well as in (now obsolete) versions of PowerShell (Core) 7 (up to v7.2.x) is that an extra, manual layer of \-escaping of embedded " characters is required in arguments passed to external programs.

    Therefore, in Windows PowerShell (and now-obsolete PowerShell 7.2- versions; in PowerShell 7.3+, the calls work analogously with the \ instances removed):

    echo_args.exe "hello \`"\`"world\`"\`""
    

    Alternatively, since no string interpolation is needed in your case, using a verbatim string ('...') obviates the need for PowerShell's escaping:

    echo_args.exe 'hello \"\"world\"\"'
    

    However, even when an expandable (interpolating) string ("...") is needed, there is a way to avoid the need for PowerShell's escaping by way of using the (invariably multiline) here-string variant

    $addressee = 'world'
    echo_args.exe @"
    hello \"\"$addressee\"\"
    "@
    

    Finally, for the sake of completeness, both PowerShell editions (all versions) offer --%, the so-called stop-parsing token, which essentially copies what follows it verbatim to the process command line constructed behind the scenes. This entails severe limitations, however, notably the inability to reference PowerShell variables - see the bottom section of this answer for details:

    # Use of the stop-parsing token, --%
    
    # Both variations work, because most CLIs on Windows accept "" and \"
    # interchangeably as an escaped "
    
    echo_args.exe --% "hello """"world""""
    
    echo_args.exe --% "hello \"\"world\"\""
    

    Caveat:


    Caveat re PowerShell 7.3+ when invoking a batch file:


    As for your observations and questions:

    is there a way to write the .cmd so that it behaves correctly

    It follows from the above that Windows PowerShell is the culprit, so you must compensate for its buggy behavior there on invocation.

    the double quotes get 'eaten' by the command processor running the .cmd (i.e. cmd.exe). So, Powershell passes on the hello ""world"" to cmd.exe (like it would to echo_args.exe) and cmd.exe dedupes the double quotes and passes hello "world" to the .exe.

    No, it isn't cmd.exe that "eats" the double quotes, it is, in effect, the broken way in which Windows PowerShell places the verbatim value it has parsed according to its syntax rules on the process command line it uses to call external programs behind the scenes. cmd.exe passes whatever it receives on as-is, with only minimal interpretation (none in the case at hand).

    Specifically, the - broken - process command line that Windows PowerShell constructs is:

    # Windows PowerShell (and PowerShell 7.2-): BROKEN
    # *Process* command line constructed behind the scenes, if
    # echo_args.exe "hello `"`"world`"`"" is submitted: 
    echo_args.exe "hello ""world"""
    

    This is broken, because (Windows) PowerShell - which of necessity must pass a double-quoted string to external programs, as only this form of quoting can be expected to be understood by Windows CLIs - neglects to escape the embedded ".
    That is, in order to pass verbatim hello ""world"" to an external program, either "hello """"world""""" or, more typically, "hello \"\"world\"\"" must be placed on the process command line.

    PowerShell 7.3+ now does perform this escaping, using \" (but as noted, by default not for batch files).

    # PowerShell 7.3+: OK
    # *Process* command line constructed behind the scenes, if
    # echo_args.exe "hello `"`"world`"`"" is submitted: 
    echo_args.exe "hello \"\"world\"\""