12

简短的问题:有人有关于 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”对每个传入的项目重复,目录计数/文件计数都正确。到目前为止,一切都很好。

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

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,发现后面的 2 个脚本块实际上是 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个block时,虽然第一个进入-Process,但后来它实际上被用作“Begin”,而剩下的2个成为“Process”和“End”

尽管如此,以上只是我的猜测。我没有找到支持我猜测的文档

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

谢谢。

4

5 回答 5

6

我做了更多的研究,现在有信心回答传入多个 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. -Process & -Begin -> 按指定执行
    2. -Process & -End -> 按指定执行
    3. -Process & -RemainingScripts -> Process 变为 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. 按指定绑定所有脚本块(开始、处理、结束);剩余的 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 的更多内部行为!

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

于 2013-05-20T18:31:05.650 回答
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
于 2013-05-20T08:12:39.760 回答
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: 
于 2019-10-11T15:26:54.633 回答
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
于 2019-10-11T17:46:48.413 回答
0

源代码似乎同意。从github 上的 powershell,我能够找到ForEach-Objectcmdlet 的以下相关源代码。

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

第一部分是最初如何读取参数。需要注意的重要一点是,所有<ScripBlock>s 都放入同一个数组中,除了-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>s的函数。_script基本上,如果 中有多个,则<ScripBlock>设置_scripts_start1。如果至少有三个,并且-End未明确设置,则_end设置为_scripts[_scripts.Count - 1]。然后_script[0]被调用,流被打开_script[1.._end-1],然后_endScript被调用(_script[_end]或者-End)。

private int _start, _end;

private void InitScriptBlockParameterSet() {
  // Calculate the start and end indexes for the processRecord script blocks
  _end = _scripts.Count;
  _start = _scripts.Count > 1 ? 1 : 0;

  // and set the end script if it wasnt explicitly set with a named parameter.
  if (!_setEndScript) {
    if (_scripts.Count > 2) {
      _end = _scripts.Count - 1;
      _endScript = _scripts[_end];
    }
  }

  // only process the start script if there is more than one script...
  if (_end < 2) return;

  if (_scripts[0] == null) return;
  /* invoke scripts[0] */
}

private void ProcessScriptBlockParameterSet() {
  /* for (i in _start.._end) invoke _scripts[i] */
}

private void EndBlockParameterSet() {
  /* invoke _endScript */
}

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

我猜他们中的一部分将“开源”并且更多地“*nix-y”将源代码定义为文档的一部分:)

于 2020-03-09T21:32:09.210 回答