パイプラインとフィルター

PowerShellでは、あるコマンドレットの出力を次のコマンドレットの入力として活用するパイプライン機能が充実しています。DOSやWindows、UNIX系OSでもパイプラインを使用することができますが、従来のパイプラインはテキストストリームで、文字単位または行単位のテキスト処理しかできませんでした。PowerShellではオブジェクト単位で処理できます。

パイプラインから入力を受け取り、パイプラインに処理結果を出力するプログラムを、特にフィルターと呼びます。たとえば、DOSやWindowsのコマンドプロンプトであれば、sortやmoreがフィルターになります。UNIX系OSでは、他にもgrep、sed、awkなど、数多くのフィルターとなるプログラムを装備しています。従来、こうしたフィルタプログラムを自作するには、CやC++など、本格的な開発環境を必要としていました。しかし、PowerShellではスクリプトやコマンドライン関数でも簡単にフィルタを作成できます。

※ パイプラインを略してパイプと呼びます。また、順次データを入力または出力する処理をストリーム(stream: 流れ)と呼びます。

フィルター機能を持つ関数を作成

まず、簡単なフィルター関数をPowerShellコマンドラインで作成してみましょう。関数を作ることができれば、スクリプトを作成するのも簡単です。

※ 関数の作成については当連載の第6回、スクリプトの作成については第7回を参照して下さい。

関数内でパイプラインからの入力を利用する方法の一つは、$inputの活用です。たとえば、以下の関数testは...全く意味のないことですが...パイプラインの入力を何もせずにパイプラインに出力します。

funcion test { return $input }

1..4をパイプラインでtestに送り、testがそのまま出力。

※ 2つの整数を「..」で結ぶと、PowerShellはその範囲の連続した整数を自動生成します。つまり、「1,2,3,4」と「1..4」は同じ結果となります。

$inputに含まれる複数のオブジェクトを1つずつ取り出して処理するには、foreach構文と組み合わせて使用します。foreachキーワードの使い方は次のようになります。

foreach ( 変数  in  コレクション )  {  繰り返す処理  }

※ 当連載の第8回で紹介したForEach-Objectと混同しないように注意して下さい。ForEach-Objectはコマンドレットですが、ここで説明するforeachはifやwhileと同様のPowerShell構文のステートメントです。なお、ForEach-Objectのエイリアス名もforeachです。コマンドラインで使用する場合、PowerShellは行頭にあるforeachをステートメント、パイプラインにあるforeachをForEach-Objectのエイリアス(別名)と、状況に応じて解釈します。

たとえば、パイプラインから入力された1つ1つの数値を受け取って、その平均を算出するプログラムは次のようになります。

function test {
    $i = $sum = 0
    foreach ($num in $input ) {
        $i += 1
        $sum += $num
    }
    return $sum / $i
}

始めに変数$iと$sumを0に初期化します。

$inputはパイプラインで受け取ったオブジェクトのコレクション(集合)です。その中から、1つずつ$numに代入して繰り返し処理を行います。$iをカウントすることで受け取ったオブジェクトを数え、$sumに足し込みます。最後に$sumを$nで割って、returnで平均値を返します。

実行結果。10,15,13,14,12,12,16,13の平均を表示。

※ このサンプルプログラムは0除算発生時の例外処理をしていませんので、パイプラインから何も受け取れないときにはエラーになります。

フィルタ用ブロックbegin、process、end

フィルタ定義専用のブロックbegin、process、endを使うと、もっと簡単に記述できます。

function 関数名 {
    begin { 前処理 }
    process { 繰り返し処理 }
    end { 後処理 }
}

begin、endブロックは不要であれば省略できます。

※ {}で囲まれた範囲をブロックと呼びます。多くのプログラム言語と同様、ブロックはプログラムの単位の一つであり、内には複数のステートメント(式)を記述できます。

パイプラインから受け取った個々のオブジェクトは、ForEach-Objectと同様に$_変数で利用します。

最初のプログラム例をbegin、process、endブロックを使って書き換えると、次のようになります。

function test2 {
    begin {
        $i = $sum = 0
    }
    process {
        $i += 1
        $sum += $_
    }
    end {
        return $sum / $i
    }
}

実行結果はtestと同じです。

最初に紹介した$inputを使う方法に比べると、begin、process、endブロックを使う方法は、記述を単純化でき、プログラムの見通しも良くなってプログラミングミス防止にもなります。反面、スタイルが固定化されるため、プログラムの自由度がややせばまります。

$inputを使う方法でも、begin、process、endブロックを使う方法でも、上記サンプルプログラムの関数定義部分(再外側の「function 関数名 {」と「}」)を外して、中身だけ拡張子.ps1のテキストファイルに記述すれば、そのままスクリプトとして利用できます。

スクリプトファイルを作成するときは、スクリプトファイル自体が1つの関数のように働きますので、関数定義部分は不要です。average.ps1として保存。

フィルタ作成専用キーワードfilter

さらにfilterキーワードを使ってフィルタ専用の関数を作成することもできます。filterキーワードで定義するときは、前述のfunction関数定義のprocessブロック部分だけを記述します。これはForEach-Objectのスタイルとよく似ています。

filter  関数名 { 繰り返し処理 }

たとえば、パイプラインで受け取った数値が正か負かゼロかを表示する関数を記述すると、次のようになります。

filter test3 {
    if ($_ -gt 0) {
        $_.toString() + " は正の数。"
    } elseif ($_ -lt 0) {
        $_.toString() + " は負の数。"
    } else {
        $_.toString() +  " はゼロ。"
    }
}

パイプラインで受け取った個々の数値について正、負を判定し、メンバ関数toString()で文字列化して表示するメッセージを作成しています。

実行結果。-3から3までの整数をパイプラインで受け取り、正負を判定して表示します。

繰り返し部分だけを記述すればいいので、begin、process、endブロックを使うよりさらに単純化できますが、プログラムの自由度はきわめて限られます。

functionキーワードのようにfilterキーワードで指定する外側のブロックをはずしてスクリプトを作成することはできません。