windows - script - $ LastExitCode=0 pero $?=False en PowerShell. Redirigir stderr a stdout da NativeCommandError
script en powershell (4)
(Estoy usando PowerShell v2.)
El '' $?
''variable está documentada en about_Automatic_Variables
:
$? Contains the execution status of the last operation
Esto se refiere a la operación más reciente de PowerShell, a diferencia del último comando externo, que es lo que obtienes en $LastExitCode
.
En su ejemplo, $LastExitCode
es 0, porque el último comando externo fue cmd
, que tuvo éxito al hacer eco de algún texto. Pero el 2>&1
hace que los mensajes a stderr
se conviertan en registros de error en la secuencia de salida, que le dice a PowerShell que hubo un error durante la última operación , lo que provocó $?
ser False
Para ilustrar esto un poco más, considere esto:
> java -jar foo; $?; $LastExitCode Unable to access jarfile foo False 1
$LastExitCode
es 1, porque ese era el código de salida de java.exe. $?
es False, porque la última cosa que falló el shell fue
Pero si todo lo que hago es cambiarlos:
> java -jar foo; $LastExitCode; $? Unable to access jarfile foo 1 True
... entonces $?
es verdadero, porque lo último que hizo Shell fue imprimir $LastExitCode
al host, que fue exitoso.
Finalmente:
> &{ java -jar foo }; $?; $LastExitCode Unable to access jarfile foo True 1
... que parece un poco contra-intuitivo, pero $?
es verdadero ahora, porque la ejecución del bloque de script fue exitosa , incluso si el comando ejecutado dentro de él no.
Volviendo al redireccionamiento 2>&1
.... que hace que un registro de error vaya en la secuencia de salida, que es lo que le da a ese blob largo y sinuoso acerca del NativeCommandError
. El shell está volcando todo el registro de errores.
Esto puede ser especialmente molesto cuando todo lo que quiere hacer es stderr
tuberías stderr
y stdout
juntas para que puedan combinarse en un archivo de registro o algo así. ¿Quién quiere que PowerShell se meta en su archivo de registro? Si hago ant build 2>&1 >build.log
, entonces cualquier error que vaya a stderr
tiene el codificador de $ 0.02 de PowerShell, en lugar de obtener mensajes de error limpios en mi archivo de registro.
¡Pero el flujo de salida no es una secuencia de texto ! Los redireccionamientos son solo otra sintaxis para la canalización de objetos . Los registros de errores son objetos, por lo que todo lo que tienes que hacer es convertir los objetos en esa secuencia en cadenas antes de redirigir:
De:
> 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
A:
> cmd /c "echo Hello from standard error 1>&2" 2>&1 | %{ "$_" } Hello from standard error
... y con un redireccionamiento a un archivo:
> cmd /c "echo Hello from standard error 1>&2" 2>&1 | %{ "$_" } | tee out.txt Hello from standard error
...o solo:
> cmd /c "echo Hello from standard error 1>&2" 2>&1 | %{ "$_" } >out.txt
¿Por qué Powershell muestra el comportamiento sorprendente en el segundo ejemplo a continuación?
Primero, un ejemplo de comportamiento sano:
PS C:/> & cmd /c "echo Hello from standard error 1>&2"; echo "`$LastExitCode=$LastExitCode and `$?=$?"
Hello from standard error
$LastExitCode=0 and $?=True
No hay sorpresas. Imprimo un mensaje a un error estándar (usando el echo
cmd
). Inspecciono las variables $?
y $LastExitCode
. Son iguales a True y 0 respectivamente, como se esperaba.
Sin embargo, si le pido a PowerShell que redirija el error estándar a la salida estándar sobre el primer comando, obtengo un 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
Mi primera pregunta, ¿por qué NativeCommandError?
En segundo lugar, ¿por qué es $?
¿Falso cuando cmd
ejecutó correctamente y $LastExitCode
es 0? La documentación de PowerShell sobre variables automáticas no define explícitamente $?
. Siempre supuse que es verdadero si y solo si $LastExitCode
es 0, pero mi ejemplo contradice eso.
Así es como me encontré con este comportamiento en el mundo real (simplificado). Realmente es FUBAR. Estaba llamando un script de PowerShell desde otro. La secuencia de comandos interna:
cmd /c "echo Hello from standard error 1>&2"
if (! $?)
{
echo "Job failed. Sending email.."
exit 1
}
# Do something else
Ejecutando esto simplemente como ./job.ps1
, funciona bien y no se envía ningún correo electrónico. Sin embargo, lo llamaba desde otra secuencia de comandos de PowerShell, ./job.ps1 2>&1 > log.txt
sesión en un archivo ./job.ps1 2>&1 > log.txt
. En este caso, se envía un correo electrónico Lo que haces fuera del script con el flujo de errores afecta el comportamiento interno del script. Observar un fenómeno cambia el resultado. ¡Esto se siente como física cuántica en lugar de guiones!
[Curiosamente:. ./job.ps1 2>&1
puede o no explotar dependiendo de dónde lo ejecute]
Este error es una consecuencia imprevista del diseño prescriptivo de PowerShell para el manejo de errores, por lo que es probable que nunca se solucione. Si su script solo se reproduce con otros scripts de PowerShell, está seguro. Sin embargo, si su script interactúa con aplicaciones del mundo, este error puede afectar.
PS> nslookup microsoft.com 2>&1 ; echo $?
False
Gotcha! Aún así, después de algunos rasguños dolorosos, nunca olvidará la lección.
Use ($LastExitCode -eq 0)
lugar de $?
Para mí fue un problema con ErrorActionPreference. Al ejecutar desde ISE he establecido $ ErrorActionPreference = "Stop" en las primeras líneas y eso fue interceptar todo evento con *> & 1 agregado como parámetros a la llamada.
Entonces primero tuve esta línea:
& $exe $parameters *>&1
Lo cual, como he dicho, no funcionó porque tenía $ ErrorActionPreference = "Stop" antes en el archivo (o puede establecerse globalmente en el perfil para el usuario que inicia el script).
Así que he tratado de envolverlo en Invoke-Expression para forzar ErrorAction:
Invoke-Expression -Command "& `"$exe`" $parameters *>&1" -ErrorAction Continue
Y esto tampoco funciona.
Así que tuve que recurrir a hackear con la anulación temporal de ErrorActionPreference:
$old_error_action_preference = $ErrorActionPreference
try
{
$ErrorActionPreference = "Continue"
& $exe $parameters *>&1
}
finally
{
$ErrorActionPreference = $old_error_action_preference
}
Lo cual está funcionando para mí.
Y lo he envuelto en una función:
<#
.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 }
}
}
(Nota: Esto es principalmente especulación; rara vez uso muchos comandos nativos en PowerShell y otros probablemente conozcan más acerca de los componentes internos de PowerShell que yo)
Supongo que encontró una discrepancia en el host de la consola de PowerShell.
- Si PowerShell recoge elementos en la secuencia de error estándar si asumirá un error y arrojará un
NativeCommandError
. - PowerShell solo puede detectar esto si monitorea el flujo de error estándar.
- PowerShell ISE tiene que supervisarlo, ya que no es una aplicación de consola y, por lo tanto, una aplicación de consola nativa no tiene consola para escribir. Esta es la razón por la cual en el PowerShell ISE esto falla independientemente del operador de redirección
2>&1
. - El host de la consola supervisará la secuencia de error estándar si usa el operador de redirección
2>&1
porque la salida en la secuencia de error estándar debe redirigirse y leerse.
Supongo que el host de PowerShell de la consola es flojo y que la consola nativa solo ordena la consola si no necesita hacer ningún procesamiento en su salida.
Realmente creo que esto es un error porque PowerShell se comporta de manera diferente según la aplicación de host.