Search code examples
powershellparameter-passingquoting

How to escape a string to pass it to another command?


I have an array of arguments $p = "a", "b c", "d`"e" to be passed to command. I can do this like so:

& my_command $p

Now, I need to call that same command inside some external wrapper (binary), which works like this using fixed parameters:

& my_wrapper /Command=my_command "/CommandParameters=a 'b c' 'd`"e'"

But how can I pass the /CommandParameters from the array $p?


One approach that works for some inputs is to pre-process the array like so:

$p = $p | Foreach {"\`"$_\`""}
& my_wrapper /Command=my_command "/CommandParameters=$p"

but this seems pretty fragile. Values in $p do contain spaces and quotes.


In Bash, I could use printf %q to properly escape the parameters.


Solution

  • The following encloses all strings in the array in embedded "..." and additionally escapes preexisting embedded " chars. as \", which is what external programs typically expect:

    Note: In Windows PowerShell and in PowerShell (Core) up to at least v7.1, an additional round of \-escaping is - unfortunately - needed when passing arguments with embedded " to external programs:

    $p = "a", "b c", "d`"e"
    $pEscaped = $p.ForEach({ '\"{0}\"' -f ($_ -replace '"', '\\\"') })
    & my_wrapper /Command=my_command "/CommandParameters=$pEscaped"
    

    Note: While $pEscaped is a collection (array) of strings, using it inside an expandable string ("...") automatically creates a space-separated single-line representation; e.g. "$('foo', 'bar')" yields verbatim foo bar

    This longstanding problem - the unexpected need for manual \-escaping of embedded " chars. when passing arguments to external programs - is summarized in this answer.

    Preview versions of v7.2 now come with experimental feature PSNativeCommandArgumentPassing, which is an attempted fix, but, unfortunately, it looks like it will lack important accommodations for high-profile CLIs on Windows - see this summary from GitHub issue #15143. However, the fix is effective for executables that expect " to be escaped as \", so the solution simplifies to (using string concatenation (+) inside an expression ((...)) to construct the argument in-line):

    # Note: Requires experimental feature PSNativeCommandArgumentPassing
    #       to be turned on, available in preview versions of v7.2
    & my_wrapper /Command=my_command ('/CommandParameters=' + $p.ForEach({ '"{0}"' -f ($_ -replace '"', '\"') }))
    

    Note:

    • Assuming this experimental feature becomes an official one (which is in general never guaranteed), the corrected behavior will most likely be opt-in, via a preference variable: $PSNativeCommandArgumentPassing = 'Standard' ('Legacy' selects the old behavior).

    If you don't mind installing a third-party module (authored by me), the Native module (Install-Module Native) comes with a backward- and forward-compatible helper function, ie, which too obviates the need for the extra escaping, while also containing the important accommodations for high-profile CLIs on Windows missing from the experimental feature:

    # Note: Assumes `Install-Module Native` was called.
    # Just use `ie` instead of `&`
    ie my_wrapper /Command=my_command ('/CommandParameters=' + $p.ForEach({ '"{0}"' -f ($_ -replace '"', '\"') }))
    

    As for what you tried:

    [Enclosing the value in embedded "] seems pretty fragile

    If you additionally escape any preexisting " as \" (\\\", if the argument-passing bug must be compensated for), as shown above, it works robustly.

    Ultimately, the following command line must be executed, which PowerShell constructs behind the scenes:

    my_wrapper /Command=my_command "/CommandParameters=\"a\" \"b c\" \"d\\\"e\""
    

    When my_wrapper parses its command line, it ends up seeing the following verbatim string as the last argument:

    /CommandParameters="a" "b c" "d\"e"
    

    In Bash, I could use printf %q to properly escape the parameters.

    Unfortunately, PowerShell has no equivalent feature, but it's not hard to provide it ad hoc:


    Meta-quoting strings in Powershell:

    Note:

    • By meta-quoting I mean the functionality that Bash's printf %q provides: It formats a given string value to become usable as a string literal in source code. For instance (this example illustrates the general principle, not printf %q's actual behavior), verbatim string value a b is transformed to verbatim string value "a b", and the latter can be used as an argument when constructing a command line stored in a string.

    • The required approach depends on whether the meta-quoted string is to be used in the string representation of a PowerShell command (such as a cmdlet call) or that of a call to an external program, given their different escaping needs. Additionally, while most external programs on Windows (also) understand \" to be an escaped ", some - notably batch files and msiexec.exe - only understand "".

    The commands below use the following sample input input string, which contains both a ' and a " (constructed via a verbatim here-string for quoting convenience):

    $str = @'
    6'1" tall
    '@
    

    The solutions below use the -f operator to synthesize the result strings, not just for conceptual clarity, but also to work around string interpolation bugs that can cause subexpressions embedded in expandable strings to yield incorrect results (e.g., "a$('""')b" and "a$('`"')b" both yield verbatim a"b - one " / the ` is missing); an alternative is to use simple string concatenation with +.

    • Unfortunately, it looks like these bugs will not be fixed, so as to preserve backward-compatibility; see GitHub issue #3039

    The verbatim content of the resulting strings is shown in source-code comments, enclosed in «...»; e.g. «"6'1\""» (this representation is just for illustration; PowerShell does not support such delimiters).

    Meta-quoting for external programs:

    Note:

    • On Windows, console applications typically only recognize double quotes as string delimiters in their command lines, so that's what the solutions below focus on. To create a single-quoted representation that is understood by POSIX-compatible shells such as bash, use the following:
      "'{0}'" -f ($str -replace "'", "'\''"), which yields verbatim '6'\''1" tall' (sic).

    • In edge cases on Windows, you may have to bypass PowerShell's command-line parsing altogether, so as to fully control the command line that is used for actual process creation behind the scenes, either via --%, the stop-parsing symbol, which has severe limitations or by delegating the invocation to cmd.exe, by passing an entire command line to /c - again, see this answer.

    For external programs that require \"-escaping (typical):

    # With the experimental, pre-v7.2 PSNativeCommandArgumentPassing
    # feature turned on, you can directly pass the result
    # as an external-program argument.
    # Ditto with the `ie` helper function.
    '"{0}"' -f ($str -replace '"', '\"') # -> «"6'1\" tall"»
    
    # With additional \-escaping to compensate for PowerShell's
    # argument-passing bug, required up to at least v7.1
    '\"{0}\"' -f ($str -replace '"', '\\\"') # -> «\"6'1\\\" tall\"»
    

    For external programs that require ""-escaping (e.g., batch files, msiexec - Windows-only):

    # CAVEAT: With the experimental, pre-v7.2 PSNativeCommandArgumentPassing
    #         feature turned on, passing the result as an external-program argument 
    #         will NOT work as intended, because \" rather than "" is invariably used.
    #         By contrast, the `ie` helper function automatically
    #         switches to "" for batch files, msiexec.exe and msdeploy.exe
    #         and WSH scripts.
    '"{0}"' -f ($str -replace '"', '""') # -> «"6'1"" tall"»
    
    # With additional escaping to compensate for PowerShell's
    # argument-passing bug, required up to at least v7.1
    '""{0}""' -f ($str -replace '"', '""""') # -> «""6'1"""" tall""»
    

    Meta-quoting for PowerShell commands:

    Note:

    • Fortunately, no extra escaping is ever needed in this case; the argument-passing bug discussed above only affects calls to external programs.

    Creating a double-quoted representation ("..."):

    '"{0}"' -f ($str -replace '"', '`"') # -> «"6'1`" tall"»
    

    Creating a single-quoted representation ('...'):

    "'{0}'" -f ($str -replace "'", "''") # -> «'6''1" tall'»