Sdílet prostřednictvím


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á ($PSItemnebo$_) 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, ErrorRecordmůž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/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í 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.