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

4

我有以下的 .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脚本时,我会得到“主机密钥与配置的密钥ssh-rsa不匹配”的错误提示。
我怀疑在powershell中我没有正确格式化命令,因此主机密钥被winscp看到时已经损坏了。
我检查了日志,只显示了来自主机的主机密钥。它没有显示我正在使用的密钥。通过更改我的主机并注意到它没有出现在日志中,我确认了这一点。我比较了.bat和.ps1之间的日志,差别在于ps1以上述错误终止。
winscp是一个sftp实用程序。

1
日志是否显示了指定的密钥(它认为)是什么?您可以使用类似于“进程监视器”这样的工具来查看进程启动时的确切命令行。如果进程不会太快退出,您可以使用PowerShell本身查询此信息(Get-WmiObject -Class Win32_Process -Filter 'Name = ''WinSCP.exe''' -Property CommandLine)。 - Lance U. Matthews
4个回答

7
注意:
  • JamesQMurphy的有用答案在这种情况下是最好的解决方案。

  • 此答案通常讨论将为cmd.exe编写的命令行翻译为PowerShell。


cmd.exe(批处理文件)命令行翻译成 PowerShell 是棘手的 - 如此棘手,以至于 在 PSv3 中引入了伪参数--%,即停止解析标记

它的作用是允许您将 --% 后面的所有内容 原样传递 给目标程序,以便控制确切的 引号 - 但是,PowerShell[1]仍会通过 cmd.exe 风格的 %...% 环境变量引用扩展

然而,--% 有许多严重的限制 - 下面讨论 - 它的实用性仅限于 Windows,因此应视为最后的手段

另一种选择是使用PSv3+ 本地 模块(在 PSv5+ 中从 PowerShell Gallery 安装 Install-Module Native),它提供了两个命令,可以内部补偿所有 PowerShell 的参数传递和 cmd.exe 的参数解析怪异性

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

--%的限制和陷阱

注意:如果您将--%splatting结合使用,许多这些限制都可以被克服,但这需要更多的工作,并需要将参数公式化为引用字符串 - 请参见this answer

  • --% 必须跟随外部工具的名称/路径以调用(它不能是命令行上的第一个标记),因此,如果通过[环境]变量传递,则必须使用PowerShell语法定义和引用实用程序可执行文件(路径)。

  • --% 仅支持一个命令,即同一行上未引用的|||&&,如果存在,则隐式结束;这允许您将这样的命令与其他命令进行管道传输/链接。

    • 但是,使用;无条件地在同一行上放置另一个命令是不受支持的;;会按原样传递。

    • --%最多读取到行尾,因此不支持使用换行符将命令分布在多个行上[2]

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

    • 转义%字符为%%(您可以在批处理文件内部执行此操作)是不受支持的;如果<name>引用了已定义的环境变量,则%<name>%令牌无论如何都会被扩展(否则,该令牌将按原样传递)。
  • 除了%...%环境-变量引用之外,您不能嵌入任何其他动态元素在命令中;也就是说,您不能嵌入常规的PowerShell变量引用或表达式

  • 您不能使用流重定向(例如>file.txt),因为它们作为参数按原样传递给目标命令。

    • 对于stdout输出,您可以通过附加| Set-Content file.txt来解决这个问题,但是没有直接的PowerShell解决方案可以解决stderr输出。
    • 但是,如果您通过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"

注意,尽管使用类似于cmd.exe的语法,--%在PowerShell Core(macOS、Linux)中也适用于类Unix平台,但其非常有限:与本机shell(如bash)不同,在那里,--%仅适用于双引号字符串("...");例如,bash --% -c "hello world"可以工作,但bash --% -c 'hello world'则无法工作,并且通常的shell扩展,特别是globbing,不被支持,请参见this GitHub issue

[2] 即使是 PowerShell 自身的行继续字符 `,也被视为一个透传的字面量。当使用 --% 时,甚至不涉及到 cmd.exe(除非显式使用 cmd --% /c ...),因此它的行继续字符 ^ 也不能使用。


1
如果您已经使用环境变量,我认为cmd /c 'winscp /ini=nul ... "%outfile%" ...'更易读,因为它不需要转义嵌套的双引号。此外:也许有趣的内容。 - Ansgar Wiechers
@AnsgarWiechers 我明白你的意思,但我的重点是保留_原始_命令行,而不必担心它的语法。如果您将命令行括在'...'中,则必须担心原始命令行包含'实例的情况。 - mklement0
1
这很有趣,但我没有给你答案的信用,因为我想改变使用PowerShell的方式。对于其他人来说,这将非常有用。 - Be Kind To New Users
@MichaelPotter:感谢您的反馈。确实,我的答案侧重于让您尽可能地使用cmd.exe命令行“原样”,但我赞赏您尝试学习PowerShell的工作原理,这将在长期受益。如果您想了解PowerShell如何解析命令行上的“未引用”标记,请参见我的此答案 - mklement0

4
马丁·普里克里尔(WinSCP的作者)还提供了一个.Net程序集,如果您想切换到PowerShell,则可能是更好的选择。
文档中的示例已更新为使用您命令行中的参数。
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
}

我本想使用这个,但是安装指南对我来说太复杂了。 - Be Kind To New Users
将DLL和可执行文件放入模块目录中,并使用完整路径加载DLL应该就足够了。 - Ansgar Wiechers
这是我感到困惑的一个例子:我不知道什么是“DLL”。我也不知道“我的模块目录”在哪里。当我点击一个看起来像是会给我逐步说明的链接时,它开始谈论dot net。对于这个项目,我不关心dot net,所以我只是继续使用我熟悉的东西。可能再过几周使用powershell后,这就会有意义了。 - Be Kind To New Users
我在谈论的是从下载页面上找到的.Net程序集归档中的DLL(和可执行文件)。模块目录在这里有解释。 - Ansgar Wiechers
1
@MichaelPotter 只需将 WinSCP-X.X.X-Automation.zip 包与您的脚本一起解压缩即可。 - Martin Prikryl

3
每当我需要从PowerShell调用可执行文件时,我总是将参数作为列表传递,如下所示。我也使用单引号,除非我实际上要替换变量,在这种情况下,我必须使用双引号。
 & SomeUtility.exe @('param1','param2',"with$variable")

当参数中有空格时,情况就变得有点棘手了,因为你可能需要提供引号以使实用程序正确解析命令。
你的示例更加棘手,因为 WinScp 希望 /command 参数的参数要用引号括起来,并且任何字符串都要用 双重 引号括起来。由于 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来确认这一点。) 还要记住,如果您需要更改日志文件路径为带空格的内容,则需要在路径周围加上引号。


4
赞赏您的系统性方法,但是不必将参数作为 数组 传递:& SomeUtility.exe 'param1' 'param2' "with$variable" 就足够了 - 最终,参数 仍然 被作为 以空格分隔的 列表进行传递。从概念上讲 - 虽然在这里没有技术差异 - 传递 $params 中存储的数组的等效方式是:& $winscp @params(注意 _splatting 运算符_)。 - mklement0

1
我运行了您的代码(当然是经过修改以适应我的服务器),并得到了以下日志记录:
. 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""' `

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接