将具有复杂参数的批处理文件命令转换为 PowerShell

Convert a batch-file command with complex arguments to PowerShell

我在 .bat 中有以下内容(有效):

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

我试过将其复制到这样的 powershell 脚本中:

& $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"

当我 运行 .bat 脚本时,文件将上传。

当我 运行 .ps1 脚本时,我得到 Host key does not match configured key ssh-rsa

我怀疑我没有在 powershell 中正确格式化命令,并且在 winscp 看到它时主机密钥已经损坏。

我检查了日志,所有显示的都是来自主机的主机密钥。它没有显示我正在使用的密钥。我通过更改我的主机并注意到它没有出现在日志中来确认这一点。我比较了 .bat 和 .ps1 之间的日志,不同之处在于 ps1 以上述错误终止。

winscp 是一个 sftp 实用程序。

我运行你的代码(当然修改为与我自己的服务器一起工作)并得到以下日志:

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

请注意,它在第一行中如何在 -hostkey=ssh-rsa 之后立即插入一个双引号,而在最后一行中,-hostkey 参数是如何从我们期望的参数中截断的。

像这样将 -hostkey 参数周围的内部双引号加倍后,我能够成功连接:

"/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""' `

每当我必须从 PowerShell 调用可执行文件时,我总是将参数作为列表传递,如下所示。我也使用单引号,除非我实际上是在替换变量,在这种情况下我必须使用双引号。

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

参数中有空格时会有点棘手,因为您可能必须提供引号以便实用程序可以正确解析命令。

你的例子更加棘手,因为 WinScp 需要用引号括起来的 /command 参数的参数,以及用 double 引号括起来的任何字符串。由于 WinScp,所有这些都需要保留。我相信以下会起作用。为了便于阅读,我将参数分成多行。我还假设您已成功填充 $winscp$outfile$basename 变量。

$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

注意第五个参数两边的括号;这是由于那里的字符串连接操作。 (如果没有括号,每个操作数将单独添加到列表中——您可以通过查看 $params.Count 来确认这一点。)另外请记住,如果您曾经有过,则需要在日志文件路径周围加上引号将其更改为带空格的内容。

注:

  • 是这种情况下的最佳解决方案。

  • 这个答案通常讨论将为 cmd.exe 编写的命令行转换为 PowerShell.


cmd.exe(批处理文件)命令行转换为 PowerShell 非常棘手 - 如此棘手,以至于 in PSv3 伪参数 --%, the stop-parsing token,被介绍:

其目的是允许您将--%原样之后的所有内容传递给目标程序,以便控制quoting - except cmd.exe-style %...%-style 环境变量引用 仍然 由 PowerShell[1].

扩展

然而,--% 有许多严重的局限性 - 下文讨论 - 它的用处仅限于 Windows,因此它应该被视为 最后度假村;

一个备选方案 是使用 PSv5+ 中的 PSv3+ Native module (install with Install-Module Native from the PowerShell Gallery),它提供了两个内部命令 补偿所有PowerShell 的参数传递和 cmd.exe 的参数解析怪癖 :

  • 函数 ie,您可以将其添加到对外部程序的任何调用之前,包括 cmd.exe,仅允许您使用 PowerShell的syntax_,不用担心参数传递问题;例如:

    # Without `ie`, this command would malfunction.
    'a"b' | ie findstr 'a"b'
    
  • 函数 ins (Invoke-NativeShell) 函数允许您按原样重用为 cmd.exe 编写的命令行 ,作为 单个字符串 传递;与 --% 不同,这还允许您通过 PowerShell expandable string ("...") 在命令行中嵌入 PowerShell 变量和表达式:

    # 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`""
    

--%

的局限和陷阱
    要调用 (它不能是命令行上的 first 标记),因此实用程序可执行文件(路径)本身,如果由 [environment] 变量传递 ,必须使用 PowerShell 语法定义和引用。

  • --% 仅支持 一个 命令,其中不带引号的 |||&& 在同一行上,如果存在,则隐式结束 ;允许您将这样的命令通过管道/链接到其他命令/与其他命令一起使用。

    • 但是,支持使用;无条件地在同一行放置另一个命令; ; 逐字传递。

    • --% 读取(最多)到行的 末尾 所以 使用 line 将命令分散到多行- 延续字符。不支持。[2]

  • 除了%...%环境-变量引用,您不能在命令中嵌入任何其他动态元素;也就是说,您不能使用常规的 PowerShell 变量引用或表达式

    • 转义 % 个字符为 %%(您可以在批处理文件中执行的方式) 支持; %<name>% 令牌 总是 扩展,如果 <name> 引用定义的环境变量(如果不是,则令牌按原样传递)。
  • 除了%...%环境-变量引用,您不能在命令中嵌入任何其他动态元素;也就是说,您不能嵌入常规的 PowerShell 变量引用或表达式

  • 不能使用流重定向(例如,>file.txt),因为它们被逐字传递,作为参数 到目标命令。

    • 对于 stdout 输出,您可以通过附加 | Set-Content file.txt 来解决这个问题,但是 stderr[=200= 没有直接的 PowerShell 解决方法]输出。
    • 但是,如果您通过 cmd 调用您的命令 ,您可以让 cmd 处理 (stderr) 重定向(例如,cmd --% /c nosuch 2>file.txt)

适用于您的情况,这意味着:

  • %winscp% 必须翻译成它的 PowerShell 等价物,$env:winscp,后者必须以 & 为前缀,PowerShell 的调用运算符,在调用外部命令时需要由变量或带引号的字符串指定。
  • & $env:winscp 必须跟在 --% 之后,以确保所有剩余的参数都未经修改地传递(除了扩展 %...% 变量引用)。
  • 原始命令的参数列表可以原样粘贴在--%之后,但必须在一行.

因此,在您的情况下最简单的方法 - 尽管以必须使用单行为代价 - 是:

# 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] 请注意,尽管语法类似于 cmd.exe--% 也适用于 Unix 类似 PowerShell Core(macOS,Linux)中的平台,但在那里的使用非常有限:与原生 shell 不同,例如 bash--% 仅适用于 double-quoted strings ("...");例如,bash --% -c "hello world" 有效,但 bash --% -c 'hello world' 无效 - 通常的 shell 扩展,特别是 globbing,不受支持 - 参见 this GitHub issue.

[2] 即使是 PowerShell 自己的行继续符 `,也被视为直通 文字 。当你使用 --% 时甚至不涉及 cmd.exe(除非你明确使用 cmd --% /c ...),因此它的行继续符 ^ 也不能使用。

Martin Prikryl(WinSCP 的作者)也提供了一个.Net assembly,如果你想切换到 PowerShell,这可能是一个更好的选择。

Example 来自使用命令行参数更新的文档:

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
}