Convert a batch-file command with complex arguments to PowerShell

你离开我真会死。 提交于 2020-04-10 05:29:13

问题


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

"%winscp%" /ini=nul ^
           /log=C:\TEMP\winscplog.txt ^
           /command "open scp://goofy:changeme@10.61.10.225/ -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:changeme@10.61.10.225/ -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.


回答1:


Whenever I have to invoke an executable from PowerShell, I always pass the parameters as a list, like below. I also use single quote marks, unless I'm actually substituting variables, in which case I have to use the double quotation marks.

 & SomeUtility.exe @('param1','param2',"with$variable")

It gets a little tricky when there are spaces in the parameters, since you may have to provide quotation marks so that the utility can properly parse the command.

Your example is even more tricky, since WinScp wants the argument of the /command parameter enclosed in quotation marks, and any strings inside enclosed in double quotation marks. All of that needs to be preserved, because of WinScp. I believe the following would work. I've broken up the parameters into multiple lines for readability. I'm also assuming that you've successfully populated your $winscp, $outfile, and $basename variables.

$params = @(
  '/ini=nul',
  '/log=C:\TEMP\winscplog.txt',
  '/command',
  '"open scp://goofy:changeme@10.61.10.225/ -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"'
)

& $winscp $params

Note the parentheses around the fifth parameter; this is due to the string concatenation operations there. (Without the parentheses, each operand would have been added to the list separately -- you can confirm this by looking at $params.Count.) Also keep in mind that you will need quotation marks around your log file path if you ever have to change it to something with spaces.




回答2:


Martin Prikryl (the author of WinSCP) also provides a .Net assembly, which might be a better choice if you want to switch to PowerShell.

Example from the documentation updated with the parameters from your commandline:

try {
    # Load WinSCP .NET assembly
    Add-Type -Path 'WinSCPnet.dll'

    # Setup session options
    $sessionOptions = New-Object WinSCP.SessionOptions -Property @{
        Protocol = [WinSCP.Protocol]::Sftp
        HostName = '10.61.10.225'
        UserName = 'goofy'
        Password = 'changeme'
        SshHostKeyFingerprint = 'ssh-rsa 2048 d4:1c:1a:4c:c3:60:d5:05:12:02:d2:d8:d6:ae:6c:5d'
    }

    $session = New-Object WinSCP.Session

    try {
        # Connect
        $session.Open($sessionOptions)

        # Upload files
        $transferOptions = New-Object WinSCP.TransferOptions
        $transferOptions.TransferMode = [WinSCP.TransferMode]::Binary

        $transferResult = $session.PutFiles($outfile, "/home/public/somedir/somesubdir/$basename", $false, $transferOptions)

        # Throw on any error
        $transferResult.Check()

        # Print results
        foreach ($transfer in $transferResult.Transfers) {
            Write-Host ("Upload of {0} succeeded" -f $transfer.FileName)
        }
    } finally {
        # Disconnect, clean up
        $session.Dispose()
    }

    exit 0
} catch [Exception] {
    Write-Host ("Error: {0}" -f $_.Exception.Message)
    exit 1
}



回答3:


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

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

Caveats:

  • --% 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.

  • --% of necessity reads (at most) to the end of the line[1] , so spreading a command across multiple lines with line-continuation chars. is NOT supported, and neither is placing additional commands on the same line.

  • 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:changeme@10.61.10.225/ -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 only with double-quoted strings ("...") are supported; e.g., bash --% -c "hello world" works, but bash --% -c 'hello world' doesn't - see this GitHub issue; also, the native globbing feature (expanding arguments such as *.txt to their matching filenames) doesn't work.

[2] --% treats everything as pass-through until the end of the line or the next unquoted occurrence of |, && or || (the latter two operators will be introduced in PowerShell v7), whichever comes first. Stopping at | allows piping the output from the --% command to another command.
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.




回答4:


I ran your code (modified to work with my own server, of course) and got the following log:

. 2017-03-04 15:32:24.387 Command-line: "C:\Program Files (x86)\WinSCP\WinSCP.exe" /ini=nul /log=C:\TEMP\winscplog.txt /command "open sftp://goofy:***@10.61.10.225/ -hostkey=ssh-rsa" 2048 d4:1c:1a:4c:c3:60:d5:05:12:02:d2:d8:d6:ae:6c:5d"" exit
. 2017-03-04 15:32:24.388 Time zone: Current: GMT-6, Standard: GMT-6 (Central Standard Time), DST: GMT-5 (Central Daylight Time), DST Start: 3/12/2017, DST End: 11/5/2017
. 2017-03-04 15:32:24.388 Login time: Saturday, March 4, 2017 3:32:24 PM
. 2017-03-04 15:32:24.388 --------------------------------------------------------------------------
. 2017-03-04 15:32:24.388 Script: Retrospectively logging previous script records:
> 2017-03-04 15:32:24.388 Script: open sftp://goofy:***@10.61.10.225/ -hostkey=ssh-rsa
. 2017-03-04 15:32:24.388 --------------------------------------------------------------------------

Note how in the first line it inserted a double quote immediately after -hostkey=ssh-rsa and in the last line the -hostkey parameter is truncated from what we expect it to be.

I was able to connect successful after doubling up the inner double quotes around the -hostkey parameter like this:

"/command" 'open sftp://goofy:changeme@10.61.10.225/ -hostkey=""ssh-rsa 2048 d4:1c:1a:4c:c3:60:d5:05:12:02:d2:d8:d6:ae:6c:5d""' `


来源:https://stackoverflow.com/questions/42601270/convert-a-batch-file-command-with-complex-arguments-to-powershell

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!