Подтвердить что ты не робот

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

4b9b3361

Ответ 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.

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