【问题标题】:Pipeline in PowershellPowershell 中的管道
【发布时间】:2021-08-19 06:40:01
【问题描述】:

我在about_Pipelines 阅读了有关管道如何在 PowerShell 中工作的信息,并了解到管道一次交付一个对象。

所以,这个

Get-Service | Format-Table -Property Name, DependentServices

和这个不一样

Format-Table -InputObject (Get-Service) -Property Name, DependentServices

所以在这里,按照解释,在第一种情况下,Format-Table 一次在一个对象上工作,而在第二个例子中,Format-Table 在一个对象数组上工作。如果我错了,请纠正我。

如果是这种情况,那么我想知道Sort-Object 和其他需要处理数据集合的 cmdlet 如何使用管道字符。

当我这样做时:

Get-Service | Sort-Object

如果Sort-Object 一次只能处理一个对象,它如何能够进行排序。因此,假设有 100 个服务对象要传递给Sort-ObjectSort-Object 会被调用 100 次(每个对象对应一个对象)吗?而且,这将如何产生我在屏幕上看到的排序结果。

【问题讨论】:

    标签: powershell pipeline sort-object


    【解决方案1】:

    Sort-Object(以及其他需要在输出任何内容之前评估 all 输入对象的 cmdlet)通过逐个收集输入对象来工作,然后直到上游 cmdlet( Get-Service 在这种情况下)已完成发送输入。

    这是如何工作的?好吧,让我们尝试使用 PowerShell 函数重新创建 Sort-Object

    为此,我们首先需要了解 cmdlet 由 3 个独立的例程组成:

    • Begin - 管道中每个 cmdlet 的 Begin 例程在发生其他任何事情之前调用一次
    • Process - 在每个 cmdlet 上调用此例程每次接收来自上游命令的输入
    • End - 一旦上游命令调用 End 并且没有更多输入项供 Process 处理,就会调用它

    (这些是 PowerShell function 定义中使用的块标签名称 - 在二进制 cmdlet 中,您将覆盖 cmdlet 的 BeginProcessingProcessRecordEndProcessing 方法的实现)

    所以,要“收集”每个输入项,我们需要在命令的Process 块中添加一些逻辑,然后我们可以将操作所有项的代码放在End 块中:

    function Sort-ObjectCustom
    {
      param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [object[]]$InputObject
      )
    
      begin {
        # Let's use the `begin` block to create a list that'll hold all the input items
        $list = [System.Collections.Generic.List[object]]::new()
    
        Write-Verbose "Begin was called"
      }
    
      process {
        # Here we simply collect all input to our list
        $list.AddRange($InputObject)
    
        Write-Verbose "Process was called [InputObject: $InputObject]"
      }
    
      end {
        # The `end` block is only ever called _after_ we've collected all input
        # Now we can safely sort it
        $list.Sort()
    
        Write-Verbose "End was called"
    
        # and output the results
        return $list
      }
    }
    

    如果我们用-Verbose 调用我们的新命令,我们将看到输入是如何被一一收集的:

    PS ~> 10..1 |Sort-ObjectCustom -Verbose
    VERBOSE: Begin was called
    VERBOSE: Process was called [InputObject: 10]
    VERBOSE: Process was called [InputObject: 9]
    VERBOSE: Process was called [InputObject: 8]
    VERBOSE: Process was called [InputObject: 7]
    VERBOSE: Process was called [InputObject: 6]
    VERBOSE: Process was called [InputObject: 5]
    VERBOSE: Process was called [InputObject: 4]
    VERBOSE: Process was called [InputObject: 3]
    VERBOSE: Process was called [InputObject: 2]
    VERBOSE: Process was called [InputObject: 1]
    VERBOSE: End was called
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    

    有关如何为二进制 cmdlet 实现管道输入处理例程的详细信息,请参阅"How to Override Input Processing"

    有关如何在函数中利用相同管道语义的更多信息,请参阅about_Functions_Advanced_Methods and related help topics

    【讨论】:

    • 谢谢。很好解释。这消除了许多疑虑。如果我可能会问,你能不能解释一下,写在管道字符左侧的任何东西是如何处理的?那么,当微软说管道一个一个地传递对象时,是否意味着Get-Service会在保持某种状态的同时被多次调用,并且它会一个一个地发出对象以供Sort-Object的进程捕获还是意味着Get -Service 将被调用一次,并且集合将保存在缓冲区中的某个位置,并且管道将确保一次传递一个对象?
    • 好吧,Get-ServiceEnd 块中完成大部分工作,一旦End 块返回,管道处理器(“PowerShell引擎”负责执行管道)将知道Get-Service 已“完成”。这有意义吗?
    • @pragun 重要的是要了解 3 个例程中的任何一个都可以输出 1 个对象、多个对象或根本不输出 - 完全取决于命令的作者。没有硬性规定所有输出都必须以 processend 发送,例如
    • 很抱歉回复晚了。是的,这有帮助。谢谢!
    【解决方案2】:

    为了补充来自Mathias 的答案,您实际上可以使用Write-Host cmdlet 从现有的cmdlet 可视化进程的顺序,该cmdlet 会立即将输出写入显示器(而不是管道):

    $Data = ConvertFrom-Csv @'
    Id, Name
     4, Four
     2, Two
     3, Three
     1, One
    '@
    

    Select-Object例子

    $Data |
        Foreach-Object { Write-Host 'in:' ($_ |ConvertTo-Json -Compress); $_ } |
        Select-Object * |
        Foreach-Object { Write-Host 'out:' ($_ |ConvertTo-Json -Compress); $_ }
    

    节目:

    in: {"Id":"4","Name":"Four"}
    out: {"Id":"4","Name":"Four"}
    
    in: {"Id":"2","Name":"Two"}
    out: {"Id":"2","Name":"Two"}
    in: {"Id":"3","Name":"Three"}
    out: {"Id":"3","Name":"Three"}
    in: {"Id":"1","Name":"One"}
    out: {"Id":"1","Name":"One"}
    Id Name
    -- ----
    4  Four
    2  Two
    3  Three
    1  One
    

    Sort-Object例子

    $Data |
        Foreach-Object { Write-Host 'in:' ($_ |ConvertTo-Json -Compress); $_ } |
        Sort-Object * |
        Foreach-Object { Write-Host 'out:' ($_ |ConvertTo-Json -Compress); $_ }
    

    节目:

    in: {"Id":"4","Name":"Four"}
    in: {"Id":"2","Name":"Two"}
    in: {"Id":"3","Name":"Three"}
    in: {"Id":"1","Name":"One"}
    out: {"Id":"1","Name":"One"}
    
    out: {"Id":"2","Name":"Two"}
    out: {"Id":"3","Name":"Three"}
    out: {"Id":"4","Name":"Four"}
    Id Name
    -- ----
    1  One
    2  Two
    3  Three
    4  Four
    

    一般来说,PowerShell cmdlet Write Single Records to the Pipeline 在可能的情况下(此鼓励指南的优点之一是它可以减少内存消耗)。正如您的问题所暗示的那样,Sort-Object 不能这样做,因为最后一条记录可能在第一条记录之前。但也有例外,在技术上可以根据鼓励的指南编写单个记录,但事实并非如此。参见例如:#11221 Select-Object -Unique is unnecessary slow and exhaustive

    【讨论】:

    • 太棒了。因此,cmdlet 的实现(在 Begin/Process/End 块中)取决于哪个 powershell 决定执行顺序。谢谢!!我一直在研究 powershell 能够做的一些事情,我只是被这个基于 .NET 对象的 Shell 的优点所震撼。具有 shell 脚本语言灵活性的强大 Oops。我怀疑是否有任何替代方案。 Bash 在它面前感觉太原始和幼稚。然而,人们却将“bash”称为天使,并破坏微软所做的一切。
    • Select-Object 示例令人困惑,因为表格输出直到稍后才出现的唯一原因与实际输出排序无关:这完全是由于臭名昭著的 300 毫秒。延迟 - 见stackoverflow.com/a/43691123/45375
    • 有没有办法告诉哪些 cmdlet 在处理之前收集输入?我正在尝试确定管道在哪里减速,以及我是否可以重写它以保留在管道中,或者只需要对所有内容进行大修。在我的脑海中,我可以想到Sort-Group-,但是是否有一个详尽的列表,列出哪些 cmdlet 预先收集或不收集输入以及在什么情况下? (例如Import-Csv,如果你愿意,你可以通过管道输出,所以它不是必需的)
    • @immobile2,afaik,没有哪些 cmdlet 阻塞管道的列表。通常,所有标准 PowerShell cmdlet 都是 implemented for the Middle of a Pipeline,除非它的(参数)定义不可能做到这一点(例如,Sort-Object 是最后一个对象可能成为第一个对象)。另请注意,使用括号或分配管道会阻塞管道,请参阅this answer
    猜你喜欢
    • 1970-01-01
    • 2021-06-28
    • 1970-01-01
    • 1970-01-01
    • 2016-05-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多