$ LastExitCode = 0, но $? = False в PowerShell. Перенаправление stderr в stdout дает NativeCommandError
Почему PowerShell демонстрирует удивительное поведение во втором примере ниже?
Во-первых, пример нормального поведения:
PS C:\> & cmd /c "echo Hello from standard error 1>&2"; echo "'$LastExitCode=$LastExitCode and '$?=$?"
Hello from standard error
$LastExitCode=0 and $?=True
Никаких сюрпризов. Я печатаю сообщение со стандартной ошибкой (используя cmd
echo
). Я проверяю переменные $?
и $LastExitCode
. Как и ожидалось, они равны True и 0 соответственно.
Однако, если я прошу PowerShell перенаправить стандартную ошибку на стандартный вывод по первой команде, я получу ошибку NativeCommandError:
PS C:\> & cmd /c "echo Hello from standard error 1>&2" 2>&1; echo "'$LastExitCode=$LastExitCode and '$?=$?"
cmd.exe : Hello from standard error
At line:1 char:4
+ cmd <<<< /c "echo Hello from standard error 1>&2" 2>&1; echo "'$LastExitCode=$LastExitCode and '$?=$?"
+ CategoryInfo : NotSpecified: (Hello from standard error :String) [], RemoteException
+ FullyQualifiedErrorId : NativeCommandError
$LastExitCode=0 and $?=False
Мой первый вопрос, почему NativeCommandError?
Во-вторых, почему $?
False, когда cmd
успешно запущен и $LastExitCode
равен 0? Документация PowerShell об автоматических переменных явно не определяет $?
. Я всегда предполагал, что это правда, если и только если $LastExitCode
равен 0, но мой пример противоречит этому.
Вот как я столкнулся с таким поведением в реальном мире (упрощенно). Это действительно FUBAR. Я вызывал один сценарий PowerShell из другого. Внутренний скрипт:
cmd /c "echo Hello from standard error 1>&2"
if (! $?)
{
echo "Job failed. Sending email.."
exit 1
}
# Do something else
Запустив это просто как .\job.ps1
, он работает нормально, и электронное письмо не отправляется. Однако я вызывал его из другого скрипта PowerShell, записывая в файл .\job.ps1 2>&1 > log.txt
. В этом случае письмо отправляется! То, что вы делаете вне скрипта с потоком ошибок, влияет на внутреннее поведение скрипта. Наблюдение за явлением меняет результат. Это похоже на квантовую физику, а не на сценарии!
[Интересно: .\job.ps1 2>&1
может взорваться или не взорваться в зависимости от того, где вы его запускаете]
Ответы
Ответ 1
Эта ошибка является непредвиденным следствием предписывающего проектирования PowerShell для обработки ошибок, поэтому, скорее всего, она никогда не будет исправлена. Если ваш script воспроизводится только с другими сценариями PowerShell, вы в безопасности. Однако, если ваш script взаимодействует с приложениями из большого мира, эта ошибка может укусить.
PS> nslookup microsoft.com 2>&1 ; echo $?
False
Попался! Тем не менее, после некоторых болезненных царапин, вы никогда не забудете урок.
Используйте ($LastExitCode -eq 0)
вместо $?
Ответ 2
(Я использую PowerShell v2.)
Переменная '$?
документирована в about_Automatic_Variables
:
$?
Contains the execution status of the last operation
Это относится к самой последней работе PowerShell, а не к последней внешней команде, что вы получаете в $LastExitCode
.
В вашем примере $LastExitCode
равен 0, поскольку последняя внешняя команда была cmd
, что было успешным в повторении какого-либо текста. Но 2>&1
приводит к тому, что сообщения stderr
преобразуются в записи ошибок в выходном потоке, что говорит PowerShell о том, что во время последней операции произошла ошибка, в результате чего $?
было False
.
Чтобы проиллюстрировать это немного, рассмотрим следующее:
> java -jar foo; $?; $LastExitCode
Unable to access jarfile foo
False
1
$LastExitCode
равен 1, поскольку это был код выхода java.exe. $?
является False, потому что последнее, что не получилось, не получилось.
Но если все, что я делаю, это их:
> java -jar foo; $LastExitCode; $?
Unable to access jarfile foo
1
True
... then $?
является True, потому что последнее, что сделала оболочка, это печать $LastExitCode
на хост, который был успешным.
Наконец:
> &{ java -jar foo }; $?; $LastExitCode
Unable to access jarfile foo
True
1
... который кажется немного интуитивно понятным, но $?
теперь True, потому что выполнение блока script прошло успешно, даже если команда, запущенная внутри него, не была.
Возвращаясь к перенаправлению 2>&1
...., что приводит к записи ошибок в выходной поток, что и дает длинный blob об NativeCommandError
. Оболочка сбрасывает всю запись об ошибках.
Это может быть особенно раздражающим, когда все, что вы хотите сделать, это pipe stderr
и stdout
вместе, чтобы их можно было объединить в файл журнала или что-то в этом роде. Кто хочет, чтобы PowerShell загрузился в файл журнала? Если я делаю ant build 2>&1 >build.log
, то любые ошибки, которые идут на stderr
, имеют PowerShell nosy $0.02, а не чистые сообщения об ошибках в моем файле журнала.
Но выходной поток не является текстовым потоком! Перенаправления - это еще один синтаксис для конвейера объекта. Записи ошибок являются объектами, поэтому все, что вам нужно сделать, это преобразовать объекты в этот поток в строки перед перенаправлением:
From:
> cmd /c "echo Hello from standard error 1>&2" 2>&1
cmd.exe : Hello from standard error
At line:1 char:4
+ cmd &2" 2>&1
+ CategoryInfo : NotSpecified: (Hello from standard error :String) [], RemoteException
+ FullyQualifiedErrorId : NativeCommandError
To:
> cmd /c "echo Hello from standard error 1>&2" 2>&1 | %{ "$_" }
Hello from standard error
... и с перенаправлением на файл:
> cmd /c "echo Hello from standard error 1>&2" 2>&1 | %{ "$_" } | tee out.txt
Hello from standard error
... или просто:
> cmd /c "echo Hello from standard error 1>&2" 2>&1 | %{ "$_" } >out.txt
Ответ 3
(Примечание. В основном это спекуляция; я редко использую много собственных команд в PowerShell, и другие, вероятно, знают больше о внутренностях PowerShell, чем я)
Полагаю, вы обнаружили несоответствие в хосте консоли PowerShell.
- Если PowerShell обнаружит что-либо в стандартном потоке ошибок, он примет ошибку и выдаст
NativeCommandError
.
- PowerShell может поднять это, только если он отслеживает стандартный поток ошибок.
- PowerShell ISE должен следить за ним, потому что это не консольное приложение и, следовательно, нативное консольное приложение не имеет консоли для записи. Вот почему в PowerShell ISE происходит сбой независимо от оператора перенаправления
2>&1
.
- Хост консоли будет отслеживать стандартный поток ошибок, если вы используете оператор перенаправления
2>&1
, поскольку выходные данные в стандартном потоке ошибок должны быть перенаправлены и, следовательно, считаны.
Я полагаю, что консольный хост PowerShell ленив и просто передает консольным командам консоль, если ей не требуется обрабатывать их вывод.
Я действительно считаю, что это ошибка, потому что PowerShell ведет себя по-разному в зависимости от хост-приложения.
Ответ 4
Для меня это была проблема с ErrorActionPreference.
При запуске из ISE я установил $ErrorActionPreference = "Стоп" в первых строках и перехватил все событие с * > & 1, добавленным в качестве параметров для вызова.
Итак, сначала у меня была эта строка:
& $exe $parameters *>&1
Как я уже сказал, это не сработало, потому что раньше у меня было $ErrorActionPreference = "Стоп" ранее (или его можно настроить глобально в профиле для запуска пользователем script).
Итак, я попытался обернуть его в Invoke-Expression, чтобы заставить ErrorAction:
Invoke-Expression -Command "& `"$exe`" $parameters *>&1" -ErrorAction Continue
И это тоже не работает.
Поэтому мне пришлось отказаться от взлома с временным переопределением ErrorActionPreference:
$old_error_action_preference = $ErrorActionPreference
try
{
$ErrorActionPreference = "Continue"
& $exe $parameters *>&1
}
finally
{
$ErrorActionPreference = $old_error_action_preference
}
Что работает для меня.
И я включил это в функцию:
<#
.SYNOPSIS
Executes native executable in specified directory (if specified)
and optionally overriding global $ErrorActionPreference.
#>
function Start-NativeExecutable
{
[CmdletBinding(SupportsShouldProcess = $true)]
Param
(
[Parameter (Mandatory = $true, Position = 0, ValueFromPipelinebyPropertyName=$True)]
[ValidateNotNullOrEmpty()]
[string] $Path,
[Parameter (Mandatory = $false, Position = 1, ValueFromPipelinebyPropertyName=$True)]
[string] $Parameters,
[Parameter (Mandatory = $false, Position = 2, ValueFromPipelinebyPropertyName=$True)]
[string] $WorkingDirectory,
[Parameter (Mandatory = $false, Position = 3, ValueFromPipelinebyPropertyName=$True)]
[string] $GlobalErrorActionPreference,
[Parameter (Mandatory = $false, Position = 4, ValueFromPipelinebyPropertyName=$True)]
[switch] $RedirectAllOutput
)
if ($WorkingDirectory)
{
$old_work_dir = Resolve-Path .
cd $WorkingDirectory
}
if ($GlobalErrorActionPreference)
{
$old_error_action_preference = $ErrorActionPreference
$ErrorActionPreference = $GlobalErrorActionPreference
}
try
{
Write-Verbose "& $Path $Parameters"
if ($RedirectAllOutput)
{ & $Path $Parameters *>&1 }
else
{ & $Path $Parameters }
}
finally
{
if ($WorkingDirectory)
{ cd $old_work_dir }
if ($GlobalErrorActionPreference)
{ $ErrorActionPreference = $old_error_action_preference }
}
}