Windows PowerShellフィルタ処理の力
Don Jones
先月のコラムでは、あるコマンドレットから別のコマンドレットにデータのセット (厳密には、オブジェクトのストリームですが) を渡し、まさに必要とするものになるまでそのデータ セットを絞り込むという、Windows PowerShell パイプラインの威力と柔軟性について説明しました。その説明の中で、コマンドレットだけではなく独自のスクリプトでも
パイプラインを利用できることをほのめかしました。今月は、このトピックについて詳しく説明します。
私が Windows PowerShell™ でもっとも頻繁に行っている作業の 1 つは、複数のリモート コンピュータを操作するスクリプトの記述です。その際、通常は Windows® Management Instrumentation (WMI) を使用します。リモート コンピュータを扱う他のタスクと同様に、リモート コンピュータのいずれかまたは複数が、スクリプトを実行したときに使用できない状態である可能性は常にあります。そのため、この問題に対処できるようなスクリプトを作成することが必要です。
もちろん、スクリプトに WMI 接続のタイムアウトを処理する機能を持たせる方法はたくさんあるのですが、タイムアウト期間は既定で約 30 秒と非常に長いため、このアプローチはあまり好きではありません。タイムアウト期間が長いと、多数のタイムアウトを待機する必要が生じた場合に、スクリプトの実行速度が低下することになります。代わりに、特定のコンピュータに接続する前に、そのコンピュータが実際にオンラインになっているかどうかを確認するための簡単なチェック機能をスクリプトに持たせようと思います。
Windows PowerShell のパラダイム
通常、私が VBScript などの他のスクリプト言語を使用する場合、一度に処理するコンピュータは 1 台です。つまり、コンピュータ名を 1 つ取得し (おそらく、テキスト ファイルに保存された名前の一覧から名前を取得し)、そのコンピュータに対して ping を実行して、そのコンピュータが使用できるかどうかを確認します。そのコンピュータが使用できる場合は、WMI 接続を行って、完了する必要があるその他すべての処理を実行します。これは、スクリプトを使用した一般的なアプローチです。実際には、ループ処理の中にすべてのコードを記述し、接続する必要があるすべてのコンピュータごとに、そのループ処理を実行します。
けれども、Windows PowerShell は、オブジェクトを基準とし、オブジェクトのグループやコレクションを直接操作できるので、バッチ処理に適しています。Windows PowerShell のパラダイムは、単一のオブジェクトやデータの一部を扱うというものではなく、グループ全体を扱い、すべての目的を達成するまで少しずつグループを絞り込んでいくというものです。たとえば、一覧からコンピュータ名を 1 つずつ取得する代わりに、名前のコレクション全体を 1 回で読み取る方法があります。また、ループ処理で各コンピュータに対して ping を実行するのではなく、名前のコレクションを受け取り、それらの名前に対して ping を実行した後、通信可能なコンピュータを出力するという 1 つのルーチンを記述できます。次の手順は、残っている名前 (つまり ping 経由で到達できたコンピュータ) について WMI 接続を行うことです。
Windows PowerShell では、このアプローチがさまざまなタスクで使用されます。たとえば、次のようなコードを使用して、実行中のサービスの一覧を入手できます。
Get-Service | Where-Object { $_.Status –eq "Running" }
図 1 は、このコードを私のコンピュータで実行した場合の出力を示しています。各サービスを 1 つずつチェックするのではなく、Get-Service を使用してすべてのサービスを取得し、取得したサービスを Where-Object にパイプしてから、実行されていないサービスを除外しました。これが、今回のスクリプトで行おうと考えていることのアウトラインです。つまり、コンピュータ名の一覧を取得し、ping に応答しないすべてのコンピュータを除外し、ping に応答するコンピュータの一覧を次の手順に渡すことです。
図 1** ping に応答するコンピュータの一覧の取得 **(画像を拡大するには、ここをクリックします)
フィルタ処理関数
この処理を実行する独自のコマンドレットを記述することはできますが、それはしません。コマンドレットを作成するには、Visual Basic® や C# を使用する必要があり、Microsoft® .NET Framework 開発に関する相当量の専門的知識が必要だからです。さらに重要なのは、このタスクに対してかけるつもりの労力よりも、はるかに多くの作業が必要になるからです。さいわい、Windows PowerShell では、パイプライン内で機能する "フィルタ" と呼ばれる特殊な関数を記述できます。フィルタ処理関数の基本的な概要は次のようになります。
function <name> {
BEGIN {
#<code>
}
PROCESS {
#<code>
}
END {
#<code>
}
}
ご覧のように、この関数には BEGIN、PROCESS、および END という名前の、独立した 3 つのスクリプト ブロックがあります。フィルタ処理関数 (つまり、パイプライン内でオブジェクトをフィルタ処理するようにデザインされた関数) では、必要に応じてこれら 3 つのスクリプト ブロックをさまざまな組み合わせで使用できます。これらのブロックは次のように動作します。
- BEGIN ブロックは、関数が最初に呼び出されたときに 1 回実行します。必要に応じて、このブロックを使用してセットアップ処理を行うことができます。
- PROCESS ブロックは、関数に渡されたパイプライン オブジェクトごとに 1 回実行します。$_ 変数は、現在のパイプラインの入力オブジェクトを表します。PROCESS ブロックは、フィルタ処理関数の必須ブロックです。
- END ブロックは、すべてのパイプライン オブジェクトの処理が完了した後で 1 回実行します。必要に応じて、このブロックを使用して任意の終了処理を行えます。
今回の例では、入力オブジェクトとして名前のコレクションを受け取るフィルタ処理関数を作成し、各コンピュータに ping を実行することを考えています。ping に正常に応答する各コンピュータは、パイプラインに出力されます。ping に応答しないコンピュータは、単に除外されます。ping 機能には特殊なセットアップや終了処理はまったく必要ないので、PROCESS スクリプト ブロックだけを使用します。図 2 に完全なスクリプト コードを示します。
Figure 2 Ping-Address 関数と Restart-Computer 関数
1 function Ping-Address {
2 PROCESS {
3 $ping = $false
4 $results = Get-WmiObject -query `
5 "SELECT * FROM Win32_PingStatus WHERE Address = '$_'"
6 foreach ($result in $results) {
7 if ($results.StatusCode -eq 0) {
8 $ping = $true
9 }
10 }
11 if ($ping -eq $true) {
12 Write-Output $_
13 }
14 }
15 }
16
17 function Restart-Computer {
18 PROCESS {
19 $computer = Get-WmiObject Win32_OperatingSystem -computer $_
20 $computer.Reboot()
21 }
22 }
23
24 Get-Content c:\computers.txt | Ping-Address | Restart-Computer
Ping-Address および Restart-Computer という 2 つの関数を定義していることに注意してください。Windows PowerShell では、関数を呼び出すには、その関数が定義されている必要があります。そのため、スクリプトで最初に実行される行は、24 行目になります。この行では、Get-Content コマンドレットを使用して、1 行に 1 つのコンピュータ名が記載されたファイルからコンピュータ名の一覧を取得しています。この一覧 (技術的には String オブジェクトのコレクション) は、ping に応答しないコンピュータを除外する Ping-Address 関数にパイプされます。その結果は、ping に応答する各コンピュータを、WMI を使用してリモートで再起動する Restart-Computer にパイプされます。
Ping-Address 関数は、PROCESS スクリプト ブロックを実装しています。これは、Ping-Address 関数に、オブジェクトの入力コレクションを渡す必要があることを意味します。PROCESS スクリプト ブロックでは、入力オブジェクトが自動的に処理するので、私は入力を保持するための入力引数を定義する必要はありませんでした。3 行目で、変数 $ping を $false に設定して処理を開始しています ($false はブール値 False を表す組み込みの Windows PowerShell の変数です)。
次に、ローカルの Win32_PingStatus WMI クラスを使用して、指定のコンピュータに対して ping を実行します。5 行目では、現在のパイプライン オブジェクトを表す $_ 変数が WMI クエリ文字列に埋め込まれています。Windows PowerShell では、文字列が二重引用符で囲まれている場合、$_ などの変数を変数自体の内容で置換する試行が必ず行われるので、文字列の連結に時間を浪費せずに済みます。この機能を 5 行目で使用しています。
6 行目は ping の結果をチェックするループ処理です。結果が正常に返される場合 (つまり、StatusCode がゼロ (0) の場合) には、$ping を $true に設定して、ping に応答があったことを示します。11 行目では、$ping が $true に設定されているかどうかをチェックしています。$ping が $true に設定されている場合、最初の入力オブジェクトを既定の出力ストリームに出力します。Windows PowerShell では、既定の出力ストリームは自動的に管理されます。この関数がパイプラインの最後にある場合は、この出力ストリームがテキスト形式に変換されます。パイプラインに後続のコマンドがある場合は、出力ストリームのオブジェクト (この例では、コンピュータ名を格納している文字列オブジェクト) が次のパイプライン コマンドに渡されます。
Restart-Computer は少々単純な関数ですが、この関数でも PROCESS ブロックが使用されているのでパイプラインに簡単に参加できます。Restart-Computer 関数は、19 行目で指定のコンピュータに接続し、そのコンピュータの Win32_OperatingSystem WMI クラスを取得しています。そして、20 行目で、そのクラスの Reboot メソッドを実行してリモート コンピュータを再起動しています。
繰り返しになりますが、24 行目で、すべての処理が実際に実行されています。このスクリプトを実行するときは十分に注意する必要があることは言うまでもありません。c:\computers.txt に名前が記載されているすべてのコンピュータを再起動するスクリプトなので、このテキスト ファイルに記載されている名前に注意を払わなかった場合、悲惨な結果になることが十分に考えられます。
次のステップ
このスクリプトは、完璧とは言えません。リモート コンピュータ上で必要なポートをブロックするファイアウォールなど、基本的な接続に関連しない WMI エラーや WMI セキュリティ エラーが発生する可能性も視野に入れる必要があります。このような問題には、トラップ ハンドラを実装することによって対処できます。このトピックは、今後のコラムで詳しく説明するのに打って付けでしょう。
また、今回のスクリプトでは、リモート コンピュータの再起動というかなり重大な操作を実行していますが、このような危険性の高い操作を行うスクリプトでは、Windows PowerShell の一般的なパラメータである -Confirm と -WhatIf の 2 つを実装する必要があります。スペースの都合上、この実装方法を今回のコラムで説明することはできませんが、このトピックについても、今後のコラムで取り上げることにしましょう。それまでは、Windows PowerShell チームのブログを参照してください。アーキテクトの Jeffrey Snover がこのトピックについて説明しています。
このコラムで紹介した機能を使用しなくても、フィルタ処理関数で行える操作については理解していただけたと思います。今後のコラムでは、この技法を掘り下げて、関数により、スクリプトの機能が全体的に向上するだけではなく、再利用可能なコードをモジュール化する優れた方法がどのようにして提供されるのかを紹介します。
Don Jones は SAPIEN Technologies のリード スクリプト グルであり、『Windows PowerShell: **TFM』(SAPIEN Press、2007 年) の共著者でもあります。連絡先については、www.ScriptingAnswers.com にアクセスしてください。
© 2008 Microsoft Corporation and CMP Media, LLC. All rights reserved; 許可なしに一部または全体を複製することは禁止されています.