Convert a batch-file command with complex arguments to PowerShell

Note:

  • JamesQMurphy’s helpful answer is the best solution in this case.

  • This answer generally discusses translating command lines written for cmd.exe to PowerShell.


Translating cmd.exe (batch-file) command lines to PowerShell is tricky – so tricky, that in PSv3 pseudo-parameter
--%, the stop-parsing token, was introduced
:

Its purpose is to allow you to pass everything that comes after --% as-is through to the target program, so as the control the exact quotingexcept that cmd.exe-style %...%-style environment-variable references are still expanded by PowerShell[1].

However, --% comes with many severe limitations – discussed below – and its usefulness is limited to Windows, so it should be considered a last resort;

An alternative is to use the PSv3+ Native module (install with Install-Module Native from the PowerShell Gallery in PSv5+), which offers two commands that internally compensate for all of PowerShell’s argument-passing and cmd.exe‘s argument-parsing quirks:

  • Function ie, which you prepend to any call to an external program, including cmd.exe, allows you to use only PowerShell‘s syntax_, without having to worry about argument-passing problems; e.g.:

    # Without `ie`, this command would malfunction.
    'a"b' | ie findstr 'a"b'
    
  • Function ins (Invoke-NativeShell) function allows you to reuse command lines written for cmd.exe as-is, passed as a single string; unlike --%, this also allows you to embed PowerShell variables and expressions in the command line, via a PowerShell expandable string ("..."):

    # Simple, verbatim cmd.exe command line.
    ins 'ver & whoami'
    
    # Multi-line, via a here-string.
    ins @'
      dir /x ^
        c:\
     '@
    
    # With up-front string interpolation to embed a PowerShell var.
    $var="c:\windows"; ins "dir /x `"$var`""
    

Limitations and pitfalls of --%

  • --% must follow the name/path of the external utility to invoke (it can’t be the first token on the command line), so that the utility executable (path) itself, if passed by [environment] variable, must be defined and referenced using PowerShell syntax.

  • --% supports only one command, which an unquoted |, || or && on the same line, if present, implicitly ends; that allows you to pipe / chain such a command to / with other commands.

    • However, using ; in order to unconditionally place another command on the same line is not supported; the ; is passed through verbatim.

    • --% reads (at most) to the end of the line so spreading a command across multiple lines with line-continuation chars. is NOT supported.[2]

  • Other than %...% environment-variable references, you cannot embed any other dynamic elements in the command; that is, you cannot use regular PowerShell variable references or expressions.

    • Escaping % characters as %% (the way you can do inside batch files) is not supported; %<name>% tokens are invariably expanded, if <name> refers to a defined environment variable (if not, the token is passed through as-is).
  • Other than %...% environment-variable references, you cannot embed any other dynamic elements in the command; that is, you cannot embed regular PowerShell variable references or expressions.

  • You cannot use stream redirections (e.g., >file.txt), because they are passed verbatim, as arguments to the target command.

    • For stdout output you can work around that by appending | Set-Content file.txt instead, but there is no direct PowerShell workaround for stderr output.
    • However, if you invoke your command via cmd, you can let cmd handle the (stderr) redirection (e.g., cmd --% /c nosuch 2>file.txt)

Applied to your case, this means:

  • %winscp% must be translated to its PowerShell equivalent, $env:winscp, and the latter must be prefixed with &, PowerShell’s call operator, which is required when invoking external commands that are specified by variable or quoted string.
  • & $env:winscp must be followed by --% to ensure that all remaining arguments are passed through unmodified (except for expansion of %...% variable references).
  • The list of arguments from the original command can be pasted as-is after --%, but must be on a single line.

Therefore, the simplest approach in your case – albeit at the expense of having to use a single line – is:

# Invoke the command line with --%
# All arguments after --% are used as-is from the original command.
& $env:winscp --% /ini=nul /log=C:\TEMP\winscplog.txt /command "open scp://goofy:[email protected]/ -hostkey=""ssh-rsa 2048 d4:1c:1a:4c:c3:60:d5:05:12:02:d2:d8:d6:ae:6c:5d""" "put ""%outfile%"" /home/public/somedir/somesubdir/%basename%" "exit"

[1] Note that, despite the cmd.exe-like syntax, --% also works on Unix-like platforms in PowerShell Core (macOS, Linux), but is of very limited use there: unlike with native shells such as bash there, --% only works with double-quoted strings ("..."); e.g., bash --% -c "hello world" works, but bash --% -c 'hello world' doesn’t – and the usual shell expansions, notably globbing, aren’t supported – see this GitHub issue.

[2] Even `, PowerShell’s own line-continuation character, is treated as a pass-through literal. cmd.exe isn’t even involved when you use --% (unless you explicitly use cmd --% /c ...), so its line-continuation character, ^, cannot be used either.

Leave a Comment