Как * точно * работает правая часть оператора -f в PowerShell?

Кстати, в прошлый раз я запутался PowerShell нетерпеливо разворачивает коллекции, Кейт резюмировал эту эвристику следующим образом:

Помещение результатов (массива) в выражение группировки (или подвыражение, например, $ ()) снова дает право на развертывание.

Я принял этот совет близко к сердцу, но все еще не могу объяснить некоторые эзотерические вопросы. В частности, оператор Format, похоже, не играет по правилам.

$lhs = "{0} {1}"

filter Identity { $_ }
filter Square { ($_, $_) }
filter Wrap { (,$_) }
filter SquareAndWrap { (,($_, $_)) }

$rhs = "a" | Square        
# 1. all succeed
$lhs -f $rhs
$lhs -f ($rhs)
$lhs -f $($rhs)
$lhs -f @($rhs)

$rhs = "a" | Square | Wrap       
# 2. all succeed
$lhs -f $rhs
$lhs -f ($rhs)
$lhs -f $($rhs)
$lhs -f @($rhs)

$rhs = "a" | SquareAndWrap       
# 3. all succeed
$lhs -f $rhs
$lhs -f ($rhs)
$lhs -f $($rhs)
$lhs -f @($rhs)

$rhs = "a", "b" | SquareAndWrap       
# 4. all succeed by coercing the inner array to the string "System.Object[]"
$lhs -f $rhs
$lhs -f ($rhs)
$lhs -f $($rhs)
$lhs -f @($rhs)

"a" | Square | % {
    # 5. all fail
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

"a", "b" | Square | % {
    # 6. all fail
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

"a" | Square | Wrap | % {
    # 7. all fail
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

"a", "b" | Square | Wrap | % {
    # 8. all fail
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

"a" | SquareAndWrap | % {
    # 9. only @() and $() succeed
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

"a", "b" | SquareAndWrap | % {
    # 10. only $() succeeds
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

Применяя те же шаблоны, которые мы видели в предыдущем вопросе, становится ясно, почему случаи, подобные №1 и №5, ведут себя по-разному: оператор конвейера сигнализирует механизму сценария развернуть другой уровень, а оператор присваивания - нет. Другими словами, все, что находится между двумя |, рассматривается как сгруппированное выражение, как если бы оно находилось внутри ().

# all of these output 2
("a" | Square).count                       # explicitly grouped
("a" | Square | measure).count             # grouped by pipes
("a" | Square | Identity).count            # pipe + ()
("a" | Square | Identity | measure).count  # pipe + pipe

По той же причине случай №7 не является улучшением по сравнению с вариантом №5. Любая попытка добавить лишнюю оболочку будет немедленно отменена дополнительной трубкой. То же # 8 против # 6. Немного расстраивает, но до сих пор я полностью согласен.

Остающиеся вопросы:

  • Почему дело №3 не постигнет судьба №4? $ rhs должен содержать вложенный массив (, ("a", "a")), но его внешний уровень разворачивается ... где-то ...
  • Что происходит с различными операторами группировки в №9-10? Почему они ведут себя так беспорядочно и зачем они вообще нужны?
  • Почему отказы в случае №10 не ухудшаются изящно, как в случае №4?

person Richard Berg    schedule 09.12.2009    source источник


Ответы (2)


Что ж, в этом точно есть ошибка. (Я только что вчера написал об этом страницу в PoshCode Wiki, и там при подключении).

Сначала ответы, потом еще вопросы:

Чтобы добиться согласованного поведения массивов с форматированием строки -f, вам нужно на 100% убедиться, что они являются объектами PSObject. Я предлагаю сделать это при их назначении. Предполагается, что это должно выполняться PowerShell автоматически, но по какой-то причине это не выполняется до тех пор, пока вы не получите доступ к свойству или чему-то еще (как описано в этом страница вики и ошибка). Например (<##> - моя подсказка):

<##> $a = 1,2,3
<##> "$a"
1 2 3

<##> $OFS = "-"  # Set the Output field separator
<##> "$a"
1-2-3

<##> "{0}" -f $a
1 

<##> $a.Length
3 

<##> "{0}" -f $a
1-2-3

# You can enforce correct behavior by casting:
<##> [PSObject]$b = 1,2,3
<##> "{0}" -f $a
1-2-3

Обратите внимание: когда вы это сделаете, они НЕ БУДУТ развернуты при переходе к -f, а будут выводиться правильно - как если бы вы поместили переменную непосредственно в строку.

Почему дело №3 не постигнет судьба №4? $ rhs должен содержать вложенный массив (, ("a", "a")), но его внешний уровень разворачивается ... где-то ...

Самый простой вариант ответа состоит в том, что ОБЕ # 3 и # 4 разворачиваются. Разница в том, что в 4 внутреннее содержимое представляет собой массив (даже после того, как внешний массив развернут):

$rhs = "a" | SquareAndWrap
$rhs[0].GetType()  # String

$rhs = "a","b" | SquareAndWrap
$rhs[0].GetType()  # Object[]

Что происходит с различными операторами группировки в №9-10? Почему они ведут себя так беспорядочно и зачем они вообще нужны?

Как я сказал ранее, массив должен считаться одним параметром формата и должен выводиться с использованием правил форматирования строк PowerShell (то есть: разделенных $OFS) так же, как если бы вы поместили $ _ непосредственно в строку < / em> ... поэтому, когда PowerShell работает правильно, $lhs -f $rhs завершится ошибкой, если $ lhs содержит два заполнителя.

Конечно, мы уже заметили, что в нем есть ошибка.

Однако я не вижу ничего беспорядочного: @ () и $ () работают одинаково для 9 и 10, насколько я могу видеть (основная разница, по сути, вызвана тем, как ForEach разворачивает массив верхнего уровня :

> $rhs = "a", "b" | SquareAndWrap
> $rhs | % { $lhs -f @($_); " hi " }
a a
 hi 
b b
 hi 

> $rhs | % { $lhs -f $($_); " hi " }
a a
 hi 
b b
 hi     

# Is the same as:
> [String]::Format( "{0} {1}", $rhs[0] ); " hi "
a a
 hi 

> [String]::Format( "{0} {1}", $rhs[1] ); " hi "
b b
 hi     

Итак, вы видите ошибку в том, что @ () или $ () приведут к тому, что массив будет передан как [object []] в вызов строкового формата, а не как PSObject, который имеет специальные строковые значения.

Почему отказы в случае №10 не ухудшаются изящно, как в случае №4?

По сути, это тот же самый баг, но в другом проявлении. Массивы никогда не должны отображаться как «System.Object []» в PowerShell, если вы вручную не вызываете их собственный метод .ToString() или не передаете их напрямую String.Format () ... причина, по которой они делают в # 4, заключается в этой ошибке: PowerShell имеет не удалось расширить их как PSOjbects перед передачей их вызову String.Format.

Вы можете увидеть это, если вы обращаетесь к свойству массива перед его передачей или преобразуете его в PSObject, как в моих исходных примерах. Технически ошибки в № 10 являются правильным выводом: вы передаете только ОДНУ вещь (массив) в string.format, когда он ожидал ДВЕ вещи. Если вы измените $ lhs на "{0}", вы увидите массив, отформатированный с помощью $ OFS


Мне интересно, какое поведение вам нравится и какое вы считаете правильным, учитывая мой первый пример? Я думаю, что вывод, разделенный на $ OFS, верен, в отличие от разворачивания массива, как это происходит, если вы @ (переносите) его или преобразуете его в [object []] (Кстати, обратите внимание, что произойдет, если вы приведете его к [int [ ]] - это другое ошибочное поведение):

> "{0}" -f [object[]]$a
1

> "{0}, {1}" -f [object[]]$a  # just to be clear...
1,2

>  "{0}, {1}" -f [object[]]$a, "two"  # to demonstrate inconsistency
System.Object[],two

> "{0}" -f [int[]]$a
System.Int32[]

Я уверен, что многие сценарии были написаны неосознанно, используя эту ошибку, но мне все еще кажется довольно очевидным, что развертывание, происходящее в примере для ясности, НЕ является правильным поведением, но происходит потому, что при вызове (внутри ядра PowerShell) к .Net String.Format( "{0}", a ) ... $a - это object[], чего ожидал String.Format в качестве параметра Params ...

Я думаю, это нужно исправить. Если есть желание сохранить «функциональность» разворачивания массива, это нужно делать с помощью оператора @ splatting, верно?

person Community    schedule 09.12.2009
comment
Отличное резюме - отмечено как ответ, хотя я не согласен с вашим мнением. Я думаю, что {0} {1} -f a, b должно быть эквивалентно $ arr = a, b; {0} {1} -f $ обр. То есть правая часть оператора -f должна быть [params object []], чтобы смешивать жаргон C # и PS. Я не понимаю, почему $ arr следует преобразовывать в строку с помощью OFS, если вы явно не цитируете его в правой части оператора. - person Richard Berg; 10.12.2009
comment
Ну, основная причина, по которой он должен быть преобразован в строку, заключается в том, что должно выполняться форматирование строки. То есть, если вы не используете коды форматирования, такие как {{0: X} -f 42}, тогда форматирование строки должно вести себя как преобразование объекта в строку. Вот как работает форматирование строк .Net. Изменение правил для PowerShell сбивает с толку. Конечно, они уже изменили его с помощью $ OFS ... но изменение его СНОВА будет означать, что массивы будут выводить по крайней мере три разных способа, и вы никогда не узнаете, какой. Например, что должно произойти, если я сделаю: {{0} {1} -f $ arr, hello}? - person Jaykul; 10.12.2009
comment
Честно говоря, часть меня согласна с вами, потому что это удобно! Но я хочу, чтобы это происходило только явно, поэтому я могу сделать ОБЕИХ из них: { $a = 1,"+",2; "{0} = {1}" -f $a,"three"; "{2}-{0}={3}" -f @a,"one" } и получить их как: 1 + 2 = три и 2-1 = один ... прямо сейчас вы МОЖЕТЕ сделать это, но только если вы ' сумасшедший, как лис: { $a = 1,"+",2; "{0} = {1}" -f [PSObject]$a,"three"; "{2}-{0}={3}" -f [object[]]($a+"one") } и ЭТО просто непостижимо. - person Jaykul; 10.12.2009
comment
Вообще-то, это тоже работает: [PSObject]$a = 1,"+",2; "{0} = {1}" -f $a,"three"; "{2}-{0}={3}" -f @($a+"one"), но только чуть менее загадочно ... - person Jaykul; 10.12.2009
comment
Я бы согласился на любой, если вы можете легко переключать режимы (например, добавляя оператор splat). И при условии, что они предоставят нам некоторую последовательность и добросовестную документацию! :) - person Richard Berg; 10.12.2009
comment
Вторая ссылка не работает (Connect был закрыт, и URL-адрес перенаправляет на общую страницу). - person Peter Mortensen; 14.01.2019

Ни Square, ни Wrap не будут делать то, что вы пытаетесь в # 5 и 7. Независимо от того, помещаете ли вы массив в выражение группировки (), как в Square, или вы используете оператор запятой, как в Wrap, когда вы используете эти функции в конвейере, их вывод разворачивается, поскольку он поочередно передается на следующий этап конвейера. Точно так же в 6 и 8 не имеет значения, что вы вводите несколько объектов, и Square, и Wrap будут передавать их по одному на ваш этап foreach.

Случаи 9 и 10, похоже, указывают на ошибку в PowerShell. Возьмите этот измененный фрагмент и попробуйте:

"a" | SquareAndWrap | % {    
    # 9. only @() and $() succeed  
    $_.GetType().FullName
    $_.Length
    $lhs -f [object[]]$_
    $lhs -f [object[]]($_)    
    $lhs -f @($_)   
    $lhs -f $($_)            
}

Оно работает. Он также показывает, что foreach ужеd получает объект [] размером 2, поэтому $_ должен работать без преобразования в [объект []] или без упаковки в подвыражение или подвыражение массива. Мы видели некоторые ошибки V2, связанные с некорректным развертыванием psobject, и это, похоже, еще один пример этого. Если вы развернете psobject вручную, он будет работать, например. $_.psobject.baseobject.

Я "думаю" в Wrap вы стремитесь к следующему:

function Wrap2 { Begin {$coll = @();} Process {$coll += $_} End {,$coll} }

Это соберет весь ввод конвейера, а затем выведет его как единый массив. Это будет работать для случая 8, но вам все равно нужно преобразовать его в [object []] в первых двух случаях использования оператора -f.

Кстати, парные скобки в Square и Wrap и внешние скобки в SquareAndWrap не нужны.

person Keith Hill    schedule 09.12.2009
comment
Прохладный. Я запишу ошибку подключения. - person Richard Berg; 09.12.2009