begin/process/end 如何省去 foreach 的需要?仍然需要参数不是吗?

abe*_*gus 8 powershell

我了解到,对于管道中的每个对象,通过开始/处理/结束,处理部分运行多次。所以如果我有一个这样的函数:

function Test-BeginProcessEnd {
    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$True)]
        [string]$myName     
    )
    begin {}
    process {
        Write-Host $myName
    }
    end {}
}
Run Code Online (Sandbox Code Playgroud)

我可以通过管道将数组传递给它,如下所示,它会处理每个对象:

PS C:\> @('aaa','bbb') | Test-BeginProcessEnd
aaa
bbb
PS C:\>
Run Code Online (Sandbox Code Playgroud)

但是如果我尝试在命令行中使用该参数,我只能向它传递 1 个字符串,所以我可以这样做:

PS C:\> Test-BeginProcessEnd -myName 'aaa'
aaa
PS C:\>
Run Code Online (Sandbox Code Playgroud)

但我不能这样做:

PS C:\> Test-BeginProcessEnd -myName @('aaa','bbb')
Test-BeginProcessEnd : Cannot process argument transformation on parameter 'myName'. Cannot convert value to type
System.String.
At line:1 char:30
+ Test-BeginProcessEnd -myName @('aaa','bbb')
+                              ~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidData: (:) [Test-BeginProcessEnd], ParameterBindingArgumentTransformationException
    + FullyQualifiedErrorId : ParameterArgumentTransformationError,Test-BeginProcessEnd
PS C:\>
Run Code Online (Sandbox Code Playgroud)

显然我希望参数使用与通过管道相同,所以我必须将函数更改为:

function Test-BeginProcessEnd
{
    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$True)]
        [string[]]$myNames      
    )
    begin {}
    process {
        foreach ($name in $myNames) {
            Write-Host $name
        }
    }
    end {}
}
Run Code Online (Sandbox Code Playgroud)

所以我无论如何都必须使用 foreach,而 Process 部分的循环功能对我没有帮助。

我错过了什么吗?我看不出它有什么好处!谢谢你的帮助。

mkl*_*nt0 9

长话短说:博士

由于在 PowerShell 中将管道输入绑定到参数的工作方式(见下文),定义一个接受管道输入以及直接数组参数值传递的参数

  • 确实需要在内循环process
  • 总是将通过管道接收到的各个输入对象包装在单元素数组 every 中,这是低效的。

将管道绑定参数定义为标量可以避免这种尴尬,但是传递多个输入将仅限于管道- 您将无法将数组作为参数参数传递。[1]

这种不对称或许令人惊讶。


当您定义接受管道输入的参数时,您可以免费获得隐式数组逻辑:

  • 使用管道输入,PowerShell为每个输入对象调用一次process块,并将当前输入对象绑定到参数变量。

  • 相比之下,将输入作为参数值传递只会进入一次process 输入作为一个整体绑定到参数变量。

无论您的参数是否为数组值,上述内容都适用:每个管道输入对象都单独绑定/强制到与声明的参数类型完全相同的类型。


用声明参数的示例函数来具体说明这一点[Parameter(Mandatory=$true, ValueFromPipeline=$True)] [string[]] $myNames

让我们假设一个输入数组(集合)'foo', 'bar'(请注意,@()周围数组文字通常是不必要的)。

  • 参数值输入,Test-BeginProcessEnd -myNames 'foo', 'bar'

    • process块被调用一次
    • 输入数组'foo', 'bar'绑定为$myNames 一个整体
  • 管道输入,'foo', 'bar' | Test-BeginProcessEnd

    • process块被调用两次
    • with'foo''bar' every被强制为[string[]]- 即,一个元素数组。

要查看它的实际效果:

function Test-BeginProcessEnd
{
    [cmdletbinding()]
    Param(
      [Parameter(Mandatory, ValueFromPipeline)]
      [string[]]$myNames      
    )
    begin {}
    process {
      Write-Verbose -Verbose "in process block: `$myNames element count: $($myNames.Count)"
      foreach ($name in $myNames) { $name }
    }
    end {}
}
Run Code Online (Sandbox Code Playgroud)
# Input via parameter
>  Test-BeginProcessEnd 'foo', 'bar'
VERBOSE: in process block: $myNames element count: 2
foo
bar

# Input via pipeline
> 'foo', 'bar' | Test-BeginProcessEnd
VERBOSE: in process block: $myNames element count: 1
foo
VERBOSE: in process block: $myNames element count: 1
bar
Run Code Online (Sandbox Code Playgroud)

可选阅读:有关函数和管道输入的各种技巧

  • begin, process,end块可以在函数中使用,无论它是否是高级函数(类似于 cmdlet - 见下文)。

    • 如果只需要管道中的第1个或一定数量的对象,目前没有办法提前退出管道;相反,您必须设置一个布尔标志来告诉您何时忽略后续process块调用。
    • 但是,您可以使用中间的单独调用,例如| Select-Object -First 1,它在收到所需数量的对象后有效地退出管道。
    • 当前无法从用户代码中执行相同操作是GitHub 上此建议的主题。
    • 或者,您可以放弃一个块并在函数内部process使用,但是,如上所述,这将首先收集内存中的所有输入;在我的这个答案$Input | Select-Object 1中可以找到另一个同样不完美的替代方案。
  • 如果您不使用这些块,您仍然可以选择通过自动$Input变量访问管道输入;但请注意,您的函数会在所有管道输入都收集到内存中后运行(而不是像块那样逐个对象process)。

  • 不过,一般来说,使用process块是值得的:

    • 对象可以被一一处理,因为它们是由 source 命令生成的,这有两个好处:
      • 它使处理更加节省内存,因为不必首先完全收集源命令的输出。
      • 您的函数立即开始产生输出,无需等待源命令首先完成。
    • 希望很快(见上文),一旦处理完所有感兴趣的对象,您就能够退出管道。
    • 更清晰的语法和结构:该process块是所有管道输入的隐式循环,您可以有选择地分别在beginend块中执行初始化和清理任务。
  • 然而,将函数转变为高级函数很容易,这在支持常见参数(例如 和-ErrorAction)以及-OutVariable检测无法识别的参数方面提供了好处:

    • 使用param()块来声明参数并使用属性装饰该块[CmdletBinding()],如上所示(此外,使用属性隐式装饰单个参数会使[Parameter()]函数成为高级函数,但为了清楚起见,最好显式使用)。[CmdletBinding()]

[1] 严格来说,您可以,但前提是您键入参数[object](或者根本不指定类型,这是相同的)。
但是,输入数组/集合随后将作为一个整体绑定到参数变量,并且该process块仍然只输入一次,您需要执行自己的枚举。
一些标准 cmdlet(例如 cmdlet)Export-Csv是以这种方式定义的,但它们不会枚举通过-InputObject 参数传递的集合,从而导致直接使用该参数实际上毫无用处 - 请参阅此 GitHub 问题