Windows PowerShell: 高度な関数のライフサイクル

Windows PowerShell の高度な関数 (通称、スクリプト コマンドレット) は、少し複雑になることもありますが、セットアップ関数とクリーンアップ関数を適切に実行する方法があります。

Don Jones

Windows PowerShell に関するいくつかのオンサイトのクラスで、多くの受講生を混乱させているトピックがあります。今月のコラムでは、読者の皆さんの混乱もいくらか和らげられることを期待して、そのトピックについて詳しく説明をしようと思います。そのトピックは、Windows PowerShell の高度な関数 (通称、スクリプト コマンドレット) です。高度な関数のテンプレートは、次のようなものです。

Function Do-Something { [CmdletBinding()] param( [Parameter(Mandatory=$True, ValueFromPipeline=$True)] [string[]]$computername ) BEGIN {} PROCESS {} END {} }

このコマンドレットには、混乱を招く側面がいくつかあります。この例では、-computername という定義済みの入力パラメーターを使用しており、このパラメーターは、パイプラインから入力を受け取ります。つまり、この関数は 2 とおりの方法で呼び出すことができます。1 つ目は、関数に文字列をパイプラインで渡す方法です。たとえば、1 行につき 1 台のコンピューター名を記載したテキスト ファイルから文字列を渡すことができます。

Get-Content names.txt | Do-Something

2 つ目は、パイプラインをまったく使用せずに、単に 1 つまたは複数のコンピューター名を直接パラメーターに渡す方法です。

Do-Something –computername SERVER1,SERVER2

1 つ目の例では、関数の BEGIN ブロックが最初に実行されます。次に、パイプラインで渡されたコンピューター名ごとに、PROCESS ブロックが 1 回実行されます。$computername 変数には、一度に 1 つのコンピューター名しか格納されません。すべてのコンピューター名の処理が終了したら、最後に END ブロックが 1 回実行されます。

2 つ目の例では、BEGIN ブロックと END ブロックが実行されることはありません。PROCESS ブロックは 1 回しか実行されず、$computername 変数には、パラメーターに渡されたすべてのコンピューター名が格納されます。

この動作の大きな違いが、どちらの場合にも実行される、セットアップ タスクとクリーンアップ タスクを配置するのを困難にしている可能性があります。また、$computername 変数の処理に関する混乱を招くこともあります。1 つ目の例では、$computername 変数には一度に 1 つの値しか格納されませんが、2 つ目の例では、-computername パラメーターに渡されたものによって、1 つまたは複数の値が格納されます。この $computername 変数に関する問題は、次のように、PROCESS ブロックに ForEach ループを追加するだけで解決できます。

Function Do-Something { [CmdletBinding()] param( [Parameter(Mandatory=$True, ValueFromPipeline=$True)] [string[]]$computername ) BEGIN {} PROCESS { Foreach ($computer in $computername) { # use $computer here } } END {} }

この方法なら、$computer 変数には一度に 1 つのコンピューター名しか格納されないことが保証されるので、$computername 変数の代わりに使用した方が良いでしょう。

セットアップとクリーンアップは、少し複雑です。直接 PROCESS ブロックでセットアップ関数を実行するのは、お勧めできません。PROCESS ブロックは、オブジェクトがパイプラインで渡されるときに繰り返し実行されるからです。一方、直接 BEGIN ブロックにセットアップ関数を配置することもできません。何もパイプラインで渡されなかった場合、BEGIN ブロックは実行されないからです。この問題に対処する方法は多数ありますが、次の方法が最善でしょう。

Function Do-Something { [CmdletBinding()] param( [Parameter(Mandatory=$True, ValueFromPipeline=$True)] [string[]]$computername ) BEGIN { $setup_done = $false function DoSomethingSetup { set-variable -name setup_done -value $true -scope 1 } DoSomethingSetup } PROCESS { if (-not $setup_done) { DoSomethingSetup } Foreach ($computer in $computername) { # use $computer here } } }

これは、BEGIN ブロック、PROCESS ブロック、および END ブロックで、同じスコープ (つまり変数) を共有していることを活用した方法です。変数をフラグとして設定することで、BEGIN ブロックの主な内容 (DoSomethingSetup 関数) が 1 回しか呼び出されないことを保証できます。

セットアップ タスクの完了後、DoSomethingSetup 関数で、フラグに $True を設定するようにするには、特別な方法が必要になります。それは、DoSomethingSetup 関数には、固有のスコープがあるからです。通常、スコープ外の変数の値は変更できませんが、Set-Variable コマンドレットを使用すると、必要な変数を明示的に変更できます。

クリーンアップ タスクに同じ方法を適用するのは、もう少し複雑です。1 つ目の例では、オブジェクトが Do-Something 関数にパイプラインで渡され、END ブロックが実行されます。そのため、単純に END ブロックにクリーンアップ タスクを配置できます。しかし、2 つ目の例では、何もパイプラインで渡されないため、END ブロックが実行されることはありません。また、PROCESS ブロックは 1 回しか実行されないので、END ブロックが実行されないことを把握しているとは限りません。

PROCESS ブロックで、END ブロックの関数を呼び出すようにすることはできません。オブジェクトがパイプラインで渡されると、PROCESS ブロックは繰り返し実行されますが、END ブロックを繰り返し呼び出す必要が生じるのは望ましくありません。

この問題を解決するうまい方法はいくつかありますが、多くの場合、単純なアプローチが最善になります。それは、なんらかのクリーンアップ処理が必要なときに (データベース接続を閉じるときなど)、BEGIN ブロックと END ブロックをまったく使用しない方法です。PROCESS ブロックに、1 回だけ実行するように思わせ、PROCESS ブロックにセットアップ タスクとクリーンアップ タスクをすべて配置します。

この方法では、データベースへの接続を開く/閉じる処理が繰り返されるなど、多少の無駄が生じる場合がありますが、それでも効果的であることに変わりありません。また、不自然で追跡するのが困難な、余計なコードを加えないで済みます。

パイプラインを使用するシナリオと使用しないシナリオの両方に対応する高度な関数は、プログラミングが難しくなる場合があります。今後のコラムでは、この問題を他のユーザーが解決してきた方法を、いくつか紹介する予定です。ご自分の環境で使用する新たな選択肢として、お役に立てばさいわいです。

Don Jones

Don Jones は、Microsoft MVP の受賞者で、『Learn Windows PowerShell in a Month of Lunches』(Manning Publications Co.、2010 年) の著者でもあります。この書籍は、管理者が Windows PowerShell を効率的に使用できるようにすることを目的としています。また、一般ユーザーを対象にオンサイトの Windows PowerShell トレーニングも開催しています。Don に対するお問い合わせについては、彼の Web サイト (ConcentratedTech.com、英語) を参照してください。

関連コンテンツ