Почему PowerShell применяет предикат `где` к пустому списку

Если я запускаю это в PowerShell, я ожидаю увидеть вывод 0 (нуль):

Set-StrictMode -Version Latest

$x = "[]" | ConvertFrom-Json | Where { $_.name -eq "Baz" }
Write-Host $x.Count

Вместо этого я получаю эту ошибку:

The property 'name' cannot be found on this object. Verify that the     property exists and can be set.
At line:1 char:44
+     $x = "[]" | ConvertFrom-Json | Where { $_.name -eq "Baz" }
+                                            ~~~~~~~~~~~~~~~
+ CategoryInfo          : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : PropertyAssignmentException

Если я поставлю брекеты "[]" | ConvertFrom-Json это становится этим:

$y = ("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" }
Write-Host $y.Count

И тогда это "работает".

Что не так перед введением скобок?

Чтобы объяснить цитаты вокруг "работает" - установка строгого режима Set-StrictMode -Version Latest указывает на то, что я звоню .Count на $null объект. Это решается путем упаковки в @():

$z = @(("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" })
Write-Host $z.Count

Я нахожу это довольно неудовлетворяющим, но это не относится к актуальному вопросу.

2 ответа

Решение

Почему PowerShell применяет предикат Where в пустой список?

Так как ConvertFrom-Json говорит Where-Object не пытаться перечислить его вывод.

Поэтому PowerShell пытается получить доступ к name свойство самого пустого массива, как если бы мы делали:

$emptyArray = New-Object object[] 0
$emptyArray.name

Когда вы заключаете ConvertFrom-Json в скобках PowerShell интерпретирует его как отдельный конвейер, который выполняется и завершается до того, как любой вывод может быть отправлен Where-Object, а также Where-Object поэтому не может знать, что ConvertFrom-Json хотел, чтобы он относился к массиву как таковому.


Мы можем воссоздать это поведение в powershell, явно вызвав Write-Output с -NoEnumerate набор параметров переключателя:

# create a function that outputs an empty array with -NoEnumerate
function Convert-Stuff 
{
  Write-Output @() -NoEnumerate
}

# Invoke with `Where-Object` as the downstream cmdlet in its pipeline
Convert-Stuff | Where-Object {
  # this fails
  $_.nonexistingproperty = 'fail'
}

# Invoke in separate pipeline, pass result to `Where-Object` subsequently
$stuff = Convert-Stuff
$stuff | Where-Object { 
  # nothing happens
  $_.nonexistingproperty = 'meh'
}

Write-Output -NoEnumerate внутренние звонки Cmdlet.WriteObject(arg, false)что в свою очередь приводит к тому, что среда выполнения не перечисляет arg значение во время привязки параметра к нижестоящему командлету (в вашем случае Where-Object)


Почему это было бы желательно?

В конкретном контексте синтаксического анализа JSON такое поведение может быть действительно желательным:

$data = '[]', '[]', '[]', '[]' |ConvertFrom-Json

Должен ли я ожидать 5 объектов от ConvertFrom-Json теперь, когда я передал ему 5 действительных документов JSON?:-)

С пустым массивом в качестве прямого входного конвейера ничего не передается через конвейер, потому что массив перечисляется, и так как нечего перечислять - пустой массив не имеет элементов - Where блок скрипта никогда не выполняется:

# The empty array is enumerated, and since there's nothing to enumerate,
# the Where[-Object] script block is never invoked.
@() | Where { $_.name -eq "Baz" } 

В отличие от "[]" | ConvertFrom-Json создает пустой массив как отдельный объект вывода, а не перечисляет его (несуществующие) элементы, поскольку ConvertFrom-Json по конструкции не перечисляет элементы массивов, которые он выводит; это эквивалент:

# Empty array is sent as a single object through the pipeline.
# The Where script block is invoked once and sees $_ as that empty array.
Write-Output -NoEnumerate @() | Where { $_.name -eq "Baz" }

ConvertFrom-Json Поведение удивительно в контексте PowerShell - командлеты обычно перечисляют несколько выходных данных - но имеет смысл в контексте анализа JSON; в конце концов, информация будет потеряна, если ConvertFrom-Json перечислил пустой массив, учитывая, что тогда вы не сможете отличить его от пустого ввода JSON ("" | ConvertFrom-Json).

Это напряжение обсуждается в этом выпуске GitHub.

Консенсус заключается в том, что оба варианта использования являются законными и что у пользователей должен быть выбор между двумя вариантами поведения - перечислением или нет - посредством переключения; что касается PowerShell Core 6.2.0, формальное решение не было принято, но если должна быть сохранена обратная совместимость, это должно быть поведение перечисления, которое включено (например, -Enumerate).

Если перечисление желательно, то пока что обходной путь - принудительно перечислить, просто заключив ConvertFrom-Json вызывать (...) (который преобразует его в выражение, а выражения всегда перечисляют выходные данные команды при использовании в конвейере):

# (...) around the ConvertFrom-Json call forces enumeration of its output.
# The empty array has nothing to enumerate, so the Where script block is never invoked.
("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" }

Что касается того, что вы пытались: ваша попытка получить доступ к .Count собственность и ваше использование @(...):

$y = ("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" }
$y.Count # Fails with Set-StrictMode -Version 2 or higher

С ConvertFrom-Json звонок обернут в (...) Ваша общая команда не возвращает ничего: $null, но, точнее, "массив-ноль", который является [System.Management.Automation.Internal.AutomationNull]::Value синглтон, указывающий на отсутствие вывода команды. (В большинстве случаев последнее рассматривается так же, как $null, хотя особенно не когда используется как вход конвейера.)

[System.Management.Automation.Internal.AutomationNull]::Value не имеет .Count свойство, поэтому с Set-StrictMode -Version 2 или выше, вы получите The property 'count' cannot be found on this object. ошибка.

Оборачивая весь трубопровод в @(...) оператор подвыражения массива, вы гарантируете обработку вывода как массива, который с помощью [массива-нулевого вывода создает пустой массив - который имеет .Count имущество.

Обратите внимание, что вы должны быть в состоянии позвонить .Count на $null а также [System.Management.Automation.Internal.AutomationNull]::Value с учетом того, что PowerShell добавляет .Count свойство для каждого объекта, если он еще не существует, в том числе для скаляров, в похвальных усилиях по унификации обработки коллекций и скаляров.

То есть с Set-StrictMode установлен в -Off (по умолчанию) или -Version 1 следующее работает и - разумно - возвращает 0:

# With Set-StrictMode set to -Off (the default) or -Version 1:

# $null sensibly has a count of 0.
PS> $null.Count
0

# So does the "array-valued null", [System.Management.Automation.Internal.AutomationNull]::Value 
# `. {}` is a simple way to produce it.
PS> (. {}).Count # `. {}` outputs 
0

Что вышеупомянутое в настоящее время не работает с Set-StrictMode -Version 2 или выше (начиная с PowerShell Core 6.2.0), следует рассматривать как ошибку, о которой сообщалось в этой проблеме GitHub (Джеффри Сновер, не менее).

Другие вопросы по тегам