Вопрос по windows – $ LastExitCode = 0, но $? = False в PowerShell. Перенаправление stderr в stdout дает NativeCommandError

39

Почему 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& APOS; secho). Я проверяю переменные$? а также$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, В этом случае письмо отправляется! То, что вы делаете вне скрипта с потоком ошибок, влияет на внутреннее поведение скрипта.Observing a phenomenon changes the outcome. Это похоже на квантовую физику, а не на сценарии!

[Что интересно:.\job.ps1 2>&1 может или не взорвать в зависимости от того, где вы запускаете]

Рэймонд, возможно связанный, но на самом деле не дубликат. Было бы хорошо, если бы Джеффри Сновер вмешался здесь сWord of God, хоть :-) Joey
Мэтт: Конечно, они должны использовать это в качестве аргумента. В приведенном случае это был аргументshell cmd, Если вы хотите перенаправления, вам нужна оболочка, которая понимает это.nslookup это просто команда, которая делает свое дело, но это не оболочка. Joey
Похоже, что обходной путь - это избежать оператора перенаправления:& cmd /c "echo Hello from standard error 1>&2" 2`>`&1 Andy Arismendi
возможный дубликатPowershell difference between $? and $LastExitCode Raymond Chen
Я задал другой вопрос, чтобы убедиться, что я понял ожидаемое поведение, прежде чем демонстрировать неожиданное. Colonel Panic

Ваш Ответ

4   ответа
63

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

& Apos;$?& APOS; переменная задокументирована вabout_Automatic_Variables:

$?
  Contains the execution status of the last operation

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

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

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

> java -jar foo; $?; $LastExitCode
Unable to access jarfile foo
False
1

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

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

> java -jar foo; $LastExitCode; $?
Unable to access jarfile foo
1
True

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

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

> &{ java -jar foo }; $?; $LastExitCode
Unable to access jarfile foo
True
1

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

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

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

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

От:

> 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

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

> 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
But, the output stream is not a text stream! Redirects are just another syntax for the object pipeline. The error records are objects, so all you have to do is convert the objects on that stream to strings before redirecting +1
Это верно. Помещение объекта потока в кавычки (& quot; $ _ & quot;) преобразуется в строку.
Эй, люби это2>&1 | %{ "$_" } Обходной путь, я буду этим пользоваться, спасибо. Colonel Panic
Я объединил это с проверкой $ LASTEXITCODE, в противном случае команда считается неудачной:command 2>&1 | %{ "$_" }; if ($LASTEXITCODE -ne 0) { throw "Command returned exit code $LASTEXITCODE" } else { Write-Host "Command finished successfully" }
в версии 2.02>&1 | %{ "$_" } не работает, если для ErrorActionPref установлено значение «stop». Есть ли способ запустить команду и переопределить глобальное значение советника?
10

(Note: This is mostly speculation; I rarely use many native commands in PowerShell and others probably know more about PowerShell internals than me)

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

If PowerShell picks up stuff on the standard error stream it will assume an error and throw a NativeCommandError. PowerShell can only pick this up if it monitors the standard error stream. PowerShell ISE has to monitor it, because it is no console application and thus a native console application has no console to write to. This is why in the PowerShell ISE this fails regardless of the 2>&1 redirection operator. The console host will monitor the standard error stream if you use the 2>&1 redirection operator because output on the standard error stream has to be redirected and thus read.

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

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

Вы говорите мне в PowershellISEОбе мои команды взрывают. Теперь я в замешательстве! Colonel Panic
Просто искал спецификацию для этого. Ничего, что могло бы объяснить поведение, которое вы видите. Также немного неясно, что именно является ошибкой для$?.
Я согласен с оценкой Джо и считаю, что поведение PowerShell.exe должно быть улучшено.
Я сделал быстрый скрипт на python, который просто пишет сообщение в stderr консоли и выполнил его сpowershell.exe а также$? было правдой. Сценарий Python был просто импорт системы, а затемsys.stderr.write("Hi There\n"), Ну, это похоже$? Ложно, только если собственный код завершения команды не равен нулю.
Есть ли какие-нибудь документы по Powershell, касающиеся пункта 1? ИМХО, это плохое проектное решение, многие программы выводят отладочную информацию на стандартную ошибку, это не означает, что они потерпели неудачу. Colonel Panic
0

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

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

& $exe $parameters *>&1

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

Поэтому я попытался обернуть его в выражение-выражение, чтобы вызвать 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 }
    }
}
Существует активная ошибка, открытая об этом поведении с Microsoft с 2011 года:connect.microsoft.com/PowerShell/feedback/details/645954, Лучшее решение для перенаправления stderr и предотвращения ошибок - использовать команду cmd / c & quot; 2 & gt; & amp; 1 & quot;
Увы, эта ссылка для подключения кажется мертвой ....
16

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

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

False

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

Use ($LastExitCode -eq 0) instead of $?
@ piers7 или вы можете записать stderr в переменную (stackoverflow.com/questions/24222088/…), а затем проверить оба$LastExitCode а также$stderr.
Также совет должен бытьUse ($LastExitCode -eq 0) instead of $? for native commands, При запуске сценариев / командлетов PowerShell,$? это путь -$LastExitCode даже не будет установлен, если сценарий не содержит явногоExit-PSSession вызов.
Разумеется, обратное - вы можете получить приложения (особенно консольные сценарии), которые пишут в stdErr, ноdont установить код выхода, и в этом случае выmust обнаружить эти ошибки, используя $ ?. Так что ответ ... это зависит.

Похожие вопросы