SEブログ

【PowerShell】エラーハンドリング(Try-Catch)を使ってみる

こんにちは!SE ブログの相馬です。

 

 

 

今回はエラーハンドリングについて書いてみました。エラーハンドリングは PowerShell でスクリプトを書く上で非常に重要です。それでは何故エラーハンドリングが重要なのでしょうか?

 

 

 

エラーハンドリングは何故必要か

 

 

スクリプトを実行される中で、想定外のエラーが発生する場合があります。

 

 

例えばファイルの読み書きの場合、必ずそのアクセス先のファイルにアクセスできるとは 100 % 保証できないですよね。

 

 

具体的にはアクセス先のファイルが削除されたとか、ファイルがロックされたとか、ネットワーク遅延とか、そもそもアクセス権限が無いとかでアクセスができないような事があるわけです。

 

 

データベースも同様です。アクセス先のテーブルが存在しないとか、DB がメンテナンス中とか、ネットワーク遅延とか、アクセス権限が無いとかになります。

 

 

このような問題でスクリプトの処理が失敗してしまった場合、スクリプトの処理を中止しなければなりませんよね。

 

 

こういった場合にエラーハンドリングを使ってエラーメッセージや警告を表示させたり処理を中断させたりする事ができます。

 

 

ここでは、こういったエラーが出た場合のスクリプトにおけるエラーの種類として、まずは 2 種類のパターンがある事から説明します。

 

 

 

エラーの種類

 

 

Termination Error

 

 

エラーが発生した場合は処理を中断します。

 

 

原因としては、構文のエラーか、メモリの容量が足りないといったような場合になります。Terminating Error はエラーハンドリングで取得されます。

 

 

Non-Terminating Error

 

 

エラーは発生しますが、PowerShell は処理を継続します。これはエラーハンドリングでは取得できません。スクリプトで発生する多くのエラーは大体 Non-Terminating Error のほうだと言われています。エラーは発生するけれども、処理は継続するというパターンです。

 

 

サンプルを見てみましょう。これは人事の DB で使う更新スクリプトになります。日本の中~大企業ですと、4 月と 10 月に大規模な人事異動があると思います。

 

 

人事部はそれに合わせて人事部で持っている DB を更新しなければなりません。誰が退職したとか、入社したとか、違う部署に異動したとかです。ここでは実際 DB がないので動作はしませんが、内容は掴めると思います。

 


$UpdatedEmployee = Get-Content "\\FileServer\scripts\HR\CompanyEmployees.txt"
$CurrentEmployee = Get-MyEmployee

foreach ($emp in $CurrentEmployee) {

    if ($UpdatedEmployee -notcontains $emp) {

        Delete-Employee $emp # DBから社員を削除する

    } else {

        Update-Employee $emp "Add" # DBに無い社員を追加する

    }

}

 

 

 

このスクリプトですが、もしファイル (CompanyEmployees.txt) が所定の場所に存在しなかったらどうなるでしょうか、またはファイルがロックされていたらどうなるでしょうか、またはアクセス権限が無い場合はどうなるでしょうか。

 

 

ファイルが見つからないのでエラーは発生しますが、処理は継続しています。これが Non-Terminating Error です。

 

# 実行結果
Get-Content : パス 'C:\scripts\HR\CompanyEmployees.txt' が存在しないため検出できません。
発生場所 行:1 文字:20
+ $UpdatedEmployee = Get-Content "C:\scripts\HR\CompanyEmployees.txt"
 + CategoryInfo : ObjectNotFound: (C:\scripts\HR\CompanyEmployees.txt:Strin 
g) [Get-Content], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetContentComm 
and

Get-MyEmployee : 用語 'Get-MyEmployee' は、コマンドレット、関数、スクリプト ファイル、または操作可能なプログラムの名前として認識されません。
名前が正しく記述されていることを確認し、パスが含まれている場合はそのパスが正しいことを確認してから、再試行してください。
発生場所 行:2 文字:20
+ $CurrentEmployee = Get-MyEmployee
+ ~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (Get-MyEmployee:String) [], CommandNotFou 
ndException
+ FullyQualifiedErrorId : CommandNotFoundException

 

 

 

では、以下のスクリプトを見てみましょう。現在 4 名従業員がいるとして、更新分の従業員のファイルと比較するとします。

 

 

実行結果を見てもらえれば分かりますが、ファイルが見つからないのですが、現在分の従業員が削除されてしまいました。

 

 

ファイルが見つかりエラーは出力されますが、処理は継続しますので削除処理をしてしまいます。

 

 

こうなってしまうと、DB から削除された従業員は DB から復旧しなければなりませんので、それなりに大ごとになります。

 

 

夜間作業になり、バックアップしたメディアを使い、DB からリストアする必要があるかもしれません。ですので、これは実際はあってはならないスクリプトになります。

 

$UpdatedEmployee = Get-Content "C:\scripts\HR\CompanyEmployees.txt"
$CurrentEmployee = @("emp1", "emp2", "emp3", "emp10")

foreach ($emp in $CurrentEmployee) {

    if ($UpdatedEmployee -notcontains $emp) {

        Write-Host "削除されました。"

    } else {

        Write-Host "更新されました。"

    }

}


# 実行結果

Get-Content : パス 'C:\scripts\HR\CompanyEmployees.txt' が存在しないため検出できません。
発生場所 行:1 文字:20
+ $UpdatedEmployee = Get-Content "C:\scripts\HR\CompanyEmployees.txt"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\scripts\HR\CompanyEmployees.txt:Strin 
g) [Get-Content], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetContentComm 
and

削除されました。← ファイルが見つからないのに従業員が削除されてしまっている
削除されました。← ファイルが見つからないのに従業員が削除されてしまっている
削除されました。← ファイルが見つからないのに従業員が削除されてしまっている
削除されました。← ファイルが見つからないのに従業員が削除されてしまっている

 

 

 

Non-Terminating Error の中断方法

 

 

 

という事で、こういう場合には処理を中断する必要がある事はお分かりいただけたと思います。では、Non-Terminating Error で発生したエラーを中断するにはどうしたら良いでしょうか。

 

 

-ErrorAction Stop

 

 

-ErrorAction Stop を使います。処理を中止したいところに書く事によって処理を中断する事が可能になります。

 

 

$UpdatedEmployee = Get-Content "C:\scripts\HR\CompanyEmployees.txt" -ErrorAction Stop
$CurrentEmployee = @("emp1", "emp2", "emp3", "emp10")

foreach ($emp in $CurrentEmployee) {

    if ($UpdatedEmployee -notcontains $emp) {

        Write-Host "削除されました。"

    } else {

        Write-Host "更新されました。"

    }

}


# 実行結果

Get-Content : パス 'C:\scripts\HR\CompanyEmployees.txt' が存在しないため検出できません。
発生場所 行:1 文字:20
+ ... dEmployee = Get-Content "C:\scripts\HR\CompanyEmployees.txt" -ErrorAc ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\scripts\HR\CompanyEmployees.txt:String) [Get-Content], It 
emNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetContentCommand

 

 

 

もう一つは $ErrorActionPreference を使います。

 

 

 

$ErrorActionPreference

 

 

これは PowerShell の変数で、これを使うと全てのコマンドレットで発生したエラーを取得してどのように動作するか指定できます。

 

 

スクリプト単位で使用されるので、個々のコマンドでは使えません。その場合は ErrorAction パラメーターを使います。

 

 

デフォルトの動作は、 $ErrorActionPreference で定義されています。

 

 

通常は Continue のため、エラーをキャッチせず、処理をそのまま続行します。これを変更する場合は、Stop などをこの変数に代入します。

 

$ErrorActionPreference = "Stop"

 

 

 

$ErrorActionPreference で指定できる動作一覧

 

電源ステータス 説明 補足
Continue エラーを表示しますが、可能な場合は次の処理を続ける。 デフォルトの動作になります。
SilentlyContinue エラーを無視します。コンソールにはエラーが表示されません。
Stop エラーを表示し、スクリプトを中断します。
Inquire エラーに対しての動作をユーザーに聞いてきます。 処理を継続するか中断するかを聞いてきます。

 

 

それでは、実際に Stop を指定して使ってみましょう。結果は -ErrorAction Stop と同じです。

 

$ErrorActionPreference = "Stop"

$UpdatedEmployee = Get-Content "C:\scripts\HR\CompanyEmployees.txt"
$CurrentEmployee = @("emp1", "emp2", "emp3", "emp10")

foreach ($emp in $CurrentEmployee) {

    if ($UpdatedEmployee -notcontains $emp) {

        Write-Host "削除されました。"

    } else {

        Write-Host "更新されました。"

    }

}


# 実行結果

Get-Content : パス 'C:\scripts\HR\CompanyEmployees.txt' が存在しないため検出できません。
発生場所 行:3 文字:20
+ $UpdatedEmployee = Get-Content "C:\scripts\HR\CompanyEmployees.txt"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\scripts\HR\CompanyEmployees.txt:String) [Get-Content], It 
emNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetContentCommand

 

 

 

$ErrorActionPreference = “Inquire” の場合は以下の画面のように、ポップアップが表示され、エラーに対しての動作を選択するように聞いてきます。

 

 

 

 

Try Catch を使う

 

 

Try Catch を使い、エラーハンドリングをします。try -> catch -> finally の順で処理されます。

 

 

まずは try で処理をして、エラーが発生したら catch で処理をして、finally は省略可能ですが、エラーの有無関係なく、最後に行う処理です。構文としては以下になります。

 

基本構文

 

try {

    処理

} catch {

    tryの中でエラーが発生した場合は、ここで処理

} finally

    エラーの有無関係なく、最後に行われる処理

}

 

 

 

では、実際にどんな処理ができるのか、サンプルを見てみましょう。とりあえず finally は省略して try と catch だけで書いています。

 

$file Get-Content "sample.txt"

try {
    # 処理
    Get-Help $help -ErrorAction Stop -ErrorVariable $arg

} catch {
    # tryの中でエラーが発生した場合は、ここで処理
    $ErrorMessage = $_.Exception_Message
    $ErrorMessage
    Write-Host "申し訳ございません。お探しのコマンドレット($help)は存在しません。" -ForregroundColor Yellow
    Write-Host "エラー: $error" -ForegroundColor Yellow

}

 

 

 

では、もう少し実践的なものを書いてみましょう。

 

$path = "C:\temp\"
$file = "C:\temp\CompanyEmployees.txt"

try {

    if (Test-Path $path) {

        Write-Output "ファイルを読み込んでいます。"

            Get-Content $file -ErrorAction Stop -ErrorVariable "err"

    } else {
        # throw でエラーメッセージが作れて強制終了する。
        throw "$pathにあるファイルが見つかりません。"

    }

} catch [System.IOException] {

    Write-Output "$pathに対してI/Oエラーが発生しました。"

} catch {

    $ErrorMessage = $_.Exception.Message

    Write-Host "申し訳ございません。お探しのファイル($file)は存在しません。" -ForregroundColor Yellow
    Write-Host "エラー: $err" -ForegroundColor Yellow

} finally {

    Write-Host "アクセス先のファイル($file)は$(Get-Date)に作成されました。" -ForegroundColor green

}


# 実行結果

Reading File
浦島太郎
乙姫
ビルゲイツ
アクセス先のファイル(C:\temp\CompanyEmployees.txt)は07/03/2018 14:42:55に作成されました。

 

 

 

こんな感じで書いてゆきます。なかなか慣れないと難しいですね。

 

 

 

まとめ

 

 

いかがでしょうか。エラーハンドリングを使う事によって、想定外のエラーに対応する事ができ、実行されてはならない処理を防止する事ができます。

 

 

ですので、スクリプトを書く時に、エラーハンドリングが必要かどうかよく考えて必要であれば書くようにすれば良いでしょう。

 

 

では最後までお読みいただきありがとうございました!