web-dev-qa-db-fra.com

$ LastExitCode = 0, mais $? = False dans PowerShell. Rediriger stderr vers stdout donne NativeCommandError

Pourquoi PowerShell montre-t-il le comportement surprenant dans le deuxième exemple ci-dessous?

Tout d'abord, un exemple de comportement sain:

PS C:\> & cmd /c "echo Hello from standard error 1>&2"; echo "`$LastExitCode=$LastExitCode and `$?=$?"
Hello from standard error
$LastExitCode=0 and $?=True

Pas de surprises. J'imprime un message d'erreur standard (en utilisant cmd's echo). J'inspecte les variables $? et $LastExitCode. Ils sont respectivement égaux à True et 0, comme prévu.

Cependant, si je demande à PowerShell de rediriger l'erreur standard vers la sortie standard via la première commande, j'obtiens une 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

Ma première question, pourquoi le NativeCommandError?

Deuxièmement, pourquoi $? False lorsque cmd s'est exécuté avec succès et $LastExitCode est 0? La documentation de PowerShell sur les variables automatiques ne définit pas explicitement $?. J'ai toujours supposé que c'était vrai si et seulement si $LastExitCode est 0, mais mon exemple contredit cela.


Voici comment j'ai découvert ce comportement dans le monde réel (simplifié). C'est vraiment FUBAR. J'appelais un script PowerShell d'un autre. Le script intérieur:

cmd /c "echo Hello from standard error 1>&2"
if (! $?)
{
    echo "Job failed. Sending email.."
    exit 1
}
# Do something else

Exécuter cela simplement comme .\job.ps1, cela fonctionne bien et aucun e-mail n'est envoyé. Cependant, je l'appelais depuis un autre script PowerShell, en me connectant à un fichier .\job.ps1 2>&1 > log.txt. Dans ce cas, un email est envoyé! Ce que vous faites en dehors du script avec le flux d'erreurs affecte le comportement interne du script. L'observation d'un phénomène change le résultat. Cela ressemble à de la physique quantique plutôt qu'à des scripts!

[De façon intéressante: .\job.ps1 2>&1 peut exploser ou non selon l'endroit où vous l'exécutez]

42
Colonel Panic

Ce bogue est une conséquence imprévue de la conception normative de PowerShell pour la gestion des erreurs, il est donc très probable qu'il ne sera jamais corrigé. Si votre script ne joue qu'avec d'autres scripts PowerShell, vous êtes en sécurité. Cependant, si votre script interagit avec des applications du grand monde, ce bogue peut mordre.

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

False

Je t'ai eu! Pourtant, après quelques grattages douloureux, vous n'oublierez jamais la leçon.

Utilisation ($LastExitCode -eq 0) au lieu de $?

16
Colonel Panic

(J'utilise PowerShell v2.)

Le '$? 'variable est documentée dans about_Automatic_Variables:

 $? 
 Contient l'état d'exécution de la dernière opération 

Il s'agit de la dernière opération PowerShell, par opposition à la dernière commande externe, qui est ce que vous obtenez dans $LastExitCode.

Dans votre exemple, $LastExitCode est 0, car la dernière commande externe était cmd, ce qui était réussi en faisant écho à du texte. Mais le 2>&1 entraîne la conversion des messages en stderr en enregistrements d'erreur dans le flux de sortie, ce qui indique à PowerShell qu'une erreur s'est produite lors de la dernière opération, provoquant $? être False.

Pour illustrer cela un peu plus, considérez ceci:

> Java -jar foo; $ ?; $ LastExitCode 
 Impossible d'accéder au fichier jar foo 
 Faux 
 1 

$LastExitCode est 1, car il s'agissait du code de sortie de Java.exe. $? est False, car la toute dernière chose que Shell a échoué.

Mais si je ne fais que les changer:

> Java -jar foo; $ LastExitCode; $? 
 Impossible d'accéder au fichier jar foo 
 1 
 Vrai 

... puis $? est True, car la dernière chose que Shell a faite a été d'imprimer $LastExitCode à l'hôte, qui a réussi.

Finalement:

> & {Java -jar foo}; $ ?; $ LastExitCode 
 Impossible d'accéder au fichier jar foo 
 Vrai 
 1 

... ce qui semble un peu contre-intuitif, mais $? est Vrai maintenant, car l'exécution du bloc de script était réussie, même si la commande exécutée à l'intérieur ne l'était pas.


Retour au 2>&1 rediriger .... qui provoque un enregistrement d'erreur dans le flux de sortie, ce qui donne ce blob de longue haleine sur le NativeCommandError. Le Shell vide tout le dossier d'erreur.

Cela peut être particulièrement gênant lorsque tout ce que vous voulez faire est de canaliser stderretstdout ensemble afin qu'ils puissent être combinés dans un fichier journal ou quelque chose. Qui veut que PowerShell se joint à son fichier journal ??? Si je fais ant build 2>&1 >build.log, puis toutes les erreurs qui vont à stderr ont le nosy $ 0,02 de PowerShell, au lieu d'obtenir des messages d'erreur propres dans mon fichier journal.

Mais, le flux de sortie n'est pas un flux texte! Les redirections ne sont qu'une autre syntaxe pour le pipeline object. Les enregistrements d'erreur sont des objets, donc tout ce que vous avez à faire est de convertir les objets de ce flux en chaînes avant de rediriger:

De:

> cmd/c "echo Bonjour de l'erreur standard 1> & 2" 2> & 1 
 cmd.exe: Bonjour de l'erreur standard 
 À la ligne: 1 caractère: 4 
 + cmd & 2 "2> & 1 
 + CategoryInfo: NotSpecified: (Bonjour de l'erreur standard: String) [], RemoteException 
 + FullyQualifiedErrorId: NativeCommandError 

À:

> cmd/c "echo Bonjour de l'erreur standard 1> & 2" 2> & 1 | % {"$ _"} 
 Bonjour de l'erreur standard 

... et avec une redirection vers un fichier:

> cmd/c "echo Bonjour de l'erreur standard 1> & 2" 2> & 1 | % {"$ _"} | tee out.txt 
 Bonjour de l'erreur standard 

...ou juste:

> cmd/c "echo Bonjour de l'erreur standard 1> & 2" 2> & 1 | % {"$ _"}> out.txt 
72
Droj

(Remarque: il s'agit principalement de spéculations; j'utilise rarement de nombreuses commandes natives dans PowerShell et d'autres en savent probablement plus que moi sur les composants internes de PowerShell)

Je suppose que vous avez trouvé une différence dans l'hôte de la console PowerShell.

  1. Si PowerShell récupère des éléments sur le flux d'erreurs standard, il supposera une erreur et lancera un NativeCommandError.
  2. PowerShell ne peut détecter cela que s'il surveille le flux d'erreur standard.
  3. PowerShell ISE doit le surveiller, car ce n'est pas une application console et donc une application console native n'a pas de console sur laquelle écrire. C'est pourquoi dans PowerShell ISE, cela échoue quel que soit le 2>&1 opérateur de redirection.
  4. La console Host will surveillera le flux d'erreur standard si vous utilisez le 2>&1 opérateur de redirection car la sortie du flux d'erreur standard doit être redirigée et donc lue.

Je suppose ici que la console PowerShell Host est paresseuse et remet simplement la console native commande la console si elle n'a pas besoin de faire de traitement sur leur sortie.

Je pense vraiment que c'est un bogue, car PowerShell se comporte différemment selon l'application hôte.

10
Joey

Pour moi, c'était un problème avec ErrorActionPreference. Lors de l'exécution depuis ISE, j'ai défini $ ErrorActionPreference = "Stop" dans les premières lignes et qui interceptait tout événement avec *> & 1 ajouté comme paramètres à l'appel.

J'ai donc d'abord eu cette ligne:

& $exe $parameters *>&1

Ce qui, comme je l'ai dit, n'a pas fonctionné car j'avais $ ErrorActionPreference = "Stop" plus tôt dans le fichier (ou il peut être défini globalement dans le profil pour que l'utilisateur lance le script).

J'ai donc essayé de l'envelopper dans Invoke-Expression pour forcer ErrorAction:

Invoke-Expression -Command "& `"$exe`" $parameters *>&1" -ErrorAction Continue

Et cela ne fonctionne pas non plus.

J'ai donc dû recourir au piratage avec une erreur temporaire prioritaire prioritaire:

$old_error_action_preference = $ErrorActionPreference

try
{
    $ErrorActionPreference = "Continue"
    & $exe $parameters *>&1
}
finally
{
    $ErrorActionPreference = $old_error_action_preference
}

Ce qui fonctionne pour moi.

Et je l'ai enveloppé dans une fonction:

<#
    .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 }
    }
}
0
Michael Logutov