1

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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
CloudWatcher
  • 181
  • 1
  • 11
  • Have you tried using the call operator, ampersand? This prevnts parsing the arguments, but I don't know if it prevents misinterpreting the pipes. – Walter Mitty Jul 20 '23 at 09:57
  • I had not tried that. Not sure how to use it, but I have just put it in front of the whole command, same error. Putting it before the maven arg results in an error that the ampersand cannot be used in that place. – CloudWatcher Jul 20 '23 at 10:02
  • @WalterMitty, the `&` (call) operator does _not_ prevent argument parsing. It behaves just like direct invocation, and is only needed for _syntactic_ reasons (if a command is specified quoted and/or via variable references). You're probably thinking of `--%`, the stop-parsing token; while it could be used here, there's a simpler and more flexible workaround. – mklement0 Jul 20 '23 at 15:04

2 Answers2

1

A lot of trial and error later, I got this to work:

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

The single quotes from the command working in Ubuntu must be replaced with double quotes, and the whole Maven argument must be enclosed in single quotes. That way, the pipes are apparently interpreted as part of the string and the double quotes are kept. I could not get this to work with single quotes in the argument, that gave me the escaping problem again.

I would have liked to keep the single quotes in the maven argument, but this seems to work, too.

CloudWatcher
  • 181
  • 1
  • 11
1

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
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • This was very insightful, thank you. However, I copied your proposed solution (backtick definitely present), and unfortunately it still gave me the same error. – CloudWatcher Jul 21 '23 at 05:20
  • Good point, @CloudWatcher - I neglected to consider that `mvn` happens to be implemented _as a batch file_, which amounts to a perfect storm of quoting and escaping headaches. In short: your workaround is indeed the right one and will continue to work in _Windows PowerShell_. To make it future-proof, a bit more work is needed. Please see my update for the gory details. – mklement0 Jul 21 '23 at 13:39