2017-10-17

PowerShellについて少しは真面目に調べてみた

ちょっとした変換作業を効率化するためにbatを書いていたらファイル名の扱いで困った(空白を含むパスやネットワークパスの扱いが絶望的に面倒くさい)ので代わりにPowerShellでやったら(前回)すんなりうまくいったので 、気をよくしてPowerShellについて少しは真面目に勉強してみました。PowerShellってとっつきにくくて何となく見よう見まねで細部は気にせず適当にやり過ごしていたのですが、知らずに使うのも気持ちが悪いですし。

資料

PowerShellの何が分かりづらいって公式のドキュメントが不足していることですよね。特に言語仕様。結構独特な文法やセマンティックスを持っているのでその辺りの説明が必要です。

探してみると次の場所に3.0の言語仕様書があります(docx形式です)。

最新版は見当たりませんが、この手の仕様書は昔のバージョンの方がシンプルで見やすかったりもしますし我慢します。

日本語で簡単にまとめてくださっているサイトとしては次のサイトがありました。

公式っぽいところだと次のサイトが結構重要なことを書いている気がします。

パイプラインとコマンド実行と関数呼び出し

言語仕様のイントロダクションにも書いてあることですが、PowerShellでとても重要なのはパイプラインだと思います。従来のシェルスクリプトはコマンドの実行によって生じたテキストが次のコマンドへパイプでつながれていきます。しかしPowerShellではコマンドの実行結果はオブジェクトであり、それが次のコマンドへ流れていきます。この一連のオブジェクトが流れる道をパイプラインと呼びます。

PowerShellでのコマンドは、外部のスクリプトや実行ファイルだけでなくユーザーが定義した関数なども含まれます。

コマンドを実行する方法はいくつかありますが、基本的には「コマンド名 引数1 引数2 …」という単純な構文を使います。言語仕様のexpressionを見ても式の中に関数呼び出し構文というのがありません。関数もコマンドですから、式の中ですらカッコで囲んでこの構文を使って呼び出します(メソッド呼び出し構文は式の中にあります。「オブジェクト . メソッド (引数…)」みたいなやつ)。

例えば次のようなtupleという関数。この関数は引数で指定した二つの数を要素とする配列オブジェクトをパイプラインへ出力します。

function tuple($a, $b){$a,$b}

この関数を呼び出すには次のようにコマンドと同じように実行します。

tuple 2 3 #結果は (2, 3) というArray

コマンドを実行した結果、(2, 3)という2要素の配列(Array)オブジェクトが生じるのですが、特に行き先が指定されていないのでコンソールに表示されます。

tuple 2 3 > result.txt

このようにリダイレクトすれば結果をファイルへ出力することも出来ます。

次のように式の中からコマンドを実行して結果を演算することも出来ます。

(tuple 2 3) * 2 + (tuple 4 5) #結果は(2, 3, 2, 3, 4, 5)というArray

「配列 * 整数」は配列の指定回数の繰り返しを作ります。「配列 + 配列」は連結した配列を作ります。

この辺りの事情は外部のスクリプトを呼び出すときでも同じです。例えば次のようなtuple.ps1というファイルを作成します。

# tuple.ps1
$args[0],$args[1]

これを呼び出すには次のようにします。

.\tuple.ps1 2 3 #結果は (2, 3) というArray
.\tuple.ps1 2 3 > result.txt
(.\tuple.ps1 2 3) * 2 + (.\tuple.ps1 4 5) #結果は(2, 3, 2, 3, 4, 5)というArray

重要なのは、こうしてコマンドを実行するとその都度生じた値がパイプラインへ流れるということです。

流す先が実際にどこになるかは呼び出し側によって変わります。コンソール、ファイル、演算子の項、コマンドの引数、変数、などなど。

流したくなければ明示的に破棄する必要があります。

.\tuple.ps1 2 3 > $null
[void](.\tuple.ps1 2 3)
$null = .\tuple.ps1 2 3

このようにすればコマンドを実行して生じた値はどこへも流れず破棄されます。

ちなみに式のトップレベルにある代入演算子は、その結果がパイプラインへ流れません。

$a = "hello" #helloは変数に格納されるのでパイプラインへは流れない

「トップレベル」と断っていると言うことは、トップレベルでなければ流れます。

($a = "hello") # 式のトップレベルはカッコなので代入演算子の結果であるhelloがパイプラインへ流れる。

変数へ代入するたびにいちいち破棄が必要になるのは面倒だからでしょう。意味的にもリダイレクトと同じようにパイプラインの流し先を変数にしただけと考えられます。

この辺りの事情は関数の中でも同じです。

関数というと何か値を返すものと考えますが、PowerShellでももちろんそう考えても良いのですが、返す先は結局はパイプラインなのです。

function compute($filename){
  echo "begin"
  $result = 42 #$result = very_complex_processing $filename の代わり
  echo "end"
  $result
}
compute("example.txt") > result.txt

例えばこのようなcomputeという関数があったとき、result.txtはどのようになるでしょうか。 Rubyあたりをやっていると最後の$result、つまり42だけ出力されそうな気がしてしまいますが、PowerShellでは「begin end 42」が出力されます。

解説をすると次のようになります。

function compute($filename){
  echo "begin" # beginがパイプラインへ流れます。
  $result = 42 # トップレベルにある代入演算子の結果はパイプラインへ流れません。
  echo "end" # endがパイプラインへ流れます。
  $result # $resultの評価結果がパイプラインへ流れます。
}
$r = compute("example.txt") # パイプラインの接続先は変数r。$rは3要素の配列("begin", "end", 42)になります。

この辺りのことが分かっていれば、次の関数もよく理解できるでしょう。

function range($first, $last){
  for($i = $first; $i -le $last; ++$i){
    $i # パイプラインへ$iの値が流れる
  }
}
range 10 15 #結果はArray(10, 11, 12, 13, 14, 15) 10..15と同じ