Všechno, co jste chtěli vědět o výjimkách
Zpracování chyb je jen součástí života, pokud jde o psaní kódu. U očekávaného chování můžeme často kontrolovat a ověřovat podmínky. Když dojde k neočekávanému problému, obrátíme se na zpracování výjimek. Výjimky vygenerované kódem jiných lidí můžete snadno zpracovat nebo můžete vygenerovat vlastní výjimky, které můžou zpracovat jiní uživatelé.
Poznámka:
Původní verze tohoto článku se objevila na blogu napsané @KevinMarquette. Tým PowerShellu děkujeme Kevinovi za sdílení tohoto obsahu s námi. Prosím, podívejte se na jeho blog na PowerShellExplained.com.
Základní terminologie
Než se do toho pustíme, musíme se pokrýt některými základními pojmy.
Výjimka
Výjimka je podobná události, která se vytvoří, když normální zpracování chyb nedokáže problém vyřešit. Pokus o dělení čísla nulou nebo nedostatkem paměti jsou příklady něčeho, co vytvoří výjimku. Někdy autor kódu, který používáte, vytvoří výjimky pro určité problémy, když k nim dojde.
Házení a zachycení
Když dojde k výjimce, říkáme, že je vyvolán výjimka. Pokud chcete zpracovat vyvolánou výjimku, musíte ji zachytit. Pokud dojde k vyvolání výjimky a něco ji nezachytí, skript se zastaví.
Zásobník volání
Zásobník volání je seznam funkcí, které se vzájemně volají. Když je volána funkce, přidá se do zásobníku nebo do horní části seznamu. Když funkce ukončí nebo vrátí, odebere se ze zásobníku.
Při vyvolání výjimky se zásobník volání zkontroluje, aby obslužná rutina výjimky ho zachytila.
Ukončování a neukončující chyby
Výjimkou je obvykle ukončující chyba. Vyvolána výjimka je buď zachycena, nebo ukončí aktuální spuštění. Ve výchozím nastavení se vygeneruje Write-Error
neukončující chyba a přidá do výstupního datového proudu chybu bez vyvolání výjimky.
Na to upozornit, protože Write-Error
a další neukončující chyby neaktivují catch
.
Polykání výjimky
To je, když zachytíte chybu a potlačíte ji. Postupujte opatrně, protože může být velmi obtížné řešit problémy.
Základní syntaxe příkazů
Tady je rychlý přehled základní syntaxe zpracování výjimek, která se používá v PowerShellu.
Throw
Abychom vytvořili vlastní událost výjimky, vyvoláme výjimku s klíčovým slovem throw
.
function Start-Something
{
throw "Bad thing happened"
}
Tím se vytvoří výjimka za běhu, která je ukončující chybou. catch
Zpracovává ji volající funkce nebo ukončuje skript zprávou, jako je tato.
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 -Error Stop
Zmínil jsem se, že Write-Error
ve výchozím nastavení nevyvolá ukončující chybu. Pokud zadáte -ErrorAction Stop
, Write-Error
vygeneruje ukončující chybu, kterou lze zpracovat pomocí catch
.
Write-Error -Message "Houston, we have a problem." -ErrorAction Stop
Děkuji Lee Dailey za připomenutí, že tímto způsobem používáte -ErrorAction Stop
.
Rutina -ErrorAction Stop
Pokud zadáte -ErrorAction Stop
jakoukoli pokročilou funkci nebo rutinu, změní se všechny Write-Error
příkazy na ukončující chyby, které zastaví provádění nebo které může zpracovat catch
.
Start-Something -ErrorAction Stop
Další informace o parametru ErrorAction najdete v tématu about_CommonParameters. Další informace o $ErrorActionPreference
proměnné najdete v tématu about_Preference_Variables.
Try/Catch
Způsob, jakým funguje zpracování výjimek v PowerShellu (a mnoha dalších jazycích), je to, že jste nejprve try
část kódu a pokud vyvolá chybu, můžete ji použít catch
. Tady je rychlá ukázka.
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 $_
}
Skript catch
se spustí jenom v případě, že dojde k ukončovací chybě. Pokud se try
spustí správně, přeskočí se přes catch
. K informacím o výjimce catch
v bloku můžete přistupovat pomocí $_
proměnné.
Try/Finally
Někdy nemusíte zpracovávat chybu, ale přesto potřebujete nějaký kód ke spuštění, pokud dojde k výjimce nebo ne. Skript finally
to dělá přesně tak.
Podívejte se na tento příklad:
$command = [System.Data.SqlClient.SqlCommand]::New(queryString, connection)
$command.Connection.Open()
$command.ExecuteNonQuery()
$command.Connection.Close()
Kdykoli otevřete prostředek nebo se k němu připojíte, měli byste ho zavřít. Pokud vyvolá ExecuteNonQuery()
výjimku, připojení se nezavře. Tady je stejný kód uvnitř try/finally
bloku.
$command = [System.Data.SqlClient.SqlCommand]::New(queryString, connection)
try
{
$command.Connection.Open()
$command.ExecuteNonQuery()
}
finally
{
$command.Connection.Close()
}
V tomto příkladu se připojení zavře, pokud dojde k chybě. Pokud nedojde k chybě, zavře se také. Skript finally
se spustí pokaždé.
Vzhledem k tomu, že výjimku nezachytíte, stále se rozšíří do zásobníku volání.
Try/Catch/Finally
Je naprosto platné používat catch
a finally
společně. Ve většině případů použijete jednu nebo druhou, ale můžete najít scénáře, ve kterých používáte obojí.
$PSItem
Teď, když jsme získali základní informace, můžeme se trochu hlouběji ponořit.
catch
Uvnitř bloku je automatická proměnná ($PSItem
nebo$_
) typuErrorRecord
, která obsahuje podrobnosti o výjimce. Tady je rychlý přehled některých klíčových vlastností.
V těchto příkladech jsem k vygenerování ReadAllText
této výjimky použil neplatnou cestu.
[System.IO.File]::ReadAllText( '\\test\no\filefound.log')
PSItem.ToString()
Tím získáte nejčistší zprávu, která se použije při protokolování a obecném výstupu. ToString()
je automaticky volána, pokud $PSItem
je umístěn uvnitř řetězce.
catch
{
Write-Output "Ran into an issue: $($PSItem.ToString())"
}
catch
{
Write-Output "Ran into an issue: $PSItem"
}
$PSItem.InvocationInfo
Tato vlastnost obsahuje další informace shromážděné prostředím PowerShell o funkci nebo skriptu, kde byla vyvolána výjimka. Tady je InvocationInfo
ukázka výjimky, kterou jsem vytvořil.
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
Důležité podrobnosti zde ukazují ScriptName
, kód Line
a ScriptLineNumber
místo, kde bylo vyvolání zahájeno.
$PSItem.ScriptStackTrace
Tato vlastnost zobrazuje pořadí volání funkcí, která vás dostala do kódu, kde byla vygenerována výjimka.
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
Volám jenom funkce ve stejném skriptu, ale to by sledovalo volání, pokud by bylo zapojeno více skriptů.
$PSItem.Exception
Toto je skutečná výjimka, která byla vyvolán.
$PSItem.Exception.Message
Toto je obecná zpráva, která popisuje výjimku a je dobrým výchozím bodem při řešení potíží. Většina výjimek má výchozí zprávu, ale při vyvolání výjimky je také možné ji nastavit na něco vlastního.
PS> $PSItem.Exception.Message
Exception calling "ReadAllText" with "1" argument(s): "The network path was not found."
Toto je také zpráva vrácená při volání $PSItem.ToString()
, pokud nebyla v sadě ErrorRecord
.
$PSItem.Exception.InnerException
Výjimky můžou obsahovat vnitřní výjimky. To je často případ, kdy kód, který voláte, zachytí výjimku a vyvolá jinou výjimku. Původní výjimka se umístí do nové výjimky.
PS> $PSItem.Exception.InnerExceptionMessage
The network path was not found.
Později se k tomu znovu připojím, když mluvím o opětovném vyvolání výjimek.
$PSItem.Exception.StackTrace
Toto je StackTrace
výjimka. Ukázal ScriptStackTrace
jsem výše, ale tohle je pro volání spravovaného kódu.
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 )
Toto trasování zásobníku získáte pouze při vyvolání události ze spravovaného kódu. Volám přímo funkci rozhraní .NET Framework, takže to je vše, co můžeme vidět v tomto příkladu. Obecně platí, že když se díváte na trasování zásobníku, hledáte, kde se váš kód zastaví a začnou volání systému.
Práce s výjimkami
Existuje více výjimek než základní syntaxe a vlastnosti výjimky.
Zachytávání typed výjimek
Můžete být selektivní s výjimkami, které zachytíte. Výjimky mají typ a můžete zadat typ výjimky, kterou chcete zachytit.
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"
}
Typ výjimky se kontroluje pro každý catch
blok, dokud se nenajde, který odpovídá vaší výjimce.
Je důležité si uvědomit, že výjimky můžou dědit z jiných výjimek. V příkladu výše dědí FileNotFoundException
z IOException
. Takže pokud IOException
by to bylo první, volalo by se místo toho. Vyvolá se pouze jeden blok zachycení, i když existuje více shod.
Pokud bychom měli System.IO.PathTooLongException
, to IOException
by se shodovalo, ale pokud bychom měli InsufficientMemoryException
, nic by ho nechytilo a rozšířilo by se do zásobníku.
Zachycení více typů najednou
Pomocí stejného catch
příkazu je možné zachytit více typů výjimek.
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]"
}
Děkuji Redditor u/Sheppard_Ra
za navržení tohoto doplňku.
Vyvolání typed výjimek
V PowerShellu můžete vyvolat zadané výjimky. Místo volání throw
s řetězcem:
throw "Could not find: $path"
Použijte akcelerátor výjimek podobný tomuto:
throw [System.IO.FileNotFoundException] "Could not find: $path"
Když to ale uděláte, musíte zadat zprávu.
Můžete také vytvořit novou instanci výjimky, která se vyvolá. Tato zpráva je volitelná, protože systém obsahuje výchozí zprávy pro všechny předdefinované výjimky.
throw [System.IO.FileNotFoundException]::new()
throw [System.IO.FileNotFoundException]::new("Could not find path: $path")
Pokud nepoužíváte PowerShell 5.0 nebo novější, musíte použít starší New-Object
přístup.
throw (New-Object -TypeName System.IO.FileNotFoundException )
throw (New-Object -TypeName System.IO.FileNotFoundException -ArgumentList "Could not find path: $path")
Když použijete zatypovanou výjimku, můžete ji (nebo jiné) zachytit podle typu, jak je uvedeno v předchozí části.
Chyba zápisu – výjimka
K těmto typem výjimek můžeme přidat tyto typy výjimek Write-Error
a přesto catch
můžeme chyby podle typu výjimky. Použijte Write-Error
například v těchto příkladech:
# 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
Pak ho můžeme zachytit takto:
catch [System.IO.FileNotFoundException]
{
Write-Log $PSItem.ToString()
}
Velký seznam výjimek .NET
Zkompiloval jsem hlavní seznam s pomocí komunity Reddit r/PowerShell
, která obsahuje stovky výjimek .NET, které doplňují tento příspěvek.
Začnu hledáním v tomto seznamu hledat výjimky, které se cítí jako by byly vhodné pro mou situaci. Měli byste se pokusit použít výjimky v základním System
oboru názvů.
Výjimky jsou objekty.
Pokud začnete používat velké množství typed výjimek, mějte na paměti, že se jedná o objekty. Různé výjimky mají různé konstruktory a vlastnosti. Pokud se podíváme na dokumentaci FileNotFoundException pro System.IO.FileNotFoundException
, vidíme, že můžeme předat zprávu a cestu k souboru.
[System.IO.FileNotFoundException]::new("Could not find file", $path)
A má FileName
vlastnost, která zveřejňuje cestu k souboru.
catch [System.IO.FileNotFoundException]
{
Write-Output $PSItem.Exception.FileName
}
Další konstruktory a vlastnosti objektů byste měli vyhledat v dokumentaci k .NET.
Opětovné vyvolání výjimky
Pokud v bloku uděláte všechno, co uděláte catch
, je throw
stejná výjimka, pak catch
ne. Měli byste pouze catch
výjimku, kterou plánujete zpracovat nebo provést nějakou akci, když k ní dojde.
Existují časy, kdy chcete provést akci na výjimce, ale znovu vyvolat výjimku, aby se s ní mohlo vypořádat něco podřízeného. Mohli bychom napsat zprávu nebo zaznamenat problém blízko místa, kde ho zjistíme, ale problém dál zpracovat v zásobníku.
catch
{
Write-Log $PSItem.ToString()
throw $PSItem
}
Zajímavé je, že můžeme volat throw
z uvnitř catch
a znovu vyvolá aktuální výjimku.
catch
{
Write-Log $PSItem.ToString()
throw
}
Chceme znovu vyvolat výjimku, aby se zachovaly původní informace o spuštění, jako je zdrojový skript a číslo řádku. Pokud v tuto chvíli vyvoláme novou výjimku, skryje místo, kde byla výjimka spuštěna.
Opětovné vyvolání nové výjimky
Pokud zachytíte výjimku, ale chcete vyvolat jinou výjimku, měli byste původní výjimku vnořit do nové výjimky. To umožňuje, aby někdo v zásobníku přistupoval jako .$PSItem.Exception.InnerException
catch
{
throw [System.MissingFieldException]::new('Could not access field',$PSItem.Exception)
}
$PSCmdlet.ThrowTerminatingError()
Jedna věc, kterou nemám rád o použití throw
pro nezpracované výjimky je, že chybová zpráva ukazuje na throw
příkaz a indikuje, že řádek je místo, kde je problém.
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.
Chybová zpráva mi řekne, že můj skript je poškozený, protože jsem volal throw
na řádku 31 je špatná zpráva pro uživatele vašeho skriptu vidět. Nic užitečného jim to neřekne.
Dexter Dhami ukázal, že to mohu použít ThrowTerminatingError()
k nápravě.
$PSCmdlet.ThrowTerminatingError(
[System.Management.Automation.ErrorRecord]::new(
([System.IO.FileNotFoundException]"Could not find $Path"),
'My.ID',
[System.Management.Automation.ErrorCategory]::OpenError,
$MyObject
)
)
Pokud předpokládáme, že ThrowTerminatingError()
se volalo uvnitř funkce Get-Resource
, pak se jedná o chybu, kterou bychom viděli.
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
Vidíte, jak odkazuje na Get-Resource
funkci jako na zdroj problému? To uživateli řekne něco užitečného.
Protože $PSItem
je to, ErrorRecord
můžeme tento způsob použít ThrowTerminatingError
také k opětovnému vyvolání.
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
Tím změníte zdroj chyby na rutinu a skryjete vnitřní vlastnosti funkce uživatelům rutiny.
Zkuste vytvořit ukončující chyby.
Kirk Munro upozorňuje, že některé výjimky ukončují pouze chyby při spuštění uvnitř try/catch
bloku. Tady je příklad, který mi dal, že vygeneruje dělení výjimkou nulového modulu runtime.
function Start-Something { 1/(1-1) }
Potom ho vyvoláte tak, aby se zobrazila chyba a stále vypíše zprávu.
&{ Start-Something; Write-Output "We did it. Send Email" }
Ale umístěním stejného kódu do , try/catch
vidíme něco jiného.
try
{
&{ Start-Something; Write-Output "We did it. Send Email" }
}
catch
{
Write-Output "Notify Admin to fix error and send email"
}
Vidíme, že chyba se stane ukončující chybou a nevypíše první zprávu. Co se mi nelíbí o tomhle je, že tento kód můžete mít ve funkci a funguje jinak, pokud někdo používá .try/catch
Neměl jsem problémy s tím sám, ale je to rohový případ vědět o.
$PSCmdlet.ThrowTerminatingError() uvnitř try/catch
Jednou z drobných odlišností $PSCmdlet.ThrowTerminatingError()
je, že v rámci rutiny vytvoří ukončující chybu, ale po opuštění rutiny se změní na neukončující chybu. Tím se zatěžuje volající funkce, abyste se rozhodli, jak chybu zpracovat. Mohou ho převést zpět na ukončující chybu pomocí -ErrorAction Stop
nebo voláním z objektu try{...}catch{...}
.
Veřejné šablony funkcí
Jeden poslední způsob, jak jsem měl s konverzací s Kirk Munro, bylo, že kolem try{...}catch{...}
každého begin
, process
a end
blokovat ve všech jeho pokročilých funkcích. V těchto obecných blocích catch má jeden řádek, který používá $PSCmdlet.ThrowTerminatingError($PSItem)
k řešení všech výjimek, které opouštějí své funkce.
function Start-Something
{
[CmdletBinding()]
param()
process
{
try
{
...
}
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
}
Protože všechno je ve try
svých funkcích, všechno funguje konzistentně. Tím se také koncovým uživatelům zobrazí čisté chyby, které skryjí vnitřní kód před vygenerovanou chybou.
Past
Zaměřil jsem se na try/catch
aspekt výjimek. Ale je tu jedna starší funkce, kterou musím zmínit, než to zabalíme.
A trap
je umístěn ve skriptu nebo funkci pro zachycení všech výjimek, ke kterým dochází v daném oboru. Když dojde k výjimce, spustí se kód v souboru trap
a pak bude pokračovat normální kód. Pokud dojde k více výjimkám, volá se přes a přesah pasti.
trap
{
Write-Log $PSItem.ToString()
}
throw [System.Exception]::new('first')
throw [System.Exception]::new('second')
throw [System.Exception]::new('third')
Osobně jsem tento přístup nikdy nepřijal, ale vidím hodnotu ve skriptech správce nebo kontroleru, které protokolují všechny výjimky, a pak stále pokračovat v provádění.
Závěrečné poznámky
Přidáním správného zpracování výjimek do skriptů je nejen stabilnější, ale také usnadníte odstraňování těchto výjimek.
Strávil jsem spoustu času tím throw
, že je to základní koncept, když mluví o zpracování výjimek. Prostředí PowerShell nám Write-Error
také poskytlo, že zpracovává všechny situace, kdy byste použili throw
. Proto si nemyslete, že byste po přečtení museli používat throw
.
Teď, když jsem si vzal čas na psaní informací o zpracování výjimek v tomto detailu, přepnu na použití Write-Error -Stop
k vygenerování chyb v kódu. Taky si vezmu Kirkovu radu a udělám ThrowTerminatingError
svou obslužnou rutinu výjimky pro každou funkci.