PowerShell: таинственный параметр -RemainingScripts в ForEach-Object

Короткий вопрос: у кого-нибудь есть подробная информация о параметре -RemainingScripts в ForEach-Object?

Длинный вопрос:

Я только начал изучать PowerShell на прошлой неделе и просматриваю все командлеты, чтобы узнать больше. Основываясь на общедоступной документации, мы знаем, что 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"}

Ожидаемый результат: 1 раз для «block1» и «block3», «block2» повторяется для каждого переданного элемента, и счетчик каталогов / файлов все правильно. Все идет нормально.

Что интересно, следующая команда также работает и дает точно такой же результат:

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

В foreach передано всего 3 ScriptBlocks. Согласно руководству, первый идет в -Process (позиция 1). А как насчет оставшихся 2? Согласно инструкции, параметра с «Позицией 2» нет. Итак, я обратился к Trace-Command и обнаружил, что последние 2 блока скрипта были фактически для RemainingScripts как «IList с 2 элементами».

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, позже он фактически используется как «Начало», а оставшиеся 2 становятся «Процессом» и «Конец».

Тем не менее, все вышеперечисленное - мое безумное предположение. Я не нашел документации, подтверждающей мое предположение

Итак, наконец, мы возвращаемся к моему короткому вопросу :) У кого-нибудь есть подробная информация о параметре -RemainingScripts в ForEach-Object?

Спасибо.


person Fang Zhou    schedule 20.05.2013    source источник


Ответы (5)


Я провел больше исследований и теперь чувствую себя уверенно, отвечая на поведение параметра -RemainingScripts при передаче нескольких блоков ScriptBlocks.

Если вы запустите следующие команды и внимательно изучите результат, вы найдете шаблон. Это не совсем просто, но все же несложно понять.

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: easy, он просто переходит в -Process (наиболее распространенное использование)

  • Когда передается ровно 2 ScriptBlocks, возможны 3 комбинации

    1. -Process & -Begin -> execute as specified
    2. -Process & -End -> выполнить как указано
    3. -Process & -RemainingScripts -> Process становится Begin, а RemainingScripts становится Process

Если мы запустим эти 2 оператора:

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 ScriptBlocks, выполните следующий рабочий процесс:

    1. Bind all scriptblocks as specified (Begin,Process,End); remaining ScriptBlocks go to RemainingScripts
    2. Закажите все сценарии как: Начало> Процесс> Осталось> Конец.
    3. Результатом заказа является набор ScriptBlocks. Назовем эту коллекцию OrderedScriptBlocks.

      • If Begin/End are not bound, just ignore
    4. (Внутренне) повторно привязать параметры на основе OrderedScriptBlocks

      • OrderedScriptBlocks[0] becomes Begin
      • OrderedScriptBlocks [1 ..- 2] становится процессом
      • 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!

Тем не менее, я должен признать, что нет документации, подтверждающей мое предположение (не моя вина!), Но этих тестовых примеров должно быть достаточно, чтобы объяснить описанное мной поведение.

person Fang Zhou    schedule 20.05.2013

Вот подробности. 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
person Andy Arismendi    schedule 20.05.2013
comment
Спасибо, Энди, вы ответили на пункт 1: параметр -RemainingScripts примет все несвязанные блоки ScriptBlocks, но не объяснил пункт 2, когда передаются 3 блока, хотя первый идет в -Process ... что на самом деле является основной причиной этот вопрос задается. Я провел больше исследований и теперь думаю, что могу ответить на пункт 2. Тем не менее, я ценю ваш ответ и технику проверки параметра. И извините, у меня нет разрешения голосовать за ваш ответ, хотя он очень полезен. - person Fang Zhou; 20.05.2013

Вот еще несколько примеров, подтверждающих гипотезу упорядочивания @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: 
person Bacon Bits    schedule 11.10.2019

Я считаю, что -remainingscripts (с атрибутом ValueFromRemainingArguments) предназначен для включения подобной идиомы из Windows Powershell в действии, идиомы, которую почти никто не знает (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
person js2010    schedule 11.10.2019

Исходный код, кажется, согласен. Из powershell на github Мне удалось найти следующие соответствующие фрагменты исходного кода для командлета ForEach-Object.

Комментарии /* ... */ принадлежат мне, и в остальном я несколько изменил код, поэтому обратитесь к фактическому источнику для надлежащей справки.

Первая часть - это то, как изначально считываются параметры. Важно отметить, что все <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. Обычно, если в _scripts больше одного <ScripBlock>, _start устанавливается в 1. Если есть по крайней мере три, а -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 в действии, который, похоже, получил какое-то благословение от команды PowerShell (по крайней мере, Snover).

Я предполагаю, что часть из них идет «с открытым исходным кодом» и, будучи более «* nix-y», определяет исходный код как часть документации :)

person Nathan Chappell    schedule 09.03.2020
comment
Интересным моментом является то, что аргументы блока сценария обрабатываются в списке по мере их анализа (кроме -End). Таким образом, если -RemainingScripts оценивается перед -Process путем присвоения ему имени (и помещения его перед -Process, если он назван), то он помещается перед -Process скриптами в списке. Один из -RemainingScripts мог даже стать -Begin. (Подтверждение: это сводка поведения, описанного в ответе @Bacon Bits) - person Uber Kluger; 25.09.2020