首页 > 解决方案 > 将输出存储在中间变量中时保持一次一个管道处理

问题描述

作为说明,这里有一个输出十个整数的函数。

function Foo { for($i = 0; $i -lt 10; $i++) { Write-Host Inner $i; $i; }}

当我们Foo如下调用时,Foo仅迭代五次。这就是我们想要的。

Foo | Select-Object -First 5

当我们这样调用时FooFoo会迭代所有十次。这就是我们想要避免的。

$foo = Foo; $foo | Select-Object -First 5;

有时中间变量对可读性很有用。

如果有的话,当我们使用中间变量时,我们如何维护 PowerShell 的一次一个处理?


根据要求,这里详细说明了为什么我们可能希望在现实世界中这样做。它仍然令人费解,但它传达了这个想法。public struct以下输出受版本控制的 C# 文件中类型的名字。

Get-ChildItem -Directory -Recurse | 
 Where-Object { Test-Path (Join-Path $_.FullName ".git") } | 
  Select-Object -ExpandProperty FullName | 
   Get-ChildItem -File -Recurse -Filter *.cs | 
    Get-Content | 
     Select-String "public struct" |
      Select-Object -First 1;

这是一个很好的查询,但可以说一两个解释变量会很有用。

$gitRepositories = Get-ChildItem -Directory -Recurse | 
 Where-Object { Test-Path (Join-Path $_.FullName ".git") };

$csharpFiles = $gitRepositories | 
 Select-Object -ExpandProperty FullName | 
  Get-ChildItem -File -Recurse -Filter *.cs 

$structNames = $csharpFiles | 
 Get-Content | 
  Select-String "public struct" |
   Select-Object -First 1;

使用 包裹时Measure-Command,第一个查询需要 8.5 秒,第二个查询需要几分钟。

标签: powershell

解决方案


如果你让你的函数成为一个高级函数,通过[CmdletBinding()],像这样:

function Foo { 
    [CmdletBinding()]
    param()

    for($i = 0; $i -lt 10; $i++) { 
        Write-Host Inner $i; $i; 
    }
}

然后您可以访问自动参数-OutVariable,现在您可以这样做:

Foo -OutVariable foo | Select-Object -First 5

$foo

当然,在您的示例中,您可以轻松地执行以下操作:

$foo = Foo | Select-Object -First 5

所以我不确定你到底想要什么,但我确实怀疑这-OutVariable更符合要求,因为它随着管道的进行而填充。

例如:

function Foo { 
    [CmdletBinding()]
    param()

    for($i = 0; $i -lt 10; $i++) { 
        Write-Host Inner $i; $i;
        sleep -s 2 
    }
}

(现在函数在每次迭代时休眠 2 秒)

如果你然后CTRLC在它的中间,$foo将包含到目前为止所产生的内容。


有了进一步的评论,我想我明白了你现在所追求的:函数的返回值,以后可以枚举。这是一个枚举器。

从您的函数返回一个有两个复杂性:

  1. PowerShell 倾向于在返回时自动“展开”(处理)这些,但您可能可以通过返回单个元素数组的经典技巧来解决这个问题,其中元素是枚举数。
  2. 在不预处理所有项目的情况下创建枚举器将取决于它的细节。

要创建枚举器,您需要创建一个继承自IEnumerator. 你可以让这个类做你想做的任何事情,这就是实现的复杂性所在。

然后,您的函数实际上将仅用作实例化和返回此类事物的 PowerShell 接口。

class MyEnumerator : System.Collections.IEnumerator
{
    hidden [int] $index;
    hidden [int] $max;
    hidden [int] $start;
    hidden [int] $item;

    MyEnumerator([int]$max, [int]$start=0) {
        $this.index = -1
        $this.start = $start
        $this.max = $max
    }
    [bool] MoveNext() {
        ++$this.index
        $this.item = $this.index + $this.start

        # demonstration
        if ($this.item -gt 7) {
            throw
        }

        return ($this.item -le $this.max) 
    }

    [void] Reset() {
        $this.index = -1
    }

    [object] get_Current() {
        return $this.item
    }
}

function Foo ($start, $max) {
    ,[MyEnumerator]::new($max, $start)
}

所以在这个例子中,我创建了这个枚举器类,你可以给它任何开始和最大整数值,让它枚举。但是我抛出了一个小问题,如果项目值大于 7,它会被硬编码为抛出异常。

该函数创建枚举器并返回它(注意用于制作单个元素数组的一元逗号)。

所以你可以这样运行:

$foo = Foo 2 10

# no exception!

$foo | Select -First 5

# no exception!

另一方面:

$foo = Foo 2 10
$foo
# will throw

(你试图隐含地列举整个事情)

您需要注意的一件事$foo是枚举器对象,并保持状态,如果您尝试“重用”它,现在您需要对此负责。

例子:

$foo = Foo 2 10
$foo | Select -First 5

$foo.Current
# 6

$foo | Select -First 5
# 7
# exception!

您可以使用$foo.Reset()它使其回到开头。


另一种选择:从函数返回一个函数(或只是一个脚本块)。

这不会像枚举器那样自然,但它会更容易实现。

function Foo {
    {
        for($i = 0; $i -lt 10; $i++) { Write-Host Inner $i; $i; }
    }
}

$foo = Foo ; & $foo | Select-Object -First 5

为此,由于返回值是一个脚本块,因此您必须在管道之前执行它。

我想你真正要找的是枚举器,但你可以看到这需要一些更重的工作才能实现。


好的,现在从您的编辑中看到更真实的示例:

Get-ChildItem -Directory -Recurse | 
 Where-Object { Test-Path (Join-Path $_.FullName ".git") } | 
  Select-Object -ExpandProperty FullName | 
   Get-ChildItem -File -Recurse -Filter *.cs | 
    Get-Content | 
     Select-String "public struct" |
      Select-Object -First 1;

对比

$gitRepositories = Get-ChildItem -Directory -Recurse | 
 Where-Object { Test-Path (Join-Path $_.FullName ".git") };

$csharpFiles = $gitRepositories | 
 Select-Object -ExpandProperty FullName | 
  Get-ChildItem -File -Recurse -Filter *.cs 

$structNames = $csharpFiles | 
 Get-Content | 
  Select-String "public struct" |
   Select-Object -First 1;

我实际上仍然没有在这里看到中间变量的意义。你是在和这个工具作斗争。我真的不认为它们会增加可读性,而且正如您已经看到的那样,它们会损害性能。

也就是说,对于查找文件和目录,您几乎可以完全按照您的意愿行事。

这是第一部分的替换,$gitRepositories

$initialPath = Get-Location
$top = [System.IO.DirectoryInfo]::new($initialPath)

$gitRepositories = $top.EnumerateDirectories('.git', [System.IO.SearchOption]::AllDirectories)

在这个例子中,$gitRepositories将是一个可枚举的,所以它不会枚举目录,直到被强制。您可以将它与它一起使用,foreach或者只是尝试显示它,那时它将穿过树。PowerShell 中的那些东西已经知道如何处理枚举;也就是说,它们.GetEnumerator()自动调用并使用生成的枚举器。

你也可以自己做,$e = $gitRepositories.GetEnumerator()然后调用$e.MoveNext()and$e.Current等。

但是这将开始崩溃的部分是你的第二个:

$csharpFiles = $gitRepositories | 
 Select-Object -ExpandProperty FullName | 
  Get-ChildItem -File -Recurse -Filter *.cs 

这里的问题是您需要完全枚举第一个,以便获得下一个可枚举,所以一旦您想要这个中间的,您将完全失去第一个的“懒惰”方面。

另一个问题是不只有一个可以调用的目录.EnumerateFiles()$gitRepositories可以包含许多目录,并且您需要枚举每个目录的文件,因此您甚至不能“接受枚举”$gitRepositories并最终得到一个可枚举的结果......至少在没有创建某种可枚举的情况下不会您自己的类,它接受可枚举列表,然后按顺序枚举它们,提供“虚拟”单个可枚举。

虽如此,虽然这种方法不能准确地为您提供您正在寻找的东西,但总的来说,[DirectoryInfo]对象的.Enumerate*方法非常有用,并且非常高效,并且结果[DirecotryInfo]和/或[FileInfo]对象也充满了有用的属性和方法,提供从 PowerShell 原生 cmdlet 返回的对象的几乎所有功能,因此您应该考虑使用这些功能!


我为您提供的最后一个选项有点疯狂,它不会使您的任何代码更易于阅读或理解,这是肯定的。

与您的初始示例函数不同,您在更真实的示例中尝试运行的命令是支持开始/处理/结束的真正 cmdlet。“进程”部分(对应Process {}于高级函数中的块)确实充当管道中的枚举器。

实际上,您可以创建一些对象,让您通过对运行这些部分的细粒度控制来调用命令。这就是“代理功能”的工作方式,这也是隐式远程处理的工作方式。

您可以让 PowerShell 为您生成这样的代理命令,使用[System.Management.Automation.ProxyCommand]::Create().

$gci = Get-Command Get-ChildItem
[System.Management.Automation.ProxyCommand]::Create($gci)

结果将是一大堆东西:

[CmdletBinding(DefaultParameterSetName='Items', SupportsTransactions=$true, HelpUri='https://go.microsoft.com/fwlink/?LinkID=113308')]
param(
    [Parameter(ParameterSetName='Items', Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
    [string[]]
    ${Path},

    [Parameter(ParameterSetName='LiteralItems', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
    [Alias('PSPath')]
    [string[]]
    ${LiteralPath},

    [Parameter(Position=1)]
    [string]
    ${Filter},

    [string[]]
    ${Include},

    [string[]]
    ${Exclude},

    [Alias('s')]
    [switch]
    ${Recurse},

    [uint32]
    ${Depth},

    [switch]
    ${Force},

    [switch]
    ${Name})


dynamicparam
{
    try {
        $targetCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Get-ChildItem', [System.Management.Automation.CommandTypes]::Cmdlet, $PSBoundParameters)
        $dynamicParams = @($targetCmd.Parameters.GetEnumerator() | Microsoft.PowerShell.Core\Where-Object { $_.Value.IsDynamic })
        if ($dynamicParams.Length -gt 0)
        {
            $paramDictionary = [Management.Automation.RuntimeDefinedParameterDictionary]::new()
            foreach ($param in $dynamicParams)
            {
                $param = $param.Value

                if(-not $MyInvocation.MyCommand.Parameters.ContainsKey($param.Name))
                {
                    $dynParam = [Management.Automation.RuntimeDefinedParameter]::new($param.Name, $param.ParameterType, $param.Attributes)
                    $paramDictionary.Add($param.Name, $dynParam)
                }
            }
            return $paramDictionary
        }
    } catch {
        throw
    }
}

begin
{
    try {
        $outBuffer = $null
        if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
        {
            $PSBoundParameters['OutBuffer'] = 1
        }
        $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Get-ChildItem', [System.Management.Automation.CommandTypes]::Cmdlet)
        $scriptCmd = {& $wrappedCmd @PSBoundParameters }
        $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
        $steppablePipeline.Begin($PSCmdlet)
    } catch {
        throw
    }
}

process
{
    try {
        $steppablePipeline.Process($_)
    } catch {
        throw
    }
}

end
{
    try {
        $steppablePipeline.End()
    } catch {
        throw
    }
}
<#

.ForwardHelpTargetName Microsoft.PowerShell.Management\Get-ChildItem
.ForwardHelpCategory Cmdlet

#>

对你来说重要的是围绕命令创建一个“可步进的管道”,这会让你.Process()自己打电话;即调用流程管道的各个迭代。

从理论上讲,您可以将某种包装函数、专门的可枚举类等拼凑在一起,这可能会产生单个“变量”,让您可以通过管道懒惰地枚举。

我认为这更像是一种思想练习,而不是实际的东西。


推荐阅读