Почему 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 (Джеффри Сновер, не менее).