時代遅れひとりFizzBuzz祭り Windows PowerShell編

時代遅れひとりFizzBuzz祭り、今回はWindows PowerShell。前回のバッチファイルとの繋がりなんて言う必要があるとは到底思えないが、あえて言っておく。PowerShellはポスト・コマンドプロンプトの座に納まることが期待されているCUIツール/スクリプティング環境であり、PowerShellスクリプトはポスト・バッチファイルの座に納まることが期待される言語だ。

とはいえ本格的に普及するとしたらWindows Server 2008Windows 7以降のOSへの置き換えが進んでからになるだろう。Windows XPVistaでも使えるとはいえ、インストールが必要だからなあ。物好きでもない限り、入れないだろう*1

PowerShellの用途というとシステム管理が注目されがちだが、それ以外の使い道もある。プログラマの視点で見ると、PowerShellには次のような側面がある。

ツールとしてのPowerShellコマンドプロンプトの代わりのCUIシェルとして使えるだけでなく、Rubyでいうところのirbのように簡易電卓やワンライナーにも使える*2スクリプト言語としては、ちょっとした自前のツールや社内用の簡易ツールを実装するのにも使えるキャパシティがあるように思う。

個人的には、ツールとしてのPowerShellUnix環境のシェルと比較することは間違ってないが、スクリプト言語としてのPowerShellとの比較対象には相応しくないと思う。むしろREXXと比較した方が良い*3

このエントリではスクリプト言語としての側面を重視したいと思う。ちなみにPowerShell 2.0だ。

まずはC言語系の言語ではオーソドックスだと思われるパターンで書いたFizzBuzzから。

for ($i = 1; $i -le 100; $i++) {
    if ($i % 15 -eq 0) {
        "FizzBuzz"
    } elseif ($i % 3 -eq 0) {
        "Fizz"
    } elseif ($i % 5 -eq 0) {
        "Buzz"
    } else {
        $i
    }
}

C言語的な書き方をしたPerl」に似ているように見えなくもないが、メッセージ出力用のコマンドレットを呼び出していない所が一風変わっている。比較演算子が記号ではないのは、`>'をリダイレクトで使っていることに起因しているのだろうか?

foreachや範囲演算子もあるので、こんな風にも書ける。範囲演算子Perlのように配列を返す。

foreach ($i in 1..100) {
    switch ($i) {
        {$i % 15 -eq 0} {"FizzBuzz"; break}
        {$i % 3 -eq 0}  {"Fizz"; break}
        {$i % 5 -eq 0}  {"Buzz"; break}
        default         {$i; break}
    }
}

switchだけ少し変わった書き方をしている。比較対象の$iを無視して、条件式をそのまま記述している。この使い方はRubyのcaseやGoのswitchに通じるものがある気がしないでもない。

さて、PowerShellといえばオブジェクト・パイプだろう。パイプを使うとこんな風に記述できる。

1..100 | ForEach-Object {
    switch ($_) {
        {$_ % 15 -eq 0} {return "FizzBuzz"}
        {$_ % 3 -eq 0}  {return "Fizz"}
        {$_ % 5 -eq 0}  {return "Buzz"}
        default         {return $_}
    }
}

ForEach-Objectはパイプを通じて取得できる個々のオブジェクトに対して処理を適用する。ForEach-Object以外にも、SQLクエリ的な使い方で指定した条件を満たすオブジェクトのみ取り出すWhere-Objectや、各オブジェクトの特定のプロパティのみを取り出すSelect-Objectなどがある。

ForEach-Objectは頻繁に使われるので、foreachという別名(エイリアス)が設定してある。

function Get-FizzBuzzValue {
    $n = $Args[0]
    switch ($n) {
        {$n % 15 -eq 0} {return "FizzBuzz"}
        {$n % 3 -eq 0}  {return "Fizz"}
        {$n % 5 -eq 0}  {return "Buzz"}
        default         {return $n}
    }
}

$result = 1..100 | foreach {Get-FizzBuzzValue $_}
$result

それと、バッチファイルに慣れている人には盲点かもしれないが、パイプで処理した結果のオブジェクトを変数に代入することもできる*4

関数が定義できる点も、バッチファイルに慣れている身には嬉しい。やっとシェルスクリプトに追いついた。いや、変数がスコープを持っているという点では、/bin/shの素のシェルスクリプトよりも便利だ。

ForEach-Objectにはもう一つエイリアスが設定してある。`%'だ。

function Get-FizzBuzzValue($n) {
    switch ($n) {
        {$n % 15 -eq 0} {return "FizzBuzz"}
        {$n % 3 -eq 0}  {return "Fizz"}
        {$n % 5 -eq 0}  {return "Buzz"}
        default         {return $n}
    }
}

1..100 | % {Get-FizzBuzzValue $_}

しかしここまでくると、慣れないと読み難いかもしれない。

関数とは別に、パイプライン経由のオブジェクトを処理するフィルタを定義することもできる。

filter Get-FizzBuzzValue {
    $s = ""
    switch ($_) {
        {$_ % 3 -eq 0}  {$s += "Fizz"}
        {$_ % 5 -eq 0}  {$s += "Buzz"}
        default         {$s = $_}
    }
    return $s
}

1..100 | Get-FizzBuzzValue

Get-FizzBuzzValueを関数として定義していた時と違い、ForEach-Objectの呼び出しが不要となっている。

パイプは基本的にオブジェクトのコレクションからコレクションへの変換で使用される。パイプを使ってワンライナーするのは、Rubyでいうとワンライナーで配列から配列にデータを変換していく感じに近い。

コレクションからコレクションへの変換以外の処理もワンライナーに含めたい場合、例えばこんな風になる。

filter Get-FizzBuzzValue {
    $s = ""
    switch ($_) {
        {$_ % 3 -eq 0}  {$s += "Fizz"}
        {$_ % 5 -eq 0}  {$s += "Buzz"}
        default         {$s = $_}
    }
    return $s
}

(1..100 | Get-FizzBuzzValue) -join ", "

この例では、配列を連結して1つの文字列にしている。

出力の見栄えを気にして、意味もなくOut-GridViewを使うこともできる。

filter Get-FizzBuzzValue {
    $s = ""
    switch ($_) {
        {$_ % 3 -eq 0}  {$s += "Fizz"}
        {$_ % 5 -eq 0}  {$s += "Buzz"}
        default         {$s = $_}
    }
    return $s
}

1..100 | Get-FizzBuzzValue | Out-GridView

但しこのコードを単純にスクリプトとして切り出して実行しても、Out-GridViewによる表示は一瞬で消えてしまう*5。何か良い方法はないだろうか?

ところで、今までのコードは基本的にPowerShell内部の機能のみで実装していたのだが、バッチファイルのように外部コマンドを使うこともできる。例えば手元の環境にはUnix由来のseqがインストールされているのだが、それを使ってこんな風に書くことも可能だ。

filter Get-FizzBuzzValue {
    $s = ""
    switch ($_) {
        {[int]$_ % 3 -eq 0} {$s += "Fizz"}
        {[int]$_ % 5 -eq 0} {$s += "Buzz"}
        default             {$s = $_}
    }
    return $s
}

seq.exe 1 100 | Get-FizzBuzzValue

seqの出力は文字列の配列として扱われるので、Get-FizzBuzzValue内で文字列から数値に変換してから判定している。

ただseqを使うためにGet-FizzBuzzValueに手を加えるのも微妙な話だ。わざわざ入力されるオブジェクトの型ごとにフィルタを用意して使い分けるのは無駄だ。ForEach-Objectで事前に全て文字列から整数に変換しておく方が良いだろう。

filter Get-FizzBuzzValue {
    $s = ""
    switch ($_) {
        {$_ % 3 -eq 0} {$s += "Fizz"}
        {$_ % 5 -eq 0} {$s += "Buzz"}
        default             {$s = $_}
    }
    return $s
}

seq.exe 1 100 | % {[int]$_} | Get-FizzBuzzValue

こういった「データの変換」――旧来のプログラミング言語でいう型変換だとか、オブジェクト指向プログラミング的な世界でのオブジェクトの変換は、プログラマ的視点ではごく普通のことだと思うのだが、それ以外の人々の目にどう映るのか気になるところだ。

Windows上で作業するプログラマにとってPowerShellは有益なツールだろうし、比較的受け入れやすいと思う*6。その反面、コマンドプロンプトやバッチファイルの世界とは大きく違う訳で、例えばプログラミングにあまり通じていないシステム管理者がバッチファイルからPowerShellに乗り換えるようなケースでは、マニュアル片手に独学では難しいのではないかと思う。何かよいドキュメントが必要となるだろう。

WSHシェルスクリプトもそうだが、HOWTO本の類は山のようにあるが、言語としての側面にフォーカスした本はなかなか見つからない。しかしスクリプト言語として高機能になればなる程、言語機能をしっかり学んでおかないとHOWTO本の内容に正しく手を加えることすらおぼつかなくなってしまう*7PowerShell 2.0対応の良質なドキュメント、例えばPowerShellでプログラミング入門するような本を誰か書いてくれないだろうか?

*1:私は物好きで且つ興味を持ったのでインストールしましたが、何か?

*2:PowerShell上で試しに「10 + 3」とタイプして実行してみれば分かる。

*3:Unixのシェルがもたらす世界では、外部ツールとスクリプト言語としての機能がほぼ完全に融合されて一つになっている。外部ツールがどのような言語で実装されていても、だ。しかしPowerShellの世界では、関数やコマンドレットとして実装したもの以外のツールは、(他の言語と比べるとうまい具合に簡単にPowerShell上で実行できるようになっているとはいえども)PowerShellの世界観から微妙に浮いている。この点でREXXと通じるものがあるので、「REXXと比較」云々と書いている。

*4:盲点だったのは私だけ?

*5:PowerShellのシェル上でOut-GridViewを使う分には問題ないのだが。

*6:私のような平均より下のプログラマでも興味深く感じたし、短時間であまり振り回されることなくそこそこ使えるようになったぐらいなので。

*7:例えばVBScriptに通じていない人が、ネット上のVBScriptによるHOWTO系のサンプルコードを組み合わせて作ったツールは、それなりに動作はするもののメンテナンスは低かったりする。VBScriptの言語本体に関する知識が不足しているが為に(サンプルコードも含めて)本来なら1行で済むところが10行になっていて、且つそんな箇所があちらこちらに存在するのだ。しかも元のサンプルコードに問題があっても、(問題の種類によっては)VBScriptに通じていないが為に見逃していたりする。正直うんざりだ。