$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. Как и ожидалось, они равны 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, но мой пример противоречит этому.


вот как я столкнулся с этим поведением в реальном мире (упрощенном). Это действительно ФУБАР. Я вызывал один сценарий 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. В этом случае отправляется электронное письмо! То, что вы делаете вне скрипта с потоком ошибок, влияет на внутреннее поведение скрипта. наблюдение за явлением меняет результат. это похоже на quantum физика, а не сценарии!

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

4 ответов


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

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

$?
  Contains the execution status of the last operation

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

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

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

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

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

но если все, что я сделать, это включить их:

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

... тогда $? is Верно, потому что последнее, что сделала оболочка, это напечатала $LastExitCode хосту, который был успешным.

и наконец:

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

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


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

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

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

от:

> 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

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

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

False

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

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


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

Я думаю, вы нашли несоответствие в Хосте консоли PowerShell.

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

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

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


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

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

& $exe $parameters *>&1

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

поэтому я попытался оберните его в 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 }
    }
}