powershell - 将输出存储在中间变量中时保持一次一个管道处理
问题描述
作为说明,这里有一个输出十个整数的函数。
function Foo { for($i = 0; $i -lt 10; $i++) { Write-Host Inner $i; $i; }}
当我们Foo
如下调用时,Foo
仅迭代五次。这就是我们想要的。
Foo | Select-Object -First 5
当我们这样调用时Foo
,Foo
会迭代所有十次。这就是我们想要避免的。
$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 秒,第二个查询需要几分钟。
解决方案
如果你让你的函数成为一个高级函数,通过[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
将包含到目前为止所产生的内容。
有了进一步的评论,我想我明白了你现在所追求的:函数的返回值,以后可以枚举。这是一个枚举器。
从您的函数返回一个有两个复杂性:
- PowerShell 倾向于在返回时自动“展开”(处理)这些,但您可能可以通过返回单个元素数组的经典技巧来解决这个问题,其中元素是枚举数。
- 在不预处理所有项目的情况下创建枚举器将取决于它的细节。
要创建枚举器,您需要创建一个继承自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()
自己打电话;即调用流程管道的各个迭代。
从理论上讲,您可以将某种包装函数、专门的可枚举类等拼凑在一起,这可能会产生单个“变量”,让您可以通过管道懒惰地枚举。
我认为这更像是一种思想练习,而不是实际的东西。
推荐阅读
- sql - 组内的 SQL Server 查询 case 语句
- python - 转换数据 | 熊猫
- c - C - if 中的睡眠功能
- python - 如何在第一次出现值后屏蔽/拆分 Tensorflow 数组
- android - 如何实现 Firebase 查询以查找 firebase 数据库子项的最大值?
- prolog - 合并两个没有重复的列表序言
- javascript - css:检查兄弟姐妹是否集中
- python - 使用 matplotlib 在折线图上添加标记的问题
- php - Yii2 Throwing UserException 包括最终用户的堆栈跟踪
- sql - SQL - 显示具有相同 ID 的多行