$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"s echo). Я проверяю переменные $? а также $LastExitCode, Они равны Истине и 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?

Во-вторых, почему $? Ложь, когда 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 может или не взорваться в зависимости от того, где вы его запускаете]

6 ответов

Решение

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

PS> nslookup microsoft.com 2>&1 ; echo $?

False

Попался! Тем не менее, после некоторых болезненных царапин, вы никогда не забудете урок.

использование ($LastExitCode -eq 0) вместо $?

(Я использую PowerShell v2.)

' $? переменная задокументирована в about_Automatic_Variables:

$?
  Содержит статус выполнения последней операции

Это относится к самой последней операции PowerShell, а не к последней внешней команде, в которую вы входите $LastExitCode,

В вашем примере $LastExitCode 0, потому что последняя внешняя команда была cmd, который был успешным в отражении некоторого текста. Но 2>&1 вызывает сообщения stderr для преобразования в записи ошибок в выходном потоке, который сообщает PowerShell, что во время последней операции произошла ошибка, вызвавшая $? быть False,

Чтобы проиллюстрировать это немного подробнее, рассмотрим это:

> java -jar foo; $?; $ LastExitCode
Невозможно получить доступ к jarfile foo
Ложь
1

$LastExitCode равно 1, потому что это был код завершения java.exe. $? Неверно, потому что последнее, что сделала оболочка, не удалось.

Но если все, что я делаю, это переключаю их:

> java -jar foo; $ LastExitCode; $?
Невозможно получить доступ к jarfile foo
1
Правда

... затем $? Верно, потому что последнее, что сделала оболочка, была печать $LastExitCode хозяину, который был успешным.

В заключение:

> & {java -jar foo}; $?; $ LastExitCode
Невозможно получить доступ к jarfile foo
Правда
1

... что кажется немного нелогичным, но $? Верно сейчас, потому что выполнение блока скрипта было успешным, даже если команда запуска внутри него не была.


Возвращаясь к 2>&1 перенаправление...., которое вызывает запись ошибки в выходном потоке, что и дает этот многословный блоб о NativeCommandError, Оболочка сбрасывает всю запись об ошибке.

Это может быть особенно неприятно, когда все, что вы хотите сделать, это труба stderr а также stdout вместе, чтобы они могли быть объединены в файл журнала или что-то. Кто хочет, чтобы PowerShell вмешался в их файл журнала??? Если я сделаю ant build 2>&1 >build.log, то любые ошибки, которые идут на stderr вместо любопытных сообщений об ошибках в моем файле журнала добавьте любопытный $0,02 в PowerShell.

Но выходной поток не является текстовым потоком! Перенаправления - это просто еще один синтаксис для конвейера объектов. Записи об ошибках являются объектами, поэтому все, что вам нужно сделать, это преобразовать объекты в этом потоке в строки перед перенаправлением:

От:

> cmd / c "echo Hello от стандартной ошибки 1>&2" 2>&1
cmd.exe: привет от стандартной ошибки
В строке:1 символ:4
+ cmd &2" 2>&1
    + CategoryInfo: NotSpecified: (Привет из стандартной ошибки:String) [], RemoteException
    + FullyQualifiedErrorId: NativeCommandError

Для того, чтобы:

> cmd / c "echo Hello от стандартной ошибки 1>&2" 2>&1 | %{ "$_" }
Привет от стандартной ошибки

... и с перенаправлением в файл:

> cmd / c "echo Hello от стандартной ошибки 1>&2" 2>&1 | %{ "$_" } | tee out.txt
Привет от стандартной ошибки

...или просто:

> cmd / c "echo Hello от стандартной ошибки 1>&2" 2>&1 | %{ "$_" } >out.txt

Краткое описание проблем начиная с версии 7.0:

В движке PowerShell все еще есть ошибки в отношении2>перенаправления, применяемые к вызовам внешней программы:

Основная причина в том, что использование2>вызывает маршрутизацию вывода stderr (стандартная ошибка) через поток ошибок PowerShell (см. about_Redirection), что имеет следующие нежелательные последствия:

  • Если $ErrorActionPreference = 'Stop' оказывается в силе, используя 2>неожиданно вызывает ошибку завершения скрипта, т.е. прерывает скрипт (даже в форме2>$null, где очевидно намерение игнорировать строки stderr). См. Эту проблему на GitHub.

    • Обходной путь: (временно) установить $ErrorActionPreference = 'Continue'
  • поскольку 2> в настоящее время касается потока ошибок, $?, автоматическая переменная состояния успеха неизменно устанавливается на $Falseесли была выдана хотя бы одна строка stderr, которая больше не отражает истинный статус успеха команды. См. Эту проблему на GitHub.

    • Обходной путь, рекомендованный в вашем ответе: только когда-либо использовать$LASTEXITCODE -eq 0 для проверки успешности после обращения к внешним программам.
  • С 2>, строки stderr неожиданно записываются в автоматический $Error переменная (переменная, которая хранит журнал всех ошибок, произошедших в сеансе) - даже если вы используете 2>$null. См. Эту проблему на GitHub.

    • Обходной путь: не отслеживая, сколько записей об ошибках было добавлено, и удаляя их с помощью $Error.RemoveAt() один за другим их нет.

Как правило, к сожалению, некоторые хосты PowerShell по умолчанию направляют вывод stderr из внешних программ через поток ошибок PowerShell, т.е. обрабатывают его как вывод ошибки, что неуместно, потому что многие внешние программы используют stderr также для информации о состоянии или, в более общем смысле, для всего, что не данные (gitявляется ярким примером): не каждая строка stderr может рассматриваться как представляющая ошибку, и наличие вывода stderr не означает сбоя.

Затронутые хосты:

Хосты, которые ДЕЙСТВИТЕЛЬНО ведут себя должным образом при вызовах без удаленного взаимодействия и в фоновом режиме (они передают строки stderr на дисплей и обычно выводят их на печать):

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

(Примечание: это в основном спекуляция; я редко использую много собственных команд в PowerShell, и другие, вероятно, знают больше о внутренностях PowerShell, чем я)

Я полагаю, вы обнаружили несоответствие в хосте консоли PowerShell.

  1. Если PowerShell подбирает материал в стандартном потоке ошибок, он примет ошибку и выдаст NativeCommandError,
  2. PowerShell может поднять это, только если он отслеживает стандартный поток ошибок.
  3. PowerShell ISE должен следить за ним, потому что это не консольное приложение и, следовательно, нативное консольное приложение не имеет консоли для записи. Вот почему в PowerShell ISE это не работает независимо от 2>&1 оператор перенаправления.
  4. Хост консоли будет отслеживать стандартный поток ошибок, если вы используете 2>&1 оператор перенаправления, потому что вывод в стандартном потоке ошибок должен быть перенаправлен и, следовательно, считан.

Я предполагаю, что консольный хост PowerShell ленив и просто передает консольным командам консоль, если ей не требуется обрабатывать их вывод.

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

Для меня это была проблема с ErrorActionPreference. При запуске из ISE я установил $ErrorActionPreference = "Stop" в первых строках, и это перехватывало все события с добавлением *>&1 в качестве параметров к вызову.

Итак, сначала у меня была эта строка:

& $exe $parameters *>&1

Что, как я уже сказал, не сработало, потому что у меня в файле ранее было $ErrorActionPreference = "Stop" (или это можно установить глобально в профиле для пользователя, запускающего скрипт).

Итак, я попытался обернуть его в выражение-выражение, чтобы вызвать 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 }
    }
}

Powershell 7.1

Есть новая экспериментальная функция для включения "нормального" поведения:

      Enable-ExperimentalFeature PSNotApplyErrorActionToStderr

К сожалению, эту функцию нельзя включить только для текущей области, она будет включена для всей учетной записи пользователя (все пользователи с аргументом -Scope AllUsers) и требует запуска нового сеанса PS, прежде чем вступит в силу.

Теперь этот пример кода ...

      & cmd /c "echo Hello from standard error 1>&2" 2>&1
echo "`$LastExitCode=$LastExitCode and `$?=$?"
echo "`$Error.Count=$($Error.Count)"

... выводит то, что можно было бы ожидать:

      Hello from standard error
$LastExitCode=0 and $?=True
$Error.Count=0
Другие вопросы по тегам