$ LastExitCode = 0 pero $? = False en PowerShell. Redirigir stderr a stdout da NativeCommandError

¿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 <<<&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]

(Estoy usando PowerShell v2.)

El ‘ $? ‘variable está documentada en about_Automatic_Variables :

 ps
   Contiene el estado de ejecución de la última operación

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;  ps  $ LastExitCode
 No se puede acceder a jarfile foo
 Falso
 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;  ps
 No se puede acceder a jarfile foo
 1
 Cierto

… entonces $? es verdadero, porque lo último que hizo Shell fue imprimir $LastExitCode al host, que fue exitoso.

Finalmente:

 > & {java -jar foo};  ps  $ LastExitCode
 No se puede acceder a jarfile foo
 Cierto
 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 syntax 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 Hola del error estándar 1> & 2" 2> & 1
 cmd.exe: Hola del error estándar
 En la línea: 1 char: 4
 + cmd y 2 "2> & 1
     + CategoryInfo: NotSpecified: (Hola del error estándar: String) [], RemoteException
     + FullyQualifiedErrorId: NativeCommandError

A:

 > cmd / c "echo Hola del error estándar 1> & 2" 2> & 1 |  ps
 Hola del error estándar

… y con un redireccionamiento a un archivo:

 > cmd / c "echo Hola del error estándar 1> & 2" 2> & 1 |  % {"$ _"} |  tee out.txt
 Hola del error estándar

…o solo:

 > cmd / c "echo Hola del error estándar 1> & 2" 2> & 1 |  % {"$ _"}> out.txt

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 $?

(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.

  1. Si PowerShell recoge elementos en la secuencia de error estándar si asumirá un error y arrojará un NativeCommandError .
  2. PowerShell solo puede detectar esto si monitorea el flujo de error estándar.
  3. 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 .
  4. 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.

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 } } } 
    Intereting Posts