Minden, amit tudni akart a kivételekről
A hibakezelés csak része az életnek, amikor kódírásról van szó. Gyakran ellenőrizhetjük és érvényesíthetjük a várt viselkedés feltételeit. Amikor a váratlan esemény bekövetkezik, kivételkezelésre váltunk. Egyszerűen kezelheti a mások kódjai által létrehozott kivételeket, vagy létrehozhat saját kivételeket mások számára.
Feljegyzés
A cikk eredeti verziója @KevinMarquette által írt blogon jelent meg. A PowerShell csapata köszönjük Kevinnek, hogy megosztotta velünk ezt a tartalmat. Kérjük, nézze meg a blogját a PowerShellExplained.com.
Alapszintű terminológia
Néhány alapfogalmat meg kell fednünk, mielőtt belevágnánk ebbe.
Kivétel
A kivétel olyan esemény, mint egy olyan esemény, amely akkor jön létre, ha a normál hibakezelés nem tudja kezelni a problémát. A szám nullával való felosztása vagy a memória elfogyása olyan példák, amelyek kivételt okoznak. Előfordulhat, hogy a használt kód szerzője kivételeket hoz létre bizonyos problémák esetén.
Dobás és fogás
Amikor kivétel történik, azt mondjuk, hogy a rendszer kivételt ad ki. A kidobott kivétel kezeléséhez el kell kapnia. Ha a rendszer kivételt ad ki, és nem kap valamit, a szkript nem hajt végre.
A hívásverem
A hívásverem az egymásnak hívott függvények listája. Amikor meghív egy függvényt, a rendszer hozzáadja a veremhez vagy a lista elejéhez. Amikor a függvény kilép vagy visszatér, a függvény el lesz távolítva a veremből.
Kivétel esetén a hívásverem be van jelölve annak érdekében, hogy egy kivételkezelő elkapja azt.
Megszüntetési és nem megszüntetési hibák
A kivétel általában megszüntető hiba. A rendszer egy kidobott kivételt kap, vagy leállítja az aktuális végrehajtást. Alapértelmezés szerint a rendszer nem végződő hibát hoz létre Write-Error
, és kivétel nélkül hozzáad egy hibát a kimeneti streamhez.
Rámutatok erre, mert Write-Error
és más, nem végződő hibák nem aktiválják a catch
.
Kivétel lenyelése
Ekkor kap egy hibát csak azért, hogy elnyomja azt. Ezt körültekintően végezze el, mert ez megnehezítheti a hibaelhárítási problémákat.
Egyszerű parancsszintaxis
Íme egy gyors áttekintés a PowerShellben használt alapvető kivételkezelési szintaxisról.
Vet
A saját kivételesemény létrehozásához kivételt adunk a throw
kulcsszóval.
function Start-Something
{
throw "Bad thing happened"
}
Ez létrehoz egy futásidejű kivételt, amely egy megszüntetési hiba. Egy hívó függvény kezeli catch
, vagy kilép a szkriptből egy ilyen üzenettel.
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
Írási hiba – ErrorAction leállítása
Megemlítettem, hogy Write-Error
alapértelmezés szerint nem okoz megszüntetési hibát. Ha megadja -ErrorAction Stop
, Write-Error
egy olyan megszűnési hibát hoz létre, amely egy catch
.
Write-Error -Message "Houston, we have a problem." -ErrorAction Stop
Köszönjük Lee Dailey-nek, hogy emlékeztetett az ilyen módszer használatára -ErrorAction Stop
.
Parancsmag –ErrorAction Stop
Ha bármilyen speciális függvényt vagy parancsmagot ad meg -ErrorAction Stop
, az összes Write-Error
utasítást befejező hibákká alakítja, amelyek leállítják a végrehajtást, vagy amelyek egy catch
adott parancsmaggal kezelhetők.
Start-Something -ErrorAction Stop
Az ErrorAction paraméterről további információt a about_CommonParameters című témakörben talál. A változóval kapcsolatos további információkért lásd: $ErrorActionPreference
about_Preference_Variables.
Kipróbálás/fogás
A Kivételkezelés működése a PowerShellben (és sok más nyelven) az, hogy először try
egy kódszakaszt ad meg, és ha hibát jelez, megteheti catch
. Íme egy gyors minta.
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 $_
}
A catch
szkript csak akkor fut, ha megszüntetési hiba történt. Ha a try
végrehajtás megfelelően történik, akkor átugrik a catch
. A blokk kivételadatait a catch
változó használatával érheti $_
el.
Kipróbálás/végül
Néha nem kell kezelnie egy hibát, de szükség van valamilyen kód végrehajtására, ha kivétel történik vagy sem. Egy finally
szkript pontosan ezt teszi.
Figyelje meg ezt a példát:
$command = [System.Data.SqlClient.SqlCommand]::New(queryString, connection)
$command.Connection.Open()
$command.ExecuteNonQuery()
$command.Connection.Close()
Amikor megnyit vagy csatlakozik egy erőforráshoz, zárja be. Ha a ExecuteNonQuery()
rendszer kivételt jelez, a kapcsolat nincs lezárva. Itt ugyanaz a kód található egy try/finally
blokkon belül.
$command = [System.Data.SqlClient.SqlCommand]::New(queryString, connection)
try
{
$command.Connection.Open()
$command.ExecuteNonQuery()
}
finally
{
$command.Connection.Close()
}
Ebben a példában a kapcsolat megszakad, ha hiba történik. Akkor is bezárul, ha nincs hiba. A finally
szkript minden alkalommal fut.
Mivel nem kapja meg a kivételt, a rendszer továbbra is propagálja a hívásvermet.
Kipróbálás/Fogás/Végül
Teljesen érvényes, hogy együtt és finally
együtt használjukcatch
. Legtöbbször az egyiket vagy a másikat fogja használni, de olyan forgatókönyveket is találhat, ahol mindkettőt használja.
$PSItem
Most, hogy az alapok kikerültek az útból, egy kicsit mélyebbre áshatunk.
catch
A blokkon belül található egy automatikus változó ($PSItem
vagy $_
) típusErrorRecord
, amely tartalmazza a kivétel részleteit. Az alábbiakban rövid áttekintést talál néhány kulcsfontosságú tulajdonságról.
Ezekben a példákban ReadAllText
érvénytelen elérési utat használtam a kivétel létrehozásához.
[System.IO.File]::ReadAllText( '\\test\no\filefound.log')
PSItem.ToString()
Ez a legtisztább üzenetet adja a naplózáshoz és az általános kimenethez. ToString()
a rendszer automatikusan meghívja, ha $PSItem
egy sztringben van elhelyezve.
catch
{
Write-Output "Ran into an issue: $($PSItem.ToString())"
}
catch
{
Write-Output "Ran into an issue: $PSItem"
}
$PSItem.InvocationInfo
Ez a tulajdonság további információkat tartalmaz, amelyeket a PowerShell gyűjt a kivételt tartalmazó függvényről vagy szkriptről. Itt található a InvocationInfo
létrehozott mintakivétel.
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
A fontos részletek itt a ScriptName
kód, a Line
hívás indításának helyét ScriptLineNumber
mutatják.
$PSItem.ScriptStackTrace
Ez a tulajdonság azokat a függvényhívásokat jeleníti meg, amelyek a kivételt létrehozó kódhoz adták.
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
Csak ugyanabban a szkriptben indítok hívásokat a függvényekre, de ez nyomon követné a hívásokat, ha több szkript is érintett volna.
$PSItem.Exception
Ez a tényleges kivétel, amely ki lett dobva.
$PSItem.Exception.Message
Ez az általános üzenet ismerteti a kivételt, és jó kiindulópont a hibaelhárításhoz. A legtöbb kivétel alapértelmezett üzenettel rendelkezik, de a kivétel eldobásakor egyénire is beállítható.
PS> $PSItem.Exception.Message
Exception calling "ReadAllText" with "1" argument(s): "The network path was not found."
Ez az üzenet akkor is visszaadva, ha a híváshoz $PSItem.ToString()
nincs beállítva egy beállítás.ErrorRecord
$PSItem.Exception.InnerException
A kivételek belső kivételeket tartalmazhatnak. Ez gyakran akkor fordul elő, ha a hívott kód kivételt kap, és egy másik kivételt ad ki. Az eredeti kivétel az új kivételen belülre kerül.
PS> $PSItem.Exception.InnerExceptionMessage
The network path was not found.
Ezt később fogom újra áttekinteni, amikor a kivételek újravetéséről beszélek.
$PSItem.Exception.StackTrace
Ez a StackTrace
kivétel. Megmutattam a ScriptStackTrace
fenti, de ez az egyik a hívások felügyelt kód.
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 )
Ezt a veremkövetést csak akkor kapja meg, ha az eseményt felügyelt kódból dobják ki. Egy .NET-keretrendszerfüggvényt hívok meg közvetlenül, hogy ez minden, amit láthatunk ebben a példában. Általában ha egy veremkövetést keres, akkor azt keresi, hogy hol áll le a kód, és hol kezdődnek a rendszerhívások.
Kivételek használata
Több kivétel létezik, mint az alapszintaxis és a kivétel tulajdonságai.
Beírt kivételek elfogása
A kifogott kivételekkel szelektív lehet. A kivételeknek van egy típusa, és megadhatja, hogy milyen típusú kivételt szeretne elkapni.
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"
}
Az egyes blokkok kivételtípusa mindaddig catch
be van jelölve, amíg a kivételnek megfelelőt nem talál.
Fontos tisztában lennie azzal, hogy a kivételek öröklődhetnek más kivételektől. A fenti FileNotFoundException
példában a következőtől IOException
öröklődik: . Tehát ha az IOException
első lenne, akkor inkább meghívnák. A rendszer csak egy fogási blokkot hív meg, még akkor is, ha több egyezés van.
Ha lenne egy System.IO.PathTooLongException
, a IOException
megfelelne, de ha lenne, InsufficientMemoryException
akkor semmi sem fogná fel, és propagálja fel a vermet.
Több típus egyidejű elfogása
Több kivételtípust is elkaphat ugyanazzal catch
az utasítással.
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]"
}
Köszönjük Redditornak u/Sheppard_Ra
, hogy javasolta ezt a kiegészítést.
Beírt kivételek dobálása
Gépelt kivételeket a PowerShellben is megadhat. throw
Sztring helyett:
throw "Could not find: $path"
Használjon a következőhöz hasonló kivételgyorsítót:
throw [System.IO.FileNotFoundException] "Could not find: $path"
De ha így tesz, meg kell adnia egy üzenetet.
Létrehozhat egy új kivételpéldányt is, amely ki lesz dobva. Ez az üzenet nem kötelező, mert a rendszer minden beépített kivételhez alapértelmezett üzenetekkel rendelkezik.
throw [System.IO.FileNotFoundException]::new()
throw [System.IO.FileNotFoundException]::new("Could not find path: $path")
Ha nem a PowerShell 5.0-s vagy újabb verzióját használja, a régebbi New-Object
megközelítést kell használnia.
throw (New-Object -TypeName System.IO.FileNotFoundException )
throw (New-Object -TypeName System.IO.FileNotFoundException -ArgumentList "Could not find path: $path")
Egy beírt kivétel használatával Ön (vagy mások) az előző szakaszban említett típus alapján is elkaphatja a kivételt.
Írási hiba – Kivétel
Hozzáadhatjuk ezeket a beírt kivételeket, Write-Error
és kivételtípusonként továbbra is catch
meg tudjuk adni a hibákat. Használja Write-Error
az alábbi példákhoz hasonlóan:
# 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
Ezután a következő módon foghatjuk el:
catch [System.IO.FileNotFoundException]
{
Write-Log $PSItem.ToString()
}
A .NET-kivételek nagy listája
Összeállítottam egy mesterlistát a Reddit r/PowerShell
közösség segítségével, amely több száz .NET-kivételt tartalmaz, hogy kiegészítse ezt a bejegyzést.
Először is keres, hogy a listát a kivételek, amelyek úgy érzik, hogy lenne egy jó illeszkedik az én helyzetem. Próbálja meg kivételeket használni az alapnévtérben System
.
A kivételek objektumok
Ha sok beírt kivételt kezd használni, ne feledje, hogy ezek objektumok. A különböző kivételek különböző konstruktorokkal és tulajdonságokkal rendelkeznek. Ha megnézzük a FileNotFoundException dokumentációját System.IO.FileNotFoundException
, láthatjuk, hogy át tudunk adni egy üzenetet és egy fájl elérési útját.
[System.IO.FileNotFoundException]::new("Could not find file", $path)
És van egy FileName
tulajdonsága, amely elérhetővé teszi a fájl elérési útját.
catch [System.IO.FileNotFoundException]
{
Write-Output $PSItem.Exception.FileName
}
Tekintse meg a .NET dokumentációját más konstruktorok és objektumtulajdonságok esetében.
Kivétel ismételt kivetése
Ha a blokkban catch
throw
csak ugyanazt a kivételt fogja elvégezni, akkor ne catch
tegye. Csak olyan kivételt kell végrehajtania catch
, amelyet akkor szeretne kezelni vagy végrehajtani, ha ez történik.
Vannak olyan esetek, amikor egy kivételen műveletet szeretne végrehajtani, de a kivételt újra szeretné dobni, hogy valami az alsóbb rétegben kezelni tudja azt. Írhatunk egy üzenetet, vagy naplózhatjuk a problémát a felfedezés helye közelében, de a problémát tovább kezelhetjük a veremen.
catch
{
Write-Log $PSItem.ToString()
throw $PSItem
}
Érdekes módon, hívhatunk throw
belülről catch
, és ez újra dobja a jelenlegi kivételt.
catch
{
Write-Log $PSItem.ToString()
throw
}
A kivételt újra szeretnénk dobni, hogy megőrizzük az eredeti végrehajtási adatokat, például a forrásszkriptet és a sorszámot. Ha ezen a ponton új kivételt adunk ki, az elrejti, hogy hol kezdődött a kivétel.
Új kivétel ismételt eldobása
Ha kivételt észlel, de egy másikat szeretne dobni, akkor az eredeti kivételt az újba kell ágyaznia. Ez lehetővé teszi, hogy valaki a veremen lefelé, mint a $PSItem.Exception.InnerException
.
catch
{
throw [System.MissingFieldException]::new('Could not access field',$PSItem.Exception)
}
$PSCmdlet.ThrowTerminatingError()
Az egyetlen dolog, amit nem szeretek használni throw
a nyers kivételek, hogy a hibaüzenet pont az throw
utasítás, és azt jelzi, hogy a sor, ahol a probléma van.
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.
Miután a hibaüzenet azt mondja, hogy a szkriptem hibás, mert a 31. sorban hívtam throw
, az rossz üzenet a szkript felhasználói számára. Nem mond nekik semmi hasznosat.
Dexter Dhami rámutatott, hogy ezt ki tudom javítani ThrowTerminatingError()
.
$PSCmdlet.ThrowTerminatingError(
[System.Management.Automation.ErrorRecord]::new(
([System.IO.FileNotFoundException]"Could not find $Path"),
'My.ID',
[System.Management.Automation.ErrorCategory]::OpenError,
$MyObject
)
)
Ha feltételezzük, hogy ezt ThrowTerminatingError()
egy úgynevezett Get-Resource
függvényben hívták meg, akkor ez a hiba, amelyet látnánk.
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
Látja, hogyan utal a Get-Resource
függvényre a probléma forrásaként? Ez valami hasznosat jelez a felhasználónak.
Mert $PSItem
ez egy ErrorRecord
, mi is használhatja ThrowTerminatingError
ezt a módszert, hogy újra dobni.
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
Ez megváltoztatja a hiba forrását a parancsmagra, és elrejti a függvény belső elemeit a parancsmag felhasználói elől.
Megpróbálhat megszüntetési hibákat létrehozni
Kirk Munro rámutat, hogy egyes kivételek csak akkor okoznak hibákat, ha egy try/catch
blokkon belül hajtják végre. Itt van az a példa, amit adott nekem, hogy létrehoz egy osztás nulla futásidejű kivétel.
function Start-Something { 1/(1-1) }
Ezután hívja meg így, hogy lássa a hibát, és továbbra is ki tudja adni az üzenetet.
&{ Start-Something; Write-Output "We did it. Send Email" }
De azzal, hogy ugyanazt a kódot egy try/catch
adott fájlba helyezi, valami más történik.
try
{
&{ Start-Something; Write-Output "We did it. Send Email" }
}
catch
{
Write-Output "Notify Admin to fix error and send email"
}
Azt látjuk, hogy a hiba megszűnő hibává válik, és nem az első üzenetet adja ki. Amit nem szeretek ebben az egy, hogy lehet ez a kód egy függvényben, és másképp működik, ha valaki használ egy try/catch
.
Én nem futott a problémákat ezzel magam, de ez a sarok esetben, hogy tisztában kell lennie.
$PSCmdlet.ThrowTerminatingError() belül try/catch
Ennek egyik árnyalata $PSCmdlet.ThrowTerminatingError()
, hogy megszüntetési hibát hoz létre a parancsmagon belül, de a parancsmag elhagyása után nem végződő hibává válik. Ez a függvény hívójára hárul, hogy eldöntse, hogyan kezelje a hibát. Visszafordíthatják egy megszüntető hibává, ha egy adott fájlon belülről try{...}catch{...}
hívják vagy használják-ErrorAction Stop
.
Nyilvános függvénysablonok
Az egyik utolsó út volt a beszélgetésem Kirk Munro volt, hogy ő helyez egy try{...}catch{...}
körül minden begin
, process
és end
blokk az összes fejlett funkcióit. Ezekben az általános fogási blokkokban egyetlen vonallal $PSCmdlet.ThrowTerminatingError($PSItem)
kezeli a függvényeit elhagyó kivételeket.
function Start-Something
{
[CmdletBinding()]
param()
process
{
try
{
...
}
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
}
Mivel minden egy try
utasításban van a funkcióiban, minden következetesen működik. Ez tiszta hibákat is ad a végfelhasználónak, amely elrejti a belső kódot a generált hiba elől.
Csapda
A kivételekre koncentráltam try/catch
. De van egy régi funkció, amit meg kell említenem, mielőtt ezt becsomagolnánk.
A trap
parancsprogramba vagy függvénybe kerül, hogy elkapja az adott hatókörben előforduló összes kivételt. Kivétel esetén a program végrehajtja a trap
benne lévő kódot, majd a normál kód folytatódik. Ha több kivétel is előfordul, a csapda újra és újra le lesz hívva.
trap
{
Write-Log $PSItem.ToString()
}
throw [System.Exception]::new('first')
throw [System.Exception]::new('second')
throw [System.Exception]::new('third')
Én személy szerint soha nem fogadta el ezt a megközelítést, de látom az értéket a rendszergazdai vagy vezérlő szkriptek, hogy naplózza az esetleges és minden kivételt, majd továbbra is végrehajtani.
Záró megjegyzések
A megfelelő kivételkezelés hozzáadása a szkriptekhez nemcsak stabilabbá teszi őket, hanem megkönnyíti a kivételek elhárítását is.
Sok időt töltöttem a beszélgetéssel throw
, mert ez egy alapvető fogalom, amikor a kivételkezelésről beszélek. A PowerShell azt is megadta nekünk Write-Error
, hogy kezelje az összes olyan helyzetet, ahol ön használná throw
. Ezért ne gondolja, hogy ezt elolvasása után kell használnia throw
.
Most, hogy időt vettem, hogy ebben a részletben írjak a kivételkezelésről, át fogok váltani a kód hibáinak generálására szolgáló használatára Write-Error -Stop
. Én is fogadom Kirk tanácsát, és minden funkcióhoz intézem ThrowTerminatingError
a goto kivételkezelőmet.