2015/09/07
本当は怖いPowerShell その2 コマンド名の"Get-"補完
Twitterでこんな問題を出してみました。
PowerShell検定中級編。以下を実行するとそれぞれ何が起こるか。 ① &{} ② &{process} ③ &{process{}} ④ &{process{process}} ⑤ &{process{process{}}}
? 牟田口大介 (@mutaguchi) September 7, 2015
以下、解答になります。
① &{}
結果
何も出力されません。
解説
空のスクリプトブロック{}を実行演算子&で実行しています。空なので何も出力はありません。
② &{process}
結果
「'process' の後にステートメント ブロックがありません。」というパーサーのエラーになります。
解説
PowerShellのスクリプトブロックは、beginブロック、processブロック、endブロックを内包します。スクリプトブロック直下にparamブロック、DynamicParamブロック、beginブロック、processブロック、endブロック(他にもあったかも)以外のステートメントを記述すると、Endブロック内に記述されたものと暗黙的に解釈されます。
この場合、スクリプトブロック直下にprocess…と書き始めたので、パーサーはprocessブロックが開始されたと判断しますが、続くステートメントブロック{}(≠スクリプトブロック)の記述がないため、構文エラーとなります。
③ &{process{}}
結果
何も出力されません。
解説
パーサーは②のように解釈しますが、今回はステートメントブロック{}がきちんと記述されているので、エラーなく解釈されます。
processブロックは、パイプライン入力がない場合でも1回実行されますが、この場合、中身は空なので、①と同様、何も出力はありません。
④ &{process{process}}
結果
Get-Processコマンドレットが実行され、プロセス一覧が表示されます。
解説
・パーサーの挙動
③までの解説の通り、&{process{…}}とすると、…の部分が1回実行されます。今回はprocessブロック内に「process」と記述しているので、②のようなパーサーエラーは発生せず、「process」がステートメントとして実行されます。
さて、PowerShellのステートメント(文)には「For」とか「If」とかと並列して、「パイプライン」が存在します。「パイプライン」には1つの「式」もしくは複数の「コマンド」が含まれます。
たとえば、「Get-ChildItem | Select-Object Name」というパイプラインには「Get-ChildItem」と「Select-Object Name」という2つのコマンドが含まれます。
(ちなみに、「式」とは「$x+1」とかの、値を返すもののことです。PowerShellではパイプラインの最初の要素にのみ、「コマンド」ではなく「式」を記述することができます。)
今回のお題では、「process」はprocessブロック下に記述されており、ForやIf等のステートメントではないのでパイプラインとして扱われます。このパイプラインには1つの要素のみ含まれていますが、式ではないので、コマンドとして解釈されます。
・コマンド探索の挙動
PowerShellの「コマンド」は、関数、コマンドレット、ワークフロー、Configuration、ファイル(実行ファイル、スクリプトファイルを含む)、&演算子で実行するスクリプトブロック等が挙げられます。
コマンドの探索は、まずコマンドへのエイリアスを探します。ない場合は、関数名orコマンドレット名を探します。それでもない場合は、実行ファイルやスクリプトファイルの拡張子(.exe、.ps1等)を付与してパスの通ったディレクトリを探します(ちなみにカレントディレクトリにあったとしても、相対パスor絶対パス表記でない場合は実行しません)。
さて、ここからが「本当は怖い」ところなんですが、ここまで探索してコマンドがなかった場合、与えられたコマンド名に"Get-"を付与してもう一度探索します。
今回のお題では、processという名前のコマンドを探して、もしパスが通ったフォルダにprocess.exeとかがあればそれが実行されますが、ない場合はGet-Processというコマンド名を探します。
もちろん、Get-Processというコマンドレットは標準で存在するので、それが実行されてしまう、というわけでした。
(ちなみにPowerShell 3.0以降なら、Get-付与で見つからない場合、さらにCmdlet Auto Discoveryにより未ロードのモジュールを探します。)
コマンド探索の詳細な挙動は、Trace-Command -Expression {コマンド} -Name CommandDiscovery -PSHost とすると調べられるので、見てみるのもいいかもしれません。
⑤ &{process{process{}}}
結果
「Get-Process : パラメーター 'Name' を評価できません。その引数がスクリプト ブロックとして指定され、入力が存在しないためです。スクリプト ブロックは、入力を使用せずに評価できません。」というParameterBindingExceptionが発生します。
解説
・パーサーの挙動
Get-Processが実行され(ようとす)る理由については④までの理解でOKでしょう。
さて、Get-Processコマンドレットには-Nameという、プロセス名を指定する位置パラメータが存在します。位置パラメータは、パラメータ名を指定せずパラメータ値のみを指定しても、指定順にパラメータにバインドしてくれる機能を持ちます。
たとえば、Get-Process powershell とすると、「Get-Process -Name powershell」が実行されます。
今回のお題「process{}」は、パーサーによってまず、コマンド名「process」と、パラメータ値「{}」(空のスクリプトブロック)に分割されます。
(ちなみにコマンド名に「{}」を含めることができないわけではなく、そういうコマンドを実行したい場合は、`でエスケープするか、&"command{}name"のように&演算子を用いれば可能です。)
今回の場合、パラメータ名の指定はありませんが、位置パラメータ-Nameに空のスクリプトブロックがバインドされることになるわけです。
・コマンドパラメータバインドの挙動
さて、-Nameパラメータの型は、System.String[]であり、scriptblockではありません。もちろんscriptblockからSystem.String[]への暗黙の型変換はありません。でもエラーメッセージ的には、スクリプトブロックを与えたこと自体は咎めていないように思えますね。
実はこれ、スクリプトブロックパラメータと呼ばれてる機能です。詳しくはスクリプトブロックパラメータのススメを見ていただくとして、要はコマンドへのパイプライン入力を、指定のスクリプトブロックで処理し、その出力結果をパラメータ値としてバインドする機能ですね。
今回エラーになった理由は、スクリプトブロックパラメータとして解釈しようとしたが、そもそも入力がなかったから、ということになります。
あまり意味はないですが、以下のように入力を与えてやれば、スクリプトブロックパラメータとして動作します。
"powershell" | Get-Process -Name {$_}
この場合パイプライン入力が追加されるので、-Nameパラメータの指定位置がずれることになるので、パラメータ名が必要になります。また、スクリプトブロックが空だと、「パラメーター 'Name' を評価できません。その引数の入力によって出力が作成されなかったためです。」というエラーをご丁寧に出してくれます。Trace-CommandでParameterBindingソースをトレースしてみるのも一興でしょう。
ちなみにあまり関係ない余談ですが、-NameパラメータにはValueFromPipelineByPropertyName属性が付いているので、実は以下のような指定もできます。
[PSCustomObject]@{Name="PowerShell"} | Get-Process
まとめ
PowerShellパーサーと飲むとき、話の肴にどうですかね。
See also: 本当は怖いPowerShell その1
プライバシーポリシー