Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert a batch-file command with complex arguments to PowerShell

I have the following in .bat that is (this works):

"%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"

I have tried to duplicate that into a powershell script like this:

& $winscp "/ini=nul" `
           "/log=C:\TEMP\winscplog.txt" `
           "/command" 'open sftp://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"

When I run the .bat script the file will upload.

When I run the .ps1 script I get Host key does not match configured key ssh-rsa

I suspect that I have not formatted the command properly in powershell and the hostkey is getting mangled by the time winscp sees it.

I checked the log and all that is shown is the hostkey from the host. It does not show the key I am using. I confirmed that by changing my host and noting that it did not show up in the log. I compared the log between .bat and .ps1 and the difference is ps1 terminates with the error noted above.

winscp is a sftp utility.

like image 763
Be Kind To New Users Avatar asked Dec 10 '22 12:12

Be Kind To New Users


1 Answers

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 quoting - except 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.

like image 149
mklement0 Avatar answered May 11 '23 21:05

mklement0