1. ホーム
  2. スクリプト・コラム
  3. パワーシェル

PowerShellのエラートラッピングの話

2022-01-08 08:13:06

前回は、Windows PowerShellを使って、かなり高度なマニフェストツールを構築する方法を紹介しました。今回作成したツールは、シェルの組み込み機能とオブジェクトへの関数の適用により、出力に関していくつかのオプションを提供します。

私が作成した関数には紛れもない弱点があります。それは、発生しうるエラー(接続や権限の問題など)に穏便に対処できないことです。そこで今回は、Windows PowerShellのコラムとして、Windows PowerShellが提供するエラー処理機能を取り上げます。

Trapの設定

Windows PowerShell では、Trap キーワードはエラーハンドラを定義します。スクリプトで例外が発生すると、シェルは Trap が定義されているかどうかを確認します。これは、例外が発生する前にスクリプトに表示される必要があることを意味します。このデモでは、接続性の問題を発生させるテストスクリプトを作成します。Get-WmiObjectを使用して、ネットワークに存在しないコンピュータ名に接続する予定です。私の目標は、エラートラップが無効なコンピュータ名をファイルに書き出し、無効なコンピュータ名が記録されたファイルを提供することです。また、2つの有効なコンピュータ(localhostを使用する)への接続を追加する。図1のスクリプトをご覧ください。

トラップの追加

trap {
 write-host "Error connecting to $computer" -fore red
 "$computer" | out-file c:\demo\errors.txt -append 
 continue
}

$computer = "localhost"
get-wmiobject win32_operatingsystem -comp $computer 

$computer = "server2"
get-wmiobject win32_operatingsystem -comp $computer 

$computer = "localhost"
get-wmiobject win32_operatingsystem -comp $computer



このスクリプトの出力は(図2に示すように)私が期待したものとは異なっています。Error connecting to..."のメッセージは表示されないことに注意してください。Errors.txtファイルも作成されていません。どうなっているのでしょうか?

図2 期待していた出力とは違う!

止まれ!

重要なのは、通常のシェルのエラーメッセージと例外(非終了エラーと終了エラーに分けられる)は別物であることを理解することです。終了エラーはパイプラインの実行を停止させ、例外を発生させます)。キャッチできるのは例外のみです。エラーが発生すると、シェルは組み込みの $ErrorActionPreference 変数をチェックし、実行したいアクションを決定します。この変数は、デフォルトで "Continue" という値を含んでおり、これは "display error message and continue" を意味します。この変数を "Stop" に変更すると、エラーメッセージが表示され、キャッチ可能な例外が生成されます。しかし、これはスクリプト内で発生したエラーもその動作を行うことを意味します。

より良い方法は、問題を引き起こす可能性があると思われるコマンドレットにのみ "stop" の動作を使用させることです。これは、すべてのコマンドレットでサポートされている共通のパラメーターである、-ErrorAction (または -EA) パラメーターを使用して行うことができます。図3は、このスクリプトの修正版を示しています。これは期待通りに動作し、図 4 に示すような出力を生成します。

 ErrorActionを使用する

trap {
 write-host "Error connecting to $computer" -fore red
  "$computer" | out-file c:\demo\errors.txt -append 
 continue
}

$computer = "localhost"
get-wmiobject win32_operatingsystem -comp $computer -ea stop

$computer = "server2"
get-wmiobject win32_operatingsystem -comp $computer -ea stop

$computer = "localhost"
get-wmiobject win32_operatingsystem -comp $computer -ea stop



図4 -ErrorAction パラメータを使用すると、より有用な結果を得ることができる

Trapの最後にContinueを使用すると、例外を発生させたコードの行の後に続けて実行するようにシェルに指示します。また、キーワード Break を使用することもできます(これについては後ほど説明します)。また、変数 $computer (スクリプトで定義) は Trap の内部でまだ有効であることに注意してください。これは、Trapがスクリプト自体のサブスコープであるためです。つまり、Trapはスクリプト内のすべての変数を見ることができます(これについても後で詳しく説明します)。

すべてをスコープで行う

Windows PowerShellのエラーキャッチの中で特に厄介なのが、スコープの使い方です。シェル自体はグローバルスコープを表し、シェル内部で発生するすべてのイベントを含みます。スクリプトを実行すると、それ自身のスクリプトスコープを取得します。関数を定義すれば、その関数の内部が独自の特殊スコープになる、など。これによって、親子型の階層が形成されます。

例外が発生すると、シェルは現在のスコープでTrapを探します。つまり、関数内で例外が発生すると、その関数内でTrapを探し、Trapが見つかればそれを実行します。TrapがContinueで終わっていれば、シェルは例外が発生したコード行の次の行を実行しますが、まだ同じスコープ内です。これは、以下の少量の擬似コードで説明されています。

Trap {
 # Log error to a file
 Continue
}
Get-WmiObject Win32_Service -comp "Server2" -ea "Stop"
Get-Process


5行目で例外が発生した場合、1行目のTrapが実行されます。TrapはContinueで終わっているので、6行目で続行されます。

では、次のような少し変わったスコープの例を考えてみましょう。

 Trap {
  # Log error to a file
  Continue
 continue}
 
 Function MyFunction {
  Get-WmiObject Win32_Service -comp "Server2" -ea "Stop"
  Get-Process
 }
 
 MyFunction
 Write-Host "Testing!"


7行目でエラーが発生した場合、シェルは関数のスコープでTrapを探します。見つからなかった場合、シェルは関数のスコープを出て、親のスコープでTrapを探し続けます。この場合、コードはContinueなので、同じスコープで例外の後のコード、つまり8行目ではなく12行目を実行し続けることになります。つまり、シェルは関数を終了した後、再入力することはありません。

では、この動作を次の例と比較してみてください。

Function MyFunction {
 Trap {
  # Log error to a file
  Continue
 }
 Get-WmiObject Win32_Service -comp "Server2" -ea "Stop"
 Get-Process
MyFunction}
 
MyFunction
Write-Host "Testing!"


この例では、6行目のエラーで2行目のTrapが実行され、関数のスコープに残ります。Continueキーワードはそのスコープに残り、7行目の実行を継続します。エラーが発生すると予想されるスコープにTrapを置くと、スコープ内に残り、その中で実行を継続できるという利点があります。しかし、この方法があなたの状況に合わなかった場合はどうでしょうか?

compare-Object(またはDiff)は、2つのオブジェクトのセットを比較するために設計されたもので、設定ベースラインを管理するのに適しています。デフォルトでは、各オブジェクトのすべてのプロパティを比較し、そのコマンドからすべての差分を出力します。そこで、あるサーバーのサービスを思い通りに設定したとします。以下を実行するだけで、ベースラインが作成されます。

Get-Service | Export-CliXML c:\baseline.xml


ほぼすべてのオブジェクトをExport-CliXMLに渡すことができ、オブジェクトをXMLファイルに変換することができます。その後、同じコマンド(例:Get-Service)を実行し、その結果を保存したXMLと比較することができます。コマンドは次のようなものです。

Compare-Object (Get-Service) (Import-CliXML 
 c:\baseline.xml) -property name


propertyパラメータを追加すると、オブジェクト全体ではなく、そのプロパティのみを比較対象とするようになります。この例では、オリジナルのベースラインと異なるすべてのサービス名のリストを取得し、ベースラインが作成された後にサービスが追加または削除されたかどうかを知ることができます。

接続を解除する

先ほど、Breakキーワードについて触れました。図5は、Breakキーワードの使用例です。

Breakキーワードの使用

 Trap {
  # Handle the error
  Continue
 continue}
 
 Function MyFunction {
  Trap {
   # Log error to a file
   If ($condition) {
    Continue
   } Else {
    Break
   }
  }
  Get-WmiObject Win32_Service -comp "Server2" -ea "Stop"
  Get-Process
 }
 
 MyFunction
 Write-Host "Testing!"


以下は実行チェーンの概要です。まず19行目が実行され、6行目の関数が呼び出されます。15行目が実行され、例外がスローされます。この例外は7行目でキャッチされ、9行目でTrapが判断を下す必要があります。conditionがTrueであると仮定して、Trapは16行目で続行します。

しかし、$conditionがFalseの場合、Trapはブレークします。これは、現在のスコープを終了し、元の例外を親に渡します。シェルから見ると、これは、19行目で例外が投げられ、1行目でキャッチされたことを意味します。Continue キーワードは、シェルに 20 行目で継続するように強制します。

実は、どちらのTrapも、エラーを処理したり、ログを取ったりするために、少し多めのコードを含んでいます。今回は、実際のフローを見やすくするために、その関数コードを省略しただけです。

なぜ心配するのか?

どのような場合にエラーをキャッチする必要があるのでしょうか?それは、エラーが発生することを予期しているときと、通常のエラーメッセージ以上の何らかの動作(例えば、エラーをファイルに記録したり、より親切なエラーメッセージを表示したり)が必要なときの2つです。

私は通常、より複雑なスクリプトには、発生が予測されるエラーに対処するためのエラー処理を組み込みます。これらのエラーには、接続不良やパーミッションの問題などが含まれますが、これらに限定されるものではありません。

エラートラッピングを理解するには、間違いなく時間と労力がかかるでしょう。しかし、Windows PowerShellでより複雑なタスクに取り組む際には、より完全でプロフェッショナルなツールを構築するために、エラートラッピングを実装することが重要です。