【问题标题】:Convert a batch-file command with complex arguments to PowerShell将具有复杂参数的批处理文件命令转换为 PowerShell
【发布时间】:2017-07-24 21:14:45
【问题描述】:

我在 .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 实用程序。

【问题讨论】:

  • 日志是否显示指定的键(它认为)?您可以使用 Process Monitor 之类的工具来查看启动进程的确切命令行。你可以使用 PowerShell 本身 (Get-WmiObject -Class Win32_Process -Filter 'Name = ''WinSCP.exe''' -Property CommandLine) 来查询这个,前提是进程不会太快退出。

标签: powershell syntax winscp


【解决方案1】:

注意:

  • JamesQMurphy's helpful answer 是这种情况下的最佳解决方案。

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


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

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

但是,--% 有许多严重的限制 - 将在下面讨论 - 并且它的用处仅限于 Windows,因此应将其视为最后的手段; p>

替代方法是使用 PSv3+ Native module(使用 PSv5+ 中 PowerShell Gallery 中的 Install-Module Native 安装),它提供了两个内部命令 补偿所有 PowerShell 的参数传递和 cmd.exe 的参数解析怪癖

  • 函数ie,您可以在对外部程序的任何调用(包括cmd.exe)之前添加它,它允许您仅使用PowerShell 的语法_,而不必担心参数传递问题;例如:

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

--% 的限制和陷阱

  • --% 必须遵循要调用的外部实用程序的名称/路径(它不能是命令行),因此必须使用 PowerShell 语法定义和引用实用程序可执行文件(路径)本身,如果通过 [环境] 变量

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

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

    • --% (最多)读取到行尾,因此使用行继续字符将命令传播到多行。不支持。[2]

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

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

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

    • 对于 stdout 输出,您可以通过附加 | Set-Content file.txt 来解决此问题,但对于 stderr 输出没有直接的 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 的语法,但 --% 也适用于 PowerShell Core中类似 Unix 的平台>(macOS、Linux),但在那里的使用非常有限:与本地 shell(例如 bash)不同,--% 仅适用于 double -引用的字符串 ("...");例如,bash --% -c "hello world" 有效,但 bash --% -c 'hello world' 无效 - 并且不支持通常的 shell 扩展,尤其是通配符 - 请参阅 this GitHub issue

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

【讨论】:

  • 如果您仍然使用环境变量,我发现cmd /c 'winscp /ini=nul ... "%outfile%" ...' 更具可读性,因为它不需要转义嵌套的双引号。另外:maybeinteresting.
  • @AnsgarWiechers 我明白你的意思,但我的重点是保留 original 命令行,根本不必担心它的语法。如果将命令行包含在'...' 中,则必须担心包含' 实例的原始命令行。
  • 这很有趣,但我没有把答案归功于你,因为我想切换 powershell 的做事方式。对于其他人来说,这将非常有用。
  • @MichaelPotter:感谢您的反馈。确实,我的回答侧重于允许您使用cmd.exe 命令行as-is(尽可能多地),但我赞扬您尝试了解 PowerShell 如何滴答作响,这将使您受益从长远来看。如果您想知道 PowerShell 如何在命令行上解析 unquoted 标记,请参阅我的 this answer
【解决方案2】:

每当我必须从 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 来确认这一点。)另外请记住,如果您需要更改日志文件路径,则需要在日志文件路径周围加上引号把它变成带空格的东西。

【讨论】:

  • 赞一个有条不紊的方法,但没有必要将参数作为 array 传递:& SomeUtility.exe 'param1' 'param2' "with$variable" 就可以了 - 最终,参数 是 无论如何都作为 空格分隔 列表传递。 概念上 - 尽管 here 在技术上没有区别 - 相当于传递存储在 $params 中的数组将是:& $winscp @params(注意 splatting operator)。
【解决方案3】:

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
}

【讨论】:

  • 我本来想用这个,但是安装说明让我很困惑。
  • 将 DLL 和可执行文件放在模块目录中并使用完整路径加载 DLL 就足够了。
  • 这是我为什么感到困惑的一个例子:我不知道“DLL”是什么。我也不知道“我的模块目录”在哪里。当我点击一个看起来会给我逐步说明的链接时,它开始谈论 dot net。我不关心这个项目的 dot net,所以我只是继续使用我熟悉的东西。可能在使用 powershell 的几周内,这将是有意义的。
  • 我说的是 .Net 程序集存档中的 DLL(和可执行文件),您可以在 download page 上找到它。模块目录解释here.
  • @MichaelPotter 只需将WinSCP-X.X.X-Automation.zip 包与您的脚本一起提取即可。
【解决方案4】:

我运行了您的代码(当然,已修改为与我自己的服务器一起使用)并得到以下日志:

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

【讨论】:

    猜你喜欢
    • 2015-02-18
    • 2012-01-10
    • 1970-01-01
    • 2019-11-15
    • 1970-01-01
    • 2020-09-10
    • 2012-07-23
    • 1970-01-01
    • 2018-02-06
    相关资源
    最近更新 更多