PowerShell:ForEach-Object的神秘-RemainingScripts参数

时间:2013-05-20 07:37:51

标签: powershell powershell-v3.0

简短问题:任何人都有关于ForEach-Object的-RemainingScripts参数的详细信息?

长问题:

我刚从上周开始学习PowerShell,我将浏览每个Cmdlet以了解更多详细信息。基于公共文档,我们知道ForEach-Object可以有Begin-Process-End块,如下所示:

Get-ChildItem | foreach -Begin { "block1";
    $fileCount = $directoryCount = 0} -Process { "block2";
    if ($_.PsIsContainer) {$directoryCount++} else {$fileCount++}} -End {
    "block3"; "$directoryCount directories and $fileCount files"}

预期结果:“block1”和“block3”为1次,对于传入的每个项目重复“block2”,并且dir计数/文件计数都是正确的。到目前为止一切都很好。

现在,有趣的是,以下命令也起作用并给出完全相同的结果:

Get-ChildItem | foreach { "block1"
    $fileCount = $directoryCount = 0}{ "block2";
    if ($_.PsIsContainer) {$directoryCount++} else {$fileCount++}}{
    "block3"; "$directoryCount directories and $fileCount files"}

只有3个ScriptBlocks传递给foreach。根据手册,第一个进入-Process(位置1)。但剩下的2怎么样?根据手册,没有“位置2”的参数。所以我转向Trace-Command,发现后两个脚本块实际上是RemainingScripts的“IList with 2 elements”。

BIND arg [$fileCount = $directoryCount = 0] to parameter [Process]
BIND arg [System.Management.Automation.ScriptBlock[]] to param [Process] SUCCESSFUL
BIND arg [System.Collections.ArrayList] to parameter [RemainingScripts]
BIND arg [System.Management.Automation.ScriptBlock[]] to param [RemainingScripts] SUCCESSFUL

所以如果我将命令更改为:

# No difference with/without the comma "," between the last 2 blocks
Get-ChildItem | foreach -Process { "block1"
    $fileCount = $directoryCount = 0} -RemainingScripts { "block2";
    if ($_.PsIsContainer) {$directoryCount++} else {$fileCount++}},{
    "block3"; "$directoryCount directories and $fileCount files"}

仍然是完全相同的结果。

正如您所注意到的,所有3个命令都给出了相同的结果。这提出了一个有趣的问题:后两个命令(隐式地)都指定了-Process,但 ForEach-Object令人惊讶地最终使用-Process的参数作为“-Begin”!(脚本块被执行)一旦开始)。

这个实验表明:

  1. -RemainingScripts参数将采用所有未绑定的ScriptBlocks
  2. 当传入3个块时,虽然第一个块进入-Process,但后来它实际上用作“Begin”,而剩下的2个变为“Process”和“End”
  3. 尽管如此,以上所有内容都只是我的猜测。我没有找到支持我猜测的文档

    所以,最后我们回到我的简短问题:) 任何人都有关于ForEach-Object的-RemainingScripts参数的详细信息?

    感谢。

5 个答案:

答案 0 :(得分:3)

我做了更多的研究,现在有信心在传递多个ScriptBlocks时回答-RemainingScripts参数的行为。

如果您运行以下命令并仔细检查结果,您将找到该模式。这不是很简单,但仍然不难理解。

1..5 | foreach { "process block" } { "remain block" }
1..5 | foreach { "remain block" }  -Process { "process block" }
1..5 | foreach { "remain block" } -End { "end block" } -Process { "process block" } -Begin { "begin block" }
1..5 | foreach { "remain block 1" } -End { "end block" } -Process { "process block" } { "remain block 2" }
1..5 | foreach { "remain block 1" } { "remain block 2" } -Process { "process block" } -Begin { "begin block" }
1..5 | foreach { "remain block 1" } { "remain block 2" } -Process { "process block" } { "remain block 3" }
1..5 | foreach { "process block" } { "remain block 1" } { "remain block 2" } -Begin { "begin block" }
1..5 | foreach { "process block" } { "remain block 1" } { "remain block 2" } { "remain block 3" }

那么这里的模式是什么?

  • 当传入单个ScriptBlock时:简单,它只是转到-Process(最常见的用法)

  • 当传入2个ScriptBlock时,有3种可能的组合

    1. - 过程& -Begin - >按指定执行
    2. - 过程& - 结束 - >按指定执行
    3. - 过程& -RemainingScripts - >流程变为Begin,而RemainingScripts变为Process

如果我们运行这两个陈述:

1..5 | foreach { "process block" } { "remain block" }
1..5 | foreach { "remain block" }  -Process { "process block" }

# Both of them will return:
process block
remain block
remain block
remain block
remain block
remain block

正如您将发现的,这只是以下测试用例的一个特例:

  • 当传入2个以上的ScriptBlock时,请遵循以下工作流程:

    1. 绑定指定的所有脚本块(Begin,Process,End);剩下的ScriptBlocks转到RemainingScripts
    2. 将所有脚本订购为:开始>处理>剩余的>端
    3. 排序结果是ScriptBlocks的集合。我们将此集合称为 OrderedScriptBlocks

      • 如果未绑定Begin / End,则忽略
    4. (内部)根据 OrderedScriptBlocks重新绑定参数

      • OrderedScriptBlocks [0]成为Begin
      • OrderedScriptBlocks [1 ..- 2]成为Process
      • OrderedScriptBlocks [-1](最后一个)变为End

我们来看这个例子

1..5 | foreach { "remain block 1" } { "remain block 2" } -Process { "process block" } { "remain block 3" }

订单结果是:

{ "process block" }    # new Begin
{ "remain block 1" }   # new Process
{ "remain block 2" }   # new Process
{ "remain block 3" }   # new End

现在执行结果是完全可预测的:

process block
remain block 1
remain block 2
remain block 1
remain block 2
remain block 1
remain block 2
remain block 1
remain block 2
remain block 1
remain block 2
remain block 3

这是-RemainingScripts背后的秘密,现在我们了解ForEach-Object的更多内部行为!

我仍然不得不承认没有文件支持我的猜测(不是我的错!),但这些测试用例应足以解释我所描述的行为。

答案 1 :(得分:1)

这是它的细节。 ValueFromRemainingArguments设置为true,因此您的猜测是正确的。

help ForEach-Object

-RemainingScripts <ScriptBlock[]>
    Takes all script blocks that are not taken by the Process parameter.

    This parameter is introduced in Windows PowerShell 3.0.

gcm ForEach-Object | select -exp parametersets 

Parameter Name: RemainingScripts
  ParameterType = System.Management.Automation.ScriptBlock[]
  Position = -2147483648
  IsMandatory = False
  IsDynamic = False
  HelpMessage =
  ValueFromPipeline = False
  ValueFromPipelineByPropertyName = False
  ValueFromRemainingArguments = True
  Aliases = {}
  Attributes =
    System.Management.Automation.ParameterAttribute
    System.Management.Automation.AllowEmptyCollectionAttribute
    System.Management.Automation.AllowNullAttribute

答案 2 :(得分:1)

还有更多支持@FangZhou排序假设的示例。

如果指定其他块,那么它的工作方式似乎很有意义:

PS C:\> 1..5 | ForEach-Object -Begin { "Begin: $_" } -End { "End: $_" } -Process { "Process: $_" } -RemainingScripts { "R1: $_" },{ "R2: $_" },{ "R3: $_" }
Begin: 
Process: 1
R1: 1
R2: 1
R3: 1
Process: 2
R1: 2
R2: 2
R3: 2
Process: 3
R1: 3
R2: 3
R3: 3
Process: 4
R1: 4
R2: 4
R3: 4
Process: 5
R1: 5
R2: 5
R3: 5
End: 

即使您传递了空块:

PS C:\> 1..5 | ForEach-Object -Begin {} -End {} -Process { "Process: $_" } -RemainingScripts { "R1: $_" },{ "R2: $_" },{ "R3: $_" }
Process: 1
R1: 1
R2: 1
R3: 1
Process: 2
R1: 2
R2: 2
R3: 2
Process: 3
R1: 3
R2: 3
R3: 3
Process: 4
R1: 4
R2: 4
R3: 4
Process: 5
R1: 5
R2: 5
R3: 5

但是,如果您未指定-End,它将执行完全不同的操作。传递给命令的最后一个脚本块用于-End

PS C:\> 1..5 | ForEach-Object -Begin { "Begin: $_" } -Process { "Process: $_" } -RemainingScripts { "R1: $_" },{ "R2: $_" },{ "R3: $_" }
Begin: 
Process: 1
R1: 1
R2: 1
Process: 2
R1: 2
R2: 2
Process: 3
R1: 3
R2: 3
Process: 4
R1: 4
R2: 4
Process: 5
R1: 5
R2: 5
R3: 

您可以通过更改属性的顺序来更改将发生的事情:

PS C:\> 1..5 | ForEach-Object  -RemainingScripts { "R1: $_" },{ "R2: $_" },{ "R3: $_" } -Begin { "Begin: $_" } -Process { "Process: $_" }
Begin: 
R1: 1
R2: 1
R3: 1
R1: 2
R2: 2
R3: 2
R1: 3
R2: 3
R3: 3
R1: 4
R2: 4
R3: 4
R1: 5
R2: 5
R3: 5
Process: 

如果您未指定-Begin,则再次有所不同。现在,第一个传递的脚本块用于-Begin

PS C:\> 1..5 | ForEach-Object -End { "End: $_" } -Process { "Process: $_" } -RemainingScripts { "R1: $_" },{ "R2: $_" },{ "R3: $_" }
Process: 
R1: 1
R2: 1
R3: 1
R1: 2
R2: 2
R3: 2
R1: 3
R2: 3
R3: 3
R1: 4
R2: 4
R3: 4
R1: 5
R2: 5
R3: 5
End: 

如果您既未指定-Begin也未指定-End,它将两者结合在一起。现在,第一个脚本块替换-Begin,最后一个脚本块替换-End

PS C:\> 1..5 | ForEach-Object -Process { "Process: $_" } -RemainingScripts { "R1: $_" },{ "R2: $_" },{ "R3: $_" }
Process: 
R1: 1
R2: 1
R1: 2
R2: 2
R1: 3
R2: 3
R1: 4
R2: 4
R1: 5
R2: 5
R3: 

据我所知,它旨在支持您要在其中编写的位置脚本块:

1..5 | ForEach-Object { "Begin: $_" } { "Process1: $_" } { "Process2: $_" } { "Process3: $_" } { "End: $_" }

或者像这样:

1..5 | ForEach-Object { "Begin: $_" },{ "Process1: $_" },{ "Process2: $_" },{ "Process3: $_" },{ "End: $_" }

两者的输出:

Begin: 
Process1: 1
Process2: 1
Process3: 1
Process1: 2
Process2: 2
Process3: 2
Process1: 3
Process2: 3
Process3: 3
Process1: 4
Process2: 4
Process3: 4
Process1: 5
Process2: 5
Process3: 5
End: 

答案 3 :(得分:1)

我相信-remainingscripts(具有属性'ValueFromRemainingArguments')是要从 Windows Powershell in Action 中启用这样的成语,这种成语几乎没有人知道(20%的Powershell仅记录在其中)本书):

Get-ChildItem | ForEach {$sum=0} {$sum++} {$sum}

这些块最终就像开始进程结束一样起作用。实际使用的参数是-process和-remainingscripts。

trace-command -name parameterbinding { Get-ChildItem | ForEach-Object {$sum=0} {$sum++} {$sum} } -PSHost

该跟踪命令似乎证实了这一点。

这是带有脚本块的ValueFromRemainingArguments的简单演示。

function remaindemo {
  param ($arg1,  [Parameter(ValueFromRemainingArguments)]$remain)
  & $arg1
  foreach ($i in $remain) {
    & $i
  }
}

remaindemo { 'hi' } { 'how are you' } { 'I am fine' }

具有ValueFromRemainingArguments参数的其他命令:

gcm -pv cmd | select -exp parametersets | select -exp parameters |
  where ValueFromRemainingArguments | 
  select @{n='Cmdname';e={$cmd.name}},name

Cmdname        Name
-------        ----
ForEach-Object RemainingScripts
ForEach-Object ArgumentList
Get-Command    ArgumentList
Get-Command    ArgumentList
Join-Path      AdditionalChildPath
New-Module     ArgumentList
New-Module     ArgumentList
Read-Host      Prompt
Trace-Command  ArgumentList
Write-Host     Object
Write-Output   InputObject

答案 4 :(得分:0)

源代码似乎同意。从powershell on github中,我可以找到ForEach-Object cmdlet的以下相关资源。

/* ... */注释是我的,否则我对代码进行了一些更改,因此请参考实际来源以获取适当的参考。

第一部分是最初读取参数的方式。要注意的重要一点是,所有<ScripBlock>都放入同一个数组中,除了-End-End参数是经过特殊处理的,可能是因为这样做更容易(例如,很容易确定列表中的第一项只是添加到列表的第一项,但是如果您这样做,则很容易)。确保某项是列表中的最后一项要求您知道自己不会在其后追加任何其他内容。

private List<ScriptBlock> _scripts = new List<ScriptBlock>();

public ScriptBlock Begin {
  /* insert -Begin arg to beginning of _scripts */
  set { _scripts.Insert(0, value); }
}

ScriptBlock[] Process {
  /* append -Process args to _scripts */ }
  set {
    if (value == null) { _scripts.Add(null); }
    else { _scripts.AddRange(value); }
  }
}

private ScriptBlock _endScript;
private bool _setEndScript;

ScriptBlock End {
  set {
    _endScript = value;
    _setEndScript = true;
  }
}

public ScriptBlock[] RemainingScripts {
  set {
    if (value == null) { _scripts.Add(null); }
    else { _scripts.AddRange(value); }
  }
}

以下是调用<ScriptBlock>数据成员中的_script的函数。基本上,如果<ScripBlock>中有多个_scripts,则_start设置为1。如果至少有三个,并且-End未被明确设置,{ {1}}设置为_end。然后调用_scripts[_scripts.Count - 1],为_script[0]打开流,然后调用_script[1.._end-1]_endScript_script[_end])。

-End

这不是完全令人满意的,因为如果文档中未明确说明,则可以将其视为实现细节,但是,它给了我足够的信心来假设这是意图。 Powershell in Action 中也讨论了这种模式,该模式似乎得到了Powershell团队的某种祝福(至少是Snover)。

我想其中的一部分将“开源”,而将更多的“ * nix-y”定义为文档的一部分:)