Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
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ého @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 když dojde k nedostatku paměti, jsou příkladem situací, které způsobí výjimku. Někdy autor kódu, který používáte, vytvoří výjimky pro určité problémy, když k nim dojde.
Hození a chytání
Když dojde k výjimce, říkáme, že je výjimka vyvolána. 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é vzájemně volají jedna druhou. Když je volána funkce, přidá se do zásobníku nebo do horní části seznamu. Když se funkce ukončí nebo vrátí, je odstraněna 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čující a neukončující chyby
Výjimka je obecně ukončující chyba. Vyvolána výjimka je buď zachycena, nebo ukončí aktuální spuštění. Ve výchozím nastavení je Write-Error generována neukončující chyba a chyba je přidána do výstupního proudu bez vyvolání výjimky.
Uvádím to, protože Write-Error a další neukončující chyby neaktivují catch.
Ignorová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.
Hodit
Pokud chcete vytvořit 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. Zpracovává ji catch ve volající funkci nebo ukončí 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 -ErrorAction Zastavit
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ěkujeme Lee Dailey za připomenutí, jak používat -ErrorAction Stop tímto způsobem.
Cmdlet -ErrorAction Stop
Pokud zadáte -ErrorAction Stop pro libovolnou pokročilou funkci nebo rutinu, změní se všechny příkazy Write-Error na ukončující chyby, které zastaví provádění, nebo které mohou být zpracovány pomocí catch.
Start-Something -ErrorAction Stop
Další informace o parametru ErrorAction naleznete v tématu about_CommonParameters. Další informace o proměnné $ErrorActionPreference naleznete v tématu about_Preference_Variables.
Blok Try/Catch
Způsob, jakým funguje zpracování výjimek v PowerShellu (a mnoha dalších jazycích), spočívá v tom, že nejprve try sekci kódu a pokud vyvolá chybu, můžete ji 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čí catch. K informacím o výjimce v bloku catch 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. Přesně to dělá finally skript.
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 ExecuteNonQuery() vyvolá výjimku, připojení se nezavře. Tady je stejný kód uvnitř bloku try/finally.
$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, se stále šíří nahoru v zásobníku volání.
Try/Catch/Finally
Je naprosto platné používat společně catch a finally. 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.
Uvnitř bloku catch je automatická proměnná ($PSItem nebo $_) typu ErrorRecord, 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í této výjimky použil neplatnou cestu v ReadAllText.
[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() se volá automaticky, pokud je $PSItem 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 z ukázkové 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, Line kódu a ScriptLineNumber, 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
Provádím jenom volání funkcí ve stejném skriptu, ale to by mohlo sledovat volání, pokud by bylo zapojeno více skriptů.
$PSItem.Exception
Toto je skutečná výjimka, která byla vyvolána.
$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 žádná nastavena v 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.
V pozdější chvíli se k tomu vrátím, až budu mluvit o opětovném vyvolání výjimek.
$PSItem.Exception.StackTrace
Toto je StackTrace pro výjimku. Ukázal jsem ScriptStackTrace výše, ale toto je určeno 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 )
Tento výpis 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 je to vše, co můžeme vidět v tomto příkladu. Obecně, když se díváte na výpis 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í typovaných výjimek
Můžete mít možnost volit výjimky, které zachytává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 typ výjimky, 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ředchozím příkladu FileNotFoundException dědí z IOException. Takže pokud IOException byla první, volala by se místo toho. Vyvolá se pouze jeden blok zachycení, i když existuje více shod.
Pokud bychom měli System.IO.PathTooLongException, IOException by se shodoval, ale pokud bychom měli InsufficientMemoryException, pak by to nic nezachytilo a propagovalo by se nahoru po zásobníku.
Zachycení více typů najednou
Pomocí stejného příkazu catch 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ěkujeme redditoru u/Sheppard_Ra za navržení tohoto doplňku.
Vyvolání výjimek s typem
V PowerShellu můžete vyvolat zadané výjimky. Místo volání throw ř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, kterou lze vyvolat. 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.
Write-Error -Exception
Tyto typové výjimky můžeme přidat do Write-Error a stále můžeme catch chyby podle typu výjimky. Použijte Write-Error jako 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í reddit r/PowerShell komunity, 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 v základním System oboru názvů pokusit použít výjimky.
Výjimky jsou objekty.
Pokud začnete používat velké množství typovaných 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 FileNotFoundException dokumentaci 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 .NET.
Opětovné vyvolání výjimky
Pokud vše, co uděláte ve svém bloku catch, je throw stejnou výjimku, pak to catch nedělejte. Měli byste catch pouze 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. Můžeme napsat zprávu nebo zaznamenat problém blízko místa, kde jej zjistíme, ale řešit ho ve vyšší úrovni zpracování.
catch
{
Write-Log $PSItem.ToString()
throw $PSItem
}
Je zajímavé, že můžeme volat throw zevnitř catch a znovu vyvolat 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 to původní místo, kde byla výjimka vyvolána.
Opětovné vyhození 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 někomu níže ve vrstvě přístup k němu jako $PSItem.Exception.InnerException.
catch
{
throw [System.MissingFieldException]::new('Could not access field',$PSItem.Exception)
}
$PSCmdlet.ThrowTerminatingError()
Jedna věc, která se mi nelíbí na použití throw pro nezpracované výjimky, je, že chybová zpráva ukazuje na příkaz throw a tvrdí, že problém je na tomto řádku.
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.
Když mi chybová zpráva sdělí, že můj skript je poškozený, protože jsem volal throw na řádku 31, je to špatná zpráva pro uživatele vašeho skriptu. Nic užitečného jim to neřekne.
Dexter Dhami ukázal, že 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() byl volán uvnitř funkce jménem 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 funkci Get-Resource jako zdroj problému? To uživateli řekne něco užitečného.
Vzhledem k tomu, že $PSItem je ErrorRecord, můžeme ThrowTerminatingError tímto způsobem znovu hodit.
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
Tím změníte původ chyby na Cmdlet a skryjete vnitřní strukturu vaší funkce před uživateli vašeho Cmdletu.
Funkce try může vytvářet ukončující chyby.
Kirk Munro upozorňuje, že některé výjimky jsou pouze ukončující chyby při spuštění uvnitř bloku try/catch. Tady je příklad, který mi dal a který způsobuje výjimku dělení nulou během běhu programu.
function Start-Something { 1/(1-1) }
Potom ho spusťte takto, aby generoval chybu a stále zobrazil zprávu.
&{ Start-Something; Write-Output "We did it. Send Email" }
Ale umístěním stejného kódu do try/catchvidí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í na tomto je, že tento kód můžete mít ve funkci a ten funguje jinak, pokud někdo používá try/catch.
Sám jsem se s tím nesetkal, ale je to okrajový případ, na který je třeba dávat pozor.
$PSCmdlet.ThrowTerminatingError() uvnitř try/catch
Jednou z nuancí $PSCmdlet.ThrowTerminatingError() je, že uvnitř cmdletu vytvoří ukončující chybu, ale poté, co opustí cmdlet, se změní na neukončující chybu. To ponechává zátěž na volajícím vaší funkce, aby se rozhodl, jak chybu zpracovat. Mohou ho převést zpět na ukončující chybu pomocí -ErrorAction Stop nebo zavoláním zevnitř try{...}catch{...}.
Veřejné šablony funkcí
Poslední poznatek z mého rozhovoru s Kirkem Munroem bylo, že umisťuje try{...}catch{...} kolem každého bloku begin, process a end ve všech svých pokročilých funkcích. V těchto obecných catch blocích má jeden řádek, který používá $PSCmdlet.ThrowTerminatingError($PSItem) k řešení všech výjimek, které opouštějí jeho funkce.
function Start-Something
{
[CmdletBinding()]
param()
process
{
try
{
...
}
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
}
Protože všechno je v rámci try příkazu v jeho funkcích, všechno se chová 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 aspekt výjimek try/catch. Ale je tu jedna starší funkce, kterou musím zmínit, než to zabalíme.
trap se umístí do skriptu nebo funkce, aby se zachytily všechny výjimky, ke kterým dochází v daném oboru. Když dojde k výjimce, kód v trap se spustí a pak normální kód pokračuje. Pokud dojde k více výjimkám, volá se past znova a znova.
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 hodně času mluvením o throw, protože je to základní koncept při řešení výjimek. PowerShell nám dal také Write-Error, který zpracovává všechny situace, kdy byste použili throw. Nemyslete si, že po přečtení tohoto článku potřebujete používat throw.
Teď, když jsem si vzala č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. Jsem také rozhodnutý vzít si radu od Kirka a nastavit ThrowTerminatingError jako mou preferovanou obslužnou rutinu výjimek pro všechny funkce.