Search code examples
powershellmavenpipeescapingquoting

How to escape pipes in Maven argument string in Powershell


I am trying to make the following command work in Powershell:

mvn -Dhttp.nonProxyHosts='xxx.xxx.*|*.example.com|*.example2.com|localhost|127.0.0.1' -DskipTests clean install

This command works fine in Ubuntu, but I unfortunately need a version working in Powershell. This command keeps failing with the message:

The command "*.example.com" is either misspelled or could not be found

So clearly, the pipes are interpreted as pipes.

I have tried:

  • Escaping the pipes with backslashes \, backticks ` and circumflexes ^
  • Enclosing the whole argument with double quotes (turns the argument blue, still same error):
mvn "-Dhttp.nonProxyHosts='xxx.xxx.*|*.example.com|*.example2.com|localhost|127.0.0.1'" "-DskipTests" clean install
  • Enclosing the argument with double quotes and trying all of the escape characters above.

None of these had any effect.

I have no idea how to get Powershell to ignore the pipes inside of the argument. Powershell version is PSVersion 5.1.19041.3031.


Solution

  • Your workaround is effective (but you don't need the "..." around -DskipTests).
    Because there are many factors at play, let me try to explain them:

    It isn't obvious, but it isn't pipes (|) or quoting on the PowerShell side that are the problem. Instead, you're seeing the confluence of two problematic behaviors:

    • A long-standing bug in PowerShell's parameter binder when calling external programs, present up to at least v7.3.6, affecting --prefixed arguments that also contain . - see GitHub issue #6291.

      • The bug causes a --prefixed argument that also contains . to broken in two, at the (first) . That is, after removal of the ' quotes, your
        -Dhttp.nonProxyHosts=... argument is passed as two arguments, -Dhttp and .nonProxyHosts=...

      • Normally, the simplest workaround is to `-escape the initial -, but because mvn is a batch file this is not effective here, for the reasons explained below.

      • For background information and alternative workarounds, see this answer, but read on for why your specific workaround that involves nested quoting is needed.

    • mvn happens to be implemented as a batch file (mvn.cmd), and batch files inappropriately parse their arguments as if they had been passed from inside a cmd.exe session (a long-standing "quirk" that will not be fixed).

      • This becomes a problem when PowerShell passes a space-less argument that happens to contain cmd.exe metacharacters such as | to a batch file, because - when PowerShell of necessity rebuilds the command line to pass to the target process - it uses on-demand double-quoting of arguments, and never uses double quotes around arguments that do not contain spaces - irrespective of what quoting was originally used (also note that when double-quoting is applied, it is invariably the entire argument that is double-quoted, irrespective of any partial quoting in the original PowerShell argument).

      • A simplified example:

        • Submitting mvn 'foo|bar' (or mvn "foo|bar" or mvn foo`|bar) in PowerShell constructs the process command line as mvn.cmd foo|bar, i.e. without quoting, which then breaks the batch file, due to the unquoted |.
      • GitHub issue #15143 proposed making PowerShell accommodate this problematic batch-file behavior by also double-quoting space-less arguments if they contain cmd.exe metacharacters, but, sadly, it was rejected.


    Your workaround with nested quoting - '..."..."' actually only works due to another long-standing bug, namely the broken way in which arguments with embedded " chars. are passed to external programs - see this answer.

    While this bug is fixed in v7.3+, on Windows the old, broken behavior is by default selectively retained, notably when calling batch files, due to the $PSNativeCommandArgumentPassing preference variable defaulting to the ill-fated Windows mode.

    A simple example:

    • The proper translation of PowerShell argument 'foo="bar"' for the process command line is "foo=\"bar\"" (or foo=\"bar\"), whereas the bug results in foo="bar" - which in your case happens to be what you actually need.

    • In PowerShell 7.3+, "foo=\"bar\"" is indeed what you get if you call any executable not covered by the selective exceptions, or if you've opted out of the exceptions via $PSNativeCommandArgumentPassing = 'Standard'

    Note that Unix-like platforms are unaffected in v7.3+ (Standard is the default). The concept of a process-level command line (fortunately) doesn't even exist on Unix-like platforms: instead, arguments are passed as an array of verbatim values.


    Future-proof workarounds:

    • The default value of $PSNativeCommandArgumentPassing on Windows may stay Windows forever, though the fact that backward compatibility in this area was already broken once, in v7.3, suggests that future changes are a possibilty.

    • By contrast, in Windows PowerShell (the legacy edition whose latest and last version is v5.1) the old, broken behavior that your workaround relies on is guaranteed to stay in place, given that Windows PowerShell is no longer actively developed and will receive only security-critical updates.

    For PowerShell (Core) 7+, there are two future-proof workarounds - neither of them great:

    Option A: (Temporarily) set $PSNativeCommandArgumentPassing to Legacy, which guarantees that the workaround that relies on the old, broken behavior will continue to work:

    # Note the required use of embedded "..." (double-quoting)
    # Enclosing the statements in & { ... } runs them in a *child scope*,
    # which means that the $PSNativeCommandArgumentPassing change is limited to that scope.
    & {
      $PSNativeCommandArgumentPassing = 'Legacy'
      mvn '-Dhttp.nonProxyHosts="xxx.xxx.*|*.example.com|*.example2.com|localhost|127.0.0.1"' -DskipTests clean install
    }
    
    • An alternative is to pass the whole command line as a string to cmd /c, which generally gives you full control over the resulting quoting, but note that the command line must then fulfill cmd.exe's syntax requirements, and requires embedded quoting to use "...":

      cmd /c 'mvn -Dhttp.nonProxyHosts="xxx.xxx.*|*.example.com|*.example2.com|localhost|127.0.0.1"' -DskipTests clean install'
      
      • Note: This too relies on the "-escaping bug and only works because the "quirks" of cmd.exe's command-line parsing require embedded " chars. not to be escaped, i.e. cmd.exe's behavior cancels out PowerShell's bug.

      • The caveat is that this workaround isn't cross-platform, due to relying on cmd.exe; it may not be obvious, but your workaround in combination with $PSNativeCommandArgumentPassing = 'Windows' and the --% workaround below do also work on Unix-like platforms, even in v7.3+, though the (embedded) quoting must be limited to "..." (double quotes).

    Option B: Use --%, the stop-parsing token, which, however, comes with many limitations, notably the inability to directly incorporate PowerShell variables and expressions in the arguments (see the bottom section of this answer); what follows --% is in essence copied verbatim to the process command line:

    # Note the --% and the required use of "..." (double-quoting)
    mvn --% -Dhttp.nonProxyHosts="xxx.xxx.*|*.example.com|*.example2.com|localhost|127.0.0.1" -DskipTests clean install