Search code examples
powershellescapingquotesdouble-quotessingle-quotes

PowerShell: escaping rules for CLI calls


As I have learned, when invoking PowerShell from cmd.exe, with the -Command option, escaping rules are essentially those used in Linux. So you use a backslash (\), rather than the usual backtick (`).

This is to say that you don't write:

C:\> powershell -c "echo `"That's a single quote: ' `""

but

C:\> powershell -c "echo \"That's a single quote: ' \""

to get:

That's a single quote: '

And this is the exact equivalent of what you would type in a Linux terminal:

~ >>> bash -c "echo \"That's a single quote: ' \""                                                                      

If I am not wrong, this feature is named PSNativeCommandArgumentPassing. But comparisons work up to a certain point. In fact, under Linux you write:

~ >>> bash -c "echo \"That's a double quote: \\\" \""                                                              

to get:

That's a double quote: " 

while the equivalent PowerShell line:

C:\> powershell -c "echo \"That's a double quote: \\\"  \""

gives

The string is missing the terminator: ".

By trial and error, I realised that:

C:\> powershell -c "echo \"That's a double-quote: `""  \""

works as expected.

Can you explain to me what is the logic behind: `""?

Also, what are the equivalent commands when calling PowerShell from powershell.exe, rather than cmd.exe?


Solution

  • tl;dr

    • On PowerShell's command line only, use \" to pass a " through to the code that -c (-Command) should execute.

      • Only in the resulting code does PowerShell's usual escaping of " as `" apply, so you may have to combine the two escaping techniques: `\".

        • Your attempt, `"", had the same effect, but it doesn't work reliably.[1]
      • \" works robustly with respect to PowerShell's own parsing of its command line, but can situationally break calls on the cmd.exe side - see the bottom section for workarounds.

    • When calling CLIs (external console applications) from PowerShell, not only do PowerShell's own, different quoting rules apply first (support for '...' strings, embedded " inside "..." escaped as `"), a long-standing bug up to PowerShell 7.2.x additionally requires escaping " chars. with \ when embedded in external-program arguments (only); see this answer.


    this feature is named PSNativeCommandArgumentPassing

    No; this feature - which became official in PowerShell 7.3 (see this answer) - does not come into play, because:

    • it generally isn't (and won't be) available in the legacy Windows PowerShell edition that you're invoking via its CLI, powershell.exe (whereas the modern, cross-platform PowerShell (Core) edition's CLI is pwsh.exe)

    • it only applies to calls from inside a PowerShell session.

    • it is designed to address a long-standing problem when calling external programs with embedded " or empty-string string arguments from PowerShell - see this answer.

      • Therefore, the linked answer addresses your last question:

        • Also, what are the equivalent commands when calling PowerShell from powershell.exe, rather than cmd.exe?

      • In short: unfortunately, up to at least PowerShell 7.2.x you'll have to additionally, manually escape " chars. with " embedded in external-program arguments (only).


    Why the following works from cmd.exe:

    powershell -c "echo \"That's a single quote: ' \""
    
    • PowerShell only recognizes \ as an escape character on its command line, for consistency with other CLIs.

      • Inside a PowerShell session, only ` (backtick) serves as the escape character.

      • Caveat: While \" to escape " works consistently in both PowerShell editions on the PowerShell (command-line parsing) side, situationally it can break cmd.exe's own parsing - see the bottom section.

    • When the PowerShell CLI is invoke via -c (-Command) with a piece of PowerShell source code to execute, that code (the argument(s) following -c is parsed in two stages:

      • First, all unescaped " chars. are stripped, wheres escaped ones (\") are kept and unescaped.
      • Only then is the result parsed and execute as PowerShell code.

    Therefore, what PowerShell ends up executing is verbatim:

    echo "That's a single quote: ' "
    

    From the above follows why this does not work:

    :: SYNTAX ERROR
    powershell -c "echo \"That's a double quote: \\\"  \""
    

    PowerShell ends up trying to execute verbatim

    echo "That's a double quote: \" "
    

    which is a syntax error, because inside a PowerShell session \ doesn't escape " -only `" or - inside "...", alternatively - "" do.


    From the above follows why this (mostly) works:

    :: Works, but not robustly
    powershell -nop -c "echo \"That's a double-quote: `""  \""
    
    • "" sometimes, but not always works as an alternative to \";[1] here it does - see also the bottom section.

    • As a result, PowerShell ends up executing the following verbatim, which works, because the escaped " that was passed through is now `-escaped, as it needs to be inside a PowerShell "..." string:

       echo "That's a double-quote: `"  "
      

    To avoid the brittle "" escaping, it is better to formulate this command by combining the required `-escaping with the command-line \-escaping - i.e. `\" - but see the bottom section for a fully robust solution:

    powershell -nop -c "echo \"That's a double-quote: `\"  \""
    

    Avoiding parsing problems on the cmd.exe side, a safe alternative to \":

    While \" to escape " works consistently in both PowerShell editions on the PowerShell (command-line parsing) side, situationally it can break cmd.exe's own parsing. By contrast, using \" is safe when calling from no-shell environments such as Task Scheduler.

    While there are workarounds, they are, unfortunately PowerShell edition-specific:

    • In Windows PowerShell (powershell.exe)

      • Use "^"" (sic) instead of \"
    • In PowerShell (Core) (v6+, pwsh.exe)

      • Use "" instead of \"

    Important:

    • These workarounds require that the whole code to pass to -c (-Command) be passed as a single, "..."-enclosed argument.

    • -c (-Command) also accepts multiple arguments - which may individually be double-quoted or not - in which case it simply concatenates these arguments, after having stripped unescaped ", to form the code to execute. This technique situationally allows you to get away with \"-escaping (e.g., powershell -c \"Between 2 & 3`\"\", but (a) it requires you to pay close attention to which parts of the command cmd.exe will see as unquoted, (b) would require you to ^-escape any cmd.exe metacharacters such as & in those parts, and (c) invariably performs whitespace normalization, i.e. folds runs of multiple spaces into a single one each.

    The following calls, designed to print verbatim  Between 2 & 3" , demonstrate this:

    :: BREAKS, because cmd.exe sees `&` as *outside a double-quoted string*
    powershell -c " \" Between 2 & 3`\" \" "
    
    :: PowerShell (Core) workaround
    pwsh -c " "" Between 2 & 3`"" "" "
    
    :: Windows PowerShell workaround
    powershell -c " "^"" Between 2 & 3`"^"" "^"" "
    

    [1] An example of where "" inside "..." doesn't work is powershell -c "echo \" Nat `""King`"" Cole \"": instead of Nat "King" Cole, it prints Nat "King Cole, i.e. the second escaped " is missing (it would work fine in pwsh.exe, however, as discussed in the bottom section). It's ultimately not worth speculating how, precisely, embedded "" sequences are parsed by powershell.exe -c, given that it demonstrably isn't reliable and that reliable alternatives do exist (\" or, from cmd.exe, also "^"").