PowerShell 脚本块内的命令执行顺序

dav*_*ave 4 powershell scriptblock

起初我对 PowerShell ScriptBlock 很兴奋,但最近我对它在块内的执行顺序感到困惑。例如:

$test_block = {
  write-host "show 1"
  ps
  write-host "show 2"
  Get-Date
}
Run Code Online (Sandbox Code Playgroud)

调用 $test_block.Invoke() 的输出:

show 1
show 2
<result of command 'ps'>
<result of command 'get-date'>
Run Code Online (Sandbox Code Playgroud)

输出内容的命令会先运行吗?

Dav*_*tin 6

此行为是因为write-host不会将输出放到管道上。其他命令放置在管道上,因此在函数(调用)返回之前不会输出到屏幕。

为了获得我认为您期望的行为,请改用write-output,所有命令的结果将在管道中返回。

$test_block = {
  write-output "show 1"
  ps
  write-output "show 2"
  Get-Date
}

$test_block.Invoke()
Run Code Online (Sandbox Code Playgroud)


mkl*_*nt0 6

为了补充大卫·马丁的有用答案

  • 避免Write-Host(这通常是错误的工具使用)而支持Write-Output,即输出到成功输出流- 而不是使用[1]打印到显示器- 可以解决您眼前的问题。Write-Host

  • 但是,您可以通过使用&调用运算符而不是.Invoke()方法来避免该问题:

      # Invokes the script block and *streams* its output.
      & $test_block
    
    Run Code Online (Sandbox Code Playgroud)
    • 使用通常&是更好的选择,而不仅仅是为了避免.Invoke()“collect-all-success-output-stream-first”行为 - 请参阅下一节。

作为旁白:

  • 这个答案描述了具有类似症状的常见问题(成功输出(管道输出)相对于其他流出现无序),尽管它在技术上不相关,并且也发生在&

    # !! The Write-Host output prints FIRST, due to implicit, asynchronous
    # !! table formatting of the [pscustomobject] instance.
    & { [pscustomobject] @{ Foo = 'Bar' }; Write-Host 'Why do I print first?' }
    
    Run Code Online (Sandbox Code Playgroud)

为什么应该使用&而非来调用脚本块 ( ):.Invoke(){ ... }

脚本块( { ... }) 通常&在参数(解析)模式(如 cmdlet 和外部程序)中使用调用运算符进行调用,而不是通过其方法来调用,这允许使用更熟悉的语法;例如:.Invoke()

  • & $test_block而不是$test_block.Invoke()
  • 带参数:& $test_block arg1 ...而不是$test_block.Invoke(arg1, ...)

也许更重要的是,使用这种基于运算符的调用语法(& { ... } .... { ... } ...)具有以下优点

  • 保留正常的语义,这意味着成功输出(也)是在生成脚本块时从脚本块发出的,而在一个实例中,.Invoke() 首先收集所有成功输出,然后返回 - 相比之下,输出直接发送到两种情况下均显示。[System.Collections.ObjectModel.Collection[psobject]]Write-Host

    • 作为一个有益的副作用,您的特定输出排序问题消失了,但请注意,在PSv5+ 中,通常仍然存在输出排序问题,尽管其原因无关没有预定义格式数据的输出类型的隐式表格输出(隐式Format-Table)是异步的,以确定合适的列宽度(您的特定代码恰好仅使用具有预定义格式数据的 cmdlet)。

    • 请参阅此答案以获取更多信息;一个简单的重现:

       [pscustomobject] @{ foo = 1 }; Write-Host 'Should print after, but prints first.'
      
      Run Code Online (Sandbox Code Playgroud)
  • 允许您传递命名参数(例如-Foo Bar),而.Invoke()仅支持位置(未命名)参数(例如Bar)。

  • 它保留了脚本终止错误的正常语义

    # OK: & throw aborts the entire script; 'after' never prints.
    & { throw 'fatal' }; 'after'
    
    # !! .Invoke() "eats" the script-terminating error and
    # !! effectively converts it to a *statement*-terminating one.
    # !! Therefore, execution continues, and 'after' prints.
    { throw 'fatal' }.Invoke(); 'after'
    
    Run Code Online (Sandbox Code Playgroud)
  • 此外,使用基于运算符的调用使您可以选择使用.,即点源运算符来代替&,以便直接在调用者的作用域中运行脚本块,而.Invoke()方法调用子作用域中运行(&就像):

    # Dot-sourcing the script block runs it in the caller's scope.      
    . { $foo='bar' }; $foo # -> 'bar'
    
    Run Code Online (Sandbox Code Playgroud)

的使用.Invoke()最好仅限于PowerShell SDK项目(通常基于 C#,其中不能使用 PowerShell 运算符)。


[1] 从技术上讲,由于 PowerShell v5Write-Host输出到信息流(流编号6),但是默认情况下会打印到显示器,同时在管道和重定向中被忽略。请参阅此答案Write-Host以了解和的并置Write-Output,以及为什么通常需要注意后者。