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]
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.
($LastExitCode -eq 0)
au lieu de $?
(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 stderr
etstdout
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
(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.
NativeCommandError
.2>&1
opérateur de redirection.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.
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 }
}
}