Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
La gestion des erreurs fait partie de la vie quand il s’agit d’écrire du code. Nous pouvons souvent vérifier et valider les conditions pour le comportement attendu. Lorsque l’inattendu se produit, nous nous tournons vers la gestion des exceptions. Vous pouvez facilement gérer les exceptions générées par le code d’autres personnes ou vous pouvez générer vos propres exceptions pour que d’autres personnes puissent gérer.
Note
La version originale de cet article est apparue sur le blog écrit par @KevinMarquette. L’équipe PowerShell remercie Kevin de partager ce contenu avec nous. Veuillez consulter son blog à PowerShellExplained.com.
Terminologie de base
Nous devons aborder quelques termes de base avant de passer à celui-ci.
Exception
Une exception est semblable à un événement créé lorsque la gestion normale des erreurs ne peut pas traiter le problème. Essayer de diviser un nombre par zéro ou manquer de mémoire sont des exemples de quelque chose qui crée une exception. Parfois, l’auteur du code que vous utilisez crée des exceptions pour certains problèmes lorsqu’ils se produisent.
Lancer et attraper
Lorsqu’une exception se produit, nous disons qu’une exception est levée. Pour gérer une exception levée, vous devez l’intercepter. Si une exception est levée et qu’elle n’est pas interceptée par quelque chose, le script cesse d’exécuter.
Pile des appels
La pile des appels est la liste des fonctions qui se sont appelées les unes les autres. Lorsqu’une fonction est appelée, elle est ajoutée à la pile ou en haut de la liste. Lorsque la fonction se termine ou renvoie une valeur, elle est supprimée de la pile.
Lorsqu’une exception est levée, cette pile d’appels est vérifiée afin qu'un gestionnaire d'exception l'intercepte.
Erreurs de fin et de non-fin
Une exception est généralement une erreur de fin. Une exception levée est soit interceptée, soit elle met fin à l'exécution actuelle. Par défaut, une erreur non-terminante est générée par Write-Error et ajoute une erreur au flux de sortie sans lever d’exception.
Je signale cela parce que Write-Error et d’autres erreurs sans fin ne déclenchent pas le catch.
Avaler une exception
C’est là que vous interceptez une erreur simplement pour la réprimer. Faites cela avec prudence, car il peut rendre la résolution des problèmes très difficile.
Syntaxe de commande de base
Voici une vue d’ensemble rapide de la syntaxe de gestion des exceptions de base utilisée dans PowerShell.
Jeter
Pour créer notre propre événement d’exception, nous levons une exception avec le throw mot clé.
function Start-Something
{
throw "Bad thing happened"
}
Cela crée une exception d’exécution qui est une erreur de fin. Il est géré par une catch fonction appelante ou quitte le script avec un message comme celui-ci.
PS> Start-Something
Bad thing happened
At line:1 char:1
+ throw "Bad thing happened"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Bad thing happened:String) [], RuntimeException
+ FullyQualifiedErrorId : Bad thing happened
Write-Error -ErrorAction Arrêter
J’ai mentionné que Write-Error ne lève pas d’erreur de fin par défaut. Si vous spécifiez -ErrorAction Stop, Write-Error génère une erreur de fin qui peut être gérée avec un catch.
Write-Error -Message "Houston, we have a problem." -ErrorAction Stop
Merci à Lee Dailey de rappeler l’utilisation -ErrorAction Stop de cette façon.
Applet de commande -ErrorAction Arrêter
Si vous spécifiez -ErrorAction Stop sur n’importe quelle fonction ou applet de commande avancée, elle transforme toutes les Write-Error instructions en erreurs de fin qui arrêtent l’exécution ou qui peuvent être gérées par un catch.
Start-Something -ErrorAction Stop
Pour plus d’informations sur le paramètre ErrorAction , consultez about_CommonParameters. Pour plus d’informations sur la variable $ErrorActionPreference, consultez about_Preference_Variables.
Essayer/intercepter
La façon dont la gestion des exceptions fonctionne dans PowerShell (et dans de nombreux autres langages) est que vous commencez par exécuter une section de code try et si elle génère une erreur, vous pouvez catch la traiter. Voici un exemple rapide.
try
{
Start-Something
}
catch
{
Write-Output "Something threw an exception"
Write-Output $_
}
try
{
Start-Something -ErrorAction Stop
}
catch
{
Write-Output "Something threw an exception or used Write-Error"
Write-Output $_
}
Le catch script s’exécute uniquement s’il existe une erreur de fin. Si l’exécution try est correcte, elle passe au-dessus du catch. Vous pouvez accéder aux informations d'exception dans le bloc catch à l’aide de la variable $_.
Essayer/Enfin
Parfois, vous n’avez pas besoin de gérer une erreur, mais vous avez toujours besoin d’un code pour s’exécuter si une exception se produit ou non. Un finally script le fait exactement.
Examinez cet exemple :
$command = [System.Data.SqlClient.SqlCommand]::new(queryString, connection)
$command.Connection.Open()
$command.ExecuteNonQuery()
$command.Connection.Close()
Chaque fois que vous ouvrez ou vous connectez à une ressource, vous devez le fermer. Si ExecuteNonQuery() jette une exception, la connexion ne sera pas fermée. Voici le même code à l’intérieur d’un try/finally bloc.
$command = [System.Data.SqlClient.SqlCommand]::new(queryString, connection)
try
{
$command.Connection.Open()
$command.ExecuteNonQuery()
}
finally
{
$command.Connection.Close()
}
Dans cet exemple, la connexion est fermée en cas d’erreur. Il est également fermé en l’absence d’erreur. Le finally script s’exécute à chaque fois.
Puisque vous n’interceptez pas l’exception, elle continue à être propagée dans la pile des appels.
Try (Essayer)/Catch (Attraper)/Finally (Enfin)
Il est parfaitement valide d'utiliser catch et finally ensemble. La plupart du temps, vous utiliserez l’un ou l’autre, mais vous pouvez trouver des scénarios où vous utilisez les deux.
$PSItem
Maintenant que nous avons les principes de base, nous pouvons creuser un peu plus loin.
À l’intérieur du catch bloc, il existe une variable automatique ($PSItem ou $_) de type ErrorRecord qui contient les détails relatifs à l’exception. Voici une vue d’ensemble rapide de certaines des propriétés clés.
Pour ces exemples, j’ai utilisé un chemin d’accès ReadAllText non valide pour générer cette exception.
[System.IO.File]::ReadAllText( '\\test\no\filefound.log')
PSItem.ToString()
Cela vous donne le message le plus propre à utiliser dans la journalisation et la sortie générale.
ToString() est automatiquement appelé si $PSItem est placé à l’intérieur d’une chaîne de caractères.
catch
{
Write-Output "Ran into an issue: $($PSItem.ToString())"
}
catch
{
Write-Output "Ran into an issue: $PSItem"
}
$PSItem.InvocationInfo
Cette propriété contient des informations supplémentaires collectées par PowerShell sur la fonction ou le script où l’exception a été levée. Voici l’exemple InvocationInfo d’exception que j’ai créé.
PS> $PSItem.InvocationInfo | Format-List *
MyCommand : Get-Resource
BoundParameters : {}
UnboundArguments : {}
ScriptLineNumber : 5
OffsetInLine : 5
ScriptName : C:\blog\throwerror.ps1
Line : Get-Resource
PositionMessage : At C:\blog\throwerror.ps1:5 char:5
+ Get-Resource
+ ~~~~~~~~~~~~
PSScriptRoot : C:\blog
PSCommandPath : C:\blog\throwerror.ps1
InvocationName : Get-Resource
Les détails importants ici montrent le ScriptName, le Line du code, et l’endroit ScriptLineNumber où l’appel a démarré.
$PSItem.ScriptStackTrace
Cette propriété affiche l’ordre des appels de fonction qui vous ont donné le code où l’exception a été générée.
PS> $PSItem.ScriptStackTrace
at Get-Resource, C:\blog\throwerror.ps1: line 13
at Start-Something, C:\blog\throwerror.ps1: line 5
at <ScriptBlock>, C:\blog\throwerror.ps1: line 18
J’effectue uniquement des appels à des fonctions dans le même script, mais cela serait capable de suivre les appels si plusieurs scripts étaient impliqués.
$PSItem.Exception
Il s’agit de l’exception réelle levée.
$PSItem.Exception.Message
Il s’agit du message général qui décrit l’exception et qui constitue un bon point de départ lors de la résolution des problèmes. La plupart des exceptions ont un message par défaut, mais peuvent également être définies sur quelque chose de personnalisé lorsque l’exception est levée.
PS> $PSItem.Exception.Message
Exception calling "ReadAllText" with "1" argument(s): "The network path was not found."
Il s’agit également du message retourné lors de l’appel de $PSItem.ToString() s’il n’y a pas eu de valeur définie sur ErrorRecord.
$PSItem.Exception.InnerException
Les exceptions peuvent contenir des exceptions internes. C’est souvent le cas lorsque le code que vous appelez intercepte une exception et lève une autre exception. L’exception d’origine est placée dans la nouvelle exception.
PS> $PSItem.Exception.InnerExceptionMessage
The network path was not found.
Je vais revoir cela plus tard quand je parle de lever des exceptions à nouveau.
$PSItem.Exception.StackTrace
Il s'agit de StackTrace pour l'exception. J’ai montré un ScriptStackTrace ci-dessus, mais celui-ci est pour les appels au code managé.
at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean
useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs,
String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32
bufferSize, FileOptions options, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean
checkHost)
at System.IO.StreamReader..ctor(String path, Encoding encoding, Boolean detectEncodingFromByteOrderMarks,
Int32 bufferSize, Boolean checkHost)
at System.IO.File.InternalReadAllText(String path, Encoding encoding, Boolean checkHost)
at CallSite.Target(Closure , CallSite , Type , String )
Vous obtenez uniquement cette trace de pile lorsque l'événement est levé depuis le code managé. J’appelle directement une fonction .NET Framework, donc c'est tout ce que l'on peut voir dans cet exemple. En règle générale, lorsque vous examinez une trace de pile d'appels, vous recherchez l’endroit où votre code s’arrête et où les appels système commencent.
Utilisation d’exceptions
Il existe plus d’exceptions que la syntaxe de base et les propriétés d’exception.
Intercepter les exceptions typées
Vous pouvez être sélectif avec les exceptions que vous interceptez. Les exceptions ont un type et vous pouvez spécifier le type d’exception que vous souhaitez intercepter.
try
{
Start-Something -Path $path
}
catch [System.IO.FileNotFoundException]
{
Write-Output "Could not find $path"
}
catch [System.IO.IOException]
{
Write-Output "IO error with the file: $path"
}
Le type d’exception est vérifié pour chaque catch bloc jusqu’à ce qu’un bloc corresponde à votre exception.
Il est important de se rendre compte que les exceptions peuvent hériter d’autres exceptions. Dans l’exemple ci-dessus, FileNotFoundException hérite de IOException. Donc, si IOException était en premier, il serait appelé à la place. Un seul "catch block" est invoqué même s'il y a plusieurs correspondances.
Si nous avions un System.IO.PathTooLongException, le IOException correspondrait, mais si nous avions un InsufficientMemoryException, alors rien ne l'intercepterait et il remonterait dans la pile.
Capturer plusieurs types à la fois
Il est possible d’intercepter plusieurs types d’exceptions avec la même catch instruction.
try
{
Start-Something -Path $path -ErrorAction Stop
}
catch [System.IO.DirectoryNotFoundException],[System.IO.FileNotFoundException]
{
Write-Output "The path or file was not found: [$path]"
}
catch [System.IO.IOException]
{
Write-Output "IO error with the file: [$path]"
}
Merci Redditor u/Sheppard_Ra pour suggérer cet ajout.
Levée d’exceptions typées
Vous pouvez lever des exceptions typées dans PowerShell. Au lieu d’appeler throw avec une chaîne de caractères :
throw "Could not find: $path"
Utilisez un accélérateur d’exception comme suit :
throw [System.IO.FileNotFoundException] "Could not find: $path"
Mais vous devez spécifier un message lorsque vous le faites de cette façon.
Vous pouvez également créer une instance d’une exception à lever. Le message est facultatif lorsque vous effectuez cette opération, car le système a des messages par défaut pour toutes les exceptions intégrées.
throw [System.IO.FileNotFoundException]::new()
throw [System.IO.FileNotFoundException]::new("Could not find path: $path")
Si vous n’utilisez pas PowerShell 5.0 ou version ultérieure, vous devez utiliser l’ancienne New-Object approche.
throw (New-Object -TypeName System.IO.FileNotFoundException )
throw (New-Object -TypeName System.IO.FileNotFoundException -ArgumentList "Could not find path: $path")
En utilisant une exception typée, vous (ou d’autres) pouvez intercepter l’exception par le type mentionné dans la section précédente.
Write-Error -Exception
Nous pouvons ajouter ces exceptions typées à Write-Error et nous pouvons toujours catch les erreurs par type d’exception. Utilisez Write-Error comme dans ces exemples :
# with normal message
Write-Error -Message "Could not find path: $path" -Exception ([System.IO.FileNotFoundException]::new()) -ErrorAction Stop
# With message inside new exception
Write-Error -Exception ([System.IO.FileNotFoundException]::new("Could not find path: $path")) -ErrorAction Stop
# Pre PS 5.0
Write-Error -Exception ([System.IO.FileNotFoundException]"Could not find path: $path") -ErrorAction Stop
Write-Error -Message "Could not find path: $path" -Exception (New-Object -TypeName System.IO.FileNotFoundException) -ErrorAction Stop
Ensuite, nous pouvons l’attraper comme suit :
catch [System.IO.FileNotFoundException]
{
Write-Log $PSItem.ToString()
}
La grande liste des exceptions .NET
J’ai compilé une liste principale avec l’aide de la communauté Reddit r/PowerShell qui contient des centaines d’exceptions .NET pour compléter ce billet.
Je commence par rechercher dans cette liste des exceptions qui semblent être un bon ajustement pour ma situation. Vous devez essayer d’utiliser des exceptions dans l’espace de noms de base System .
Les exceptions sont des objets
Si vous commencez à utiliser beaucoup d’exceptions typées, n’oubliez pas qu’elles sont des objets. Différentes exceptions ont différents constructeurs et propriétés. Si nous examinons la documentation FileNotFoundException pour System.IO.FileNotFoundException, nous voyons que nous pouvons transmettre un message et un chemin d’accès de fichier.
[System.IO.FileNotFoundException]::new("Could not find file", $path)
Et il a une FileName propriété qui expose ce chemin d’accès de fichier.
catch [System.IO.FileNotFoundException]
{
Write-Output $PSItem.Exception.FileName
}
Vous devez consulter la documentation .NET pour d’autres constructeurs et propriétés d’objet.
Levée d’une exception
Si tout ce que vous allez faire dans votre catch bloc est throw la même exception, ne le faites pas catch . Vous ne devez gérer qu’une catch exception que vous prévoyez de traiter ou d'effectuer une action lorsque cela se produit.
Il existe des moments où vous souhaitez effectuer une action sur une exception, mais lever à nouveau l’exception afin que quelque chose en aval puisse y faire face. Nous pourrions écrire un message ou consigner le problème près de l’endroit où nous le découvrons, mais gérer le problème plus haut dans la pile.
catch
{
Write-Log $PSItem.ToString()
throw $PSItem
}
Il est intéressant de noter que nous pouvons appeler throw depuis l'intérieur de catch et qu’il relance l’exception actuelle.
catch
{
Write-Log $PSItem.ToString()
throw
}
Nous voulons lever à nouveau l’exception pour conserver les informations d’exécution d’origine telles que le script source et le numéro de ligne. Si nous lançons une nouvelle exception à ce stade, elle masque l'origine de l'exception.
Levée d’une nouvelle exception
Si vous interceptez une exception, mais que vous souhaitez en lever une autre, vous devez imbriquer l’exception d’origine à l’intérieur de la nouvelle exception. Cela permet à quelqu’un plus bas dans la pile de l’accéder comme $PSItem.Exception.InnerException.
catch
{
throw [System.MissingFieldException]::new('Could not access field',$PSItem.Exception)
}
$PSCmdlet.ThrowTerminatingError()
La seule chose que je n'aime pas dans l'utilisation de throw pour les exceptions brutes, c'est que le message d'erreur pointe vers l'instruction throw et indique que la ligne correspond à l'endroit du problème.
Unable to find the specified file.
At line:31 char:9
+ throw [System.IO.FileNotFoundException]::new()
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (:) [], FileNotFoundException
+ FullyQualifiedErrorId : Unable to find the specified file.
Le message d’erreur m’indique que mon script est rompu parce que j’ai appelé throw à la ligne 31 est un mauvais message pour que les utilisateurs de votre script puissent voir. Il ne leur dit rien d’utile.
Dexter Dhami a souligné que je peux l’utiliser ThrowTerminatingError() pour corriger cela.
$PSCmdlet.ThrowTerminatingError(
[System.Management.Automation.ErrorRecord]::new(
([System.IO.FileNotFoundException]"Could not find $Path"),
'My.ID',
[System.Management.Automation.ErrorCategory]::OpenError,
$MyObject
)
)
Si nous partons du principe qu’elle ThrowTerminatingError() a été appelée à l’intérieur d’une fonction appelée Get-Resource, il s’agit de l’erreur que nous verrons.
Get-Resource : Could not find C:\Program Files (x86)\Reference
Assemblies\Microsoft\Framework\.NETPortable\v4.6\System.IO.xml
At line:6 char:5
+ Get-Resource -Path $Path
+ ~~~~~~~~~~~~
+ CategoryInfo : OpenError: (:) [Get-Resource], FileNotFoundException
+ FullyQualifiedErrorId : My.ID,Get-Resource
Voyez-vous comment elle pointe vers la Get-Resource fonction comme source du problème ? Cela indique à l’utilisateur quelque chose d’utile.
Étant donné qu'il $PSItem s'agit d'un ErrorRecord, nous pouvons également utiliser ThrowTerminatingError pour relancer.
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
Cela modifie la source de l’erreur vers l’applet de commande et cache les détails internes de votre fonction aux utilisateurs de votre applet.
Essayer peut créer des erreurs fatales
Kirk Munro souligne que certaines exceptions ne terminent que les erreurs lorsqu’elles sont exécutées à l’intérieur d’un try/catch bloc. Voici l’exemple qu’il m’a donné qui génère une division par exception de runtime zéro.
function Start-Something { 1/(1-1) }
Appelez-le comme suit pour le voir générer l’erreur et afficher le message.
&{ Start-Something; Write-Output "We did it. Send Email" }
Mais en plaçant ce même code à l’intérieur d’un try/catch, nous voyons quelque chose d’autre se produire.
try
{
&{ Start-Something; Write-Output "We did it. Send Email" }
}
catch
{
Write-Output "Notify Admin to fix error and send email"
}
Nous voyons que l’erreur devient une erreur de fin et ne génère pas le premier message. Ce que je n’aime pas à ce sujet est que vous pouvez avoir ce code dans une fonction et qu’il agit différemment si quelqu’un utilise un try/catch.
Je n'ai pas rencontré de problèmes par moi-même, mais c'est un cas particulier qu'il faut connaître.
$PSCmdlet.ThrowTerminatingError() dans try/catch
L'une des nuances de $PSCmdlet.ThrowTerminatingError() est qu'elle crée une erreur bloquante dans votre applet de commande, mais qu'elle devient une erreur non-bloquante après avoir quitté votre applet de commande. Cela laisse la charge à l’appelant de votre fonction de décider comment gérer l’erreur. Ils peuvent le transformer en erreur fatale en utilisant -ErrorAction Stop ou en l’appelant depuis un try{...}catch{...}.
Modèles de fonction publique
Un dernier point de ma conversation avec Kirk Munro était qu’il place un try{...}catch{...} autour de chaque bloc begin, process et end dans toutes ses fonctions avancées. Dans ces blocs catch génériques, il a une seule ligne utilisant $PSCmdlet.ThrowTerminatingError($PSItem) pour traiter toutes les exceptions laissant ses fonctions.
function Start-Something
{
[CmdletBinding()]
param()
process
{
try
{
...
}
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
}
Étant donné que tout se trouve dans une try déclaration au sein de ses fonctions, tout agit de façon cohérente. Cela donne également des erreurs propres à l’utilisateur final qui masquent le code interne par rapport à l'erreur générée.
Piège
Je me suis concentré sur l’aspect try/catch des exceptions. Mais il y a une fonctionnalité héritée que j’ai besoin de mentionner avant d’encapsuler cela.
Un trap est placé dans un script ou une fonction pour intercepter toutes les exceptions qui se produisent dans cette étendue. Lorsqu’une exception se produit, le code dans le trap code est exécuté, puis le code normal se poursuit. Si plusieurs exceptions se produisent, le piège est appelé de façon répétée.
trap
{
Write-Log $PSItem.ToString()
}
throw [System.Exception]::new('first')
throw [System.Exception]::new('second')
throw [System.Exception]::new('third')
Je n’ai jamais adopté personnellement cette approche, mais je peux voir la valeur dans les scripts d’administration ou de contrôleur qui journalssent toutes les exceptions, puis continuent à s’exécuter.
Remarques de clôture
L’ajout d’une gestion appropriée des exceptions à vos scripts les rend non seulement plus stables, mais facilite également la résolution de ces exceptions.
J’ai passé beaucoup de temps à parler throw parce qu’il s’agit d’un concept fondamental lors de la gestion des exceptions. PowerShell nous a également donné Write-Error, qui gère toutes les situations où vous utiliseriez throw. Donc, ne pensez pas que vous devez utiliser throw après avoir lu cela.
Maintenant que j’ai pris le temps d’écrire sur la gestion des exceptions en détail, je vais passer à l’utilisation Write-Error -Stop pour générer des erreurs dans mon code. Je vais également suivre le conseil de Kirk et faire de ThrowTerminatingError mon gestionnaire d’exceptions goto pour chaque fonction.