2015/09/07

Twitterでこんな問題を出してみました。

以下、解答になります。

@ &{}
結果

何も出力されません。

解説

空のスクリプトブロック{}を実行演算子&で実行しています。空なので何も出力はありません。

A &{process}
結果

「'process' の後にステートメント ブロックがありません。」というパーサーのエラーになります。

解説

PowerShellのスクリプトブロックは、beginブロック、processブロック、endブロックを内包します。スクリプトブロック直下にparamブロック、DynamicParamブロック、beginブロック、processブロック、endブロック(他にもあったかも)以外のステートメントを記述すると、Endブロック内に記述されたものと暗黙的に解釈されます。

この場合、スクリプトブロック直下にprocess…と書き始めたので、パーサーはprocessブロックが開始されたと判断しますが、続くステートメントブロック{}(≠スクリプトブロック)の記述がないため、構文エラーとなります。

B &{process{}}
結果

何も出力されません。

解説

パーサーはAのように解釈しますが、今回はステートメントブロック{}がきちんと記述されているので、エラーなく解釈されます。

processブロックは、パイプライン入力がない場合でも1回実行されますが、この場合、中身は空なので、@と同様、何も出力はありません。

C &{process{process}}
結果

Get-Processコマンドレットが実行され、プロセス一覧が表示されます。

解説

・パーサーの挙動

Bまでの解説の通り、&{process{…}}とすると、…の部分が1回実行されます。今回はprocessブロック内に「process」と記述しているので、Aのようなパーサーエラーは発生せず、「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 とすると調べられるので、見てみるのもいいかもしれません。

D &{process{process{}}}
結果

「Get-Process : パラメーター 'Name' を評価できません。その引数がスクリプト ブロックとして指定され、入力が存在しないためです。スクリプト ブロックは、入力を使用せずに評価できません。」というParameterBindingExceptionが発生します。

解説

・パーサーの挙動

Get-Processが実行され(ようとす)る理由についてはCまでの理解で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

2012/12/25

本記事はPowerShell Advent Calendar 2012、最終日の記事です。

前回はAdd-Typeコマンドレットを使って独自のクラスを作成し、そのクラスを入力あるいは出力型に取る関数をどのように記述すれば良いのか、というお話でした。

今回は前回に残した課題である、ユーザー定義の独自型の出力にちゃんとした書式を設定する方法について説明していきます。

型データと書式設定データ

PowerShellは.NETオブジェクト(に限らず、型アダプタが存在するCOMやXMLなども、ですが…)をラッピングした型システムを有しているわけですが、このラッピング時にオブジェクトに対してPowerShell独自のデータを付与することができます。それが型データと書式設定データと呼ばれるものです。

型データはクラスにPowerShellエンジンが付加する独自のメンバ(プロパティ、メソッド)です。代表的なものにNoteProperty(静的な値をもつプロパティ), ScriptProperty(スクリプトで記述されたgetterとsetterをもつプロパティ), AliasProperty(既存プロパティのエイリアス), CodeProperty(.NETのスタティックプロパティ), CodeMethod(.NETのスタティックメソッド), ScriptMethod(スクリプトで記述されたメソッド)があります。

型データは .types.ps1xmlファイルにその定義を記述し、モジュールならモジュールマニフェスト(.psd1)のTypesToProcessプロパティに型データファイルパスを指定することで、インポート時に型データを反映させることができます。

Update-TypeDataコマンドレットで後から型データファイルを読み込んで反映することもできます。PowerShell 3.0ではUpdate-TypeDataコマンドレットで.types.ps1xmlファイルを読むのではなく、直接任意のメンバを任意の型に追加することも可能になっています。

また、

$obj | Get-Member -View Extended

とすることで$objに追加されたメンバがどれなのかが分かります。(ちなみに型アダプタによって追加されたメンバはAdapted指定で分かります)

今回の記事では型データについてはこれくらいにして(またいつか改めて取り上げたいですが)、以下、本題の書式設定データの話をしていきます。

書式設定データとは

書式設定データも型データと同様、クラスに付加するデータなのですが、これはオブジェクトを出力する際のデフォルトの表示フォーマットを定義するものとなります。

たとえばGet-Processコマンドレットを実行すると

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    138      13    18456       7104    62            2052 aaHMSvc
     88       8     2168       1268    55            2112 AdminService
...

のようにProcessオブジェクトが表形式で表示されます。

ここで表に含まれるプロパティ、IdとProcessNameは.NETのProcessクラスが持つオリジナルのプロパティで、HandlesはHandleCountプロパティのAliasPropertyです。NPMやWSなども対応するAliasPropertyやScriptPropertyが定義されているのですが、たとえばNPMというAliasPropertyはあってもNPM(K)というメンバはありません。これを定義しているのが書式設定データになるわけです。そもそもこの表に含まれるプロパティの種類であるとか、もっというとProcessオブジェクトは特に指定がない場合は表形式で表示する、といった定義も書式設定データに含まれます。

書式設定データも型データと同様にXMLファイルに定義されるのですが、その拡張子は.format.ps1xmlです。モジュールならマニフェストのFormatsToProcessプロパティに.format.ps1xmlファイルのパスを指定することで表示に反映されますし、Update-FormatDataコマンドレットによって後から反映させることも可能です。

この書式設定ファイルはユーザー定義型に関しても定義を記述できます。つまり、ユーザー定義型に対応する.format.ps1xmlファイルを記述し、それを読み込むことで、自分がAdd-Typeで作った型に対しても書式を設定できるわけです。次の節でそのやり方を見ていきましょう。

書式設定データの作り方

書式設定データ.format.ps1xmlの書式についてはMSDNにリファレンスがあるので、これを読めば自分で一から作成することは可能です。ですがそれはちょっと面倒くさいので、既存の書式設定データをベースに、独自型用に修正していくのがお勧めです。

書式設定データはGet-FormatDataコマンドレットで取得でき、Export-FormatDataコマンドレットでファイルとして出力できます。なお、Export-FormatDataコマンドレットの出力XMLファイルは改行コードが入っていなくて見づらいので、XmlDocumentとして再度読み込んでSave()するという小細工を施すのがお勧めです。先ほどのProcessクラス(System.Diagnostics.Process)の書式設定データをファイル化するには以下のようなスクリプトを実行します。

$ps1xml="process.format.ps1xml"
Get-FormatData System.Diagnostics.Process | Export-FormatData -Path $ps1xml -IncludeScriptBlock
([xml](Get-Content $ps1xml)).Save((Join-Path (Get-Location) $ps1xml))

このスクリプトを実行すると、Processクラスの書式設定データをprocess.format.ps1xmlファイルとして出力できます。

出力した.format.ps1xmlはPowerShell ISE(ただしv3の)で開くのがお勧めです。ちゃんとXMLノードを折りたたみできるので。

さて、出力した.format.ps1xmlファイルをつらつらと眺めると、実際の出力書式の定義はビュー(View)という単位で行われていることがわかります。View要素の下にはName要素(ビューの名前)、ViewSelectedBy要素(ビューを反映する対象の型)、TableControl要素(表の書式)があります。

TableControl要素の下にはTableHeaders要素とTableRowEntries要素が含まれており、前者は表のヘッダーに記載するラベルやその幅などをTableColumnHeader要素に一つ一つ定義し、後者は表の本体に表示するオブジェクトのプロパティ値をTableColumnItem要素に一つ一つ定義しています。

TableColumnItem要素は単純にプロパティ値を表示させるならPropertyName要素にプロパティ名を書くだけでOKです。スクリプトの結果を表示させるならScriptBlock要素内にスクリプトを書きます。ScriptBlock要素内で自動変数$_に1オブジェクトが格納されています。

結局のところ、表に表示したいプロパティの分だけ、TableColumnHeader要素(プロパティ名のラベル)とTableColumnItem要素(プロパティ値)を1:1で定義していけばOKです。

以上を踏まえてprocess.format.ps1xmlを改変して、前回作成したWinscript.Driveクラスの書式設定ファイルdrive.format.ps1xmlを作成してみました。

<?xml version="1.0" encoding="utf-8"?>
<Configuration>
  <ViewDefinitions>
    <View>
      <Name>drive</Name>
      <ViewSelectedBy>
        <TypeName>Winscript.Drive</TypeName>
      </ViewSelectedBy>
      <TableControl>
        <TableHeaders>
          <TableColumnHeader>
            <Label>Name</Label>
            <Width>4</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>VolumeName</Label>
            <Width>15</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Type</Label>
            <Width>15</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>RootPath</Label>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Size(GB)</Label>
            <Width>25</Width>
            <Alignment>Right</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Used(%)</Label>
            <Width>7</Width>
            <Alignment>Right</Alignment>
          </TableColumnHeader>
        </TableHeaders>
        <TableRowEntries>
          <TableRowEntry>
            <TableColumnItems>
              <TableColumnItem>
                <PropertyName>Name</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>VolumeName</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Type</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>RootPath</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <ScriptBlock>[int]($_.Size/1GB)</ScriptBlock>
                <FormatString>{0:#,#}</FormatString>
              </TableColumnItem>
              <TableColumnItem>
                <ScriptBlock>[int]($_.UsedSpace*100/$_.Size)</ScriptBlock>
              </TableColumnItem>
            </TableColumnItems>
          </TableRowEntry>
        </TableRowEntries>
      </TableControl>
    </View>
  </ViewDefinitions>
</Configuration>

前回作成したスクリプトを実行後、このdrive.format.ps1xmlファイルを

Update-FormatData -AppendPath .\drive.format.ps1xml

のようにして現在のセッションに読み込んでやることで、以降は定義した関数を実行すると、

PS> Get-Drive
Name VolumeName      Type            RootPath                  Size(GB) Used(%)
---- ----------      ----            --------                  -------- -------
C:                   LocalDisk       C:\                            112      88
D:                   LocalDisk       D:\                            466      63
Q:                   CompactDisc     Q:\                                       
V:                   NetworkDrive    \\server\D                   1,397      64

このように定義した型のオブジェクトに対しても、綺麗な書式で出力することができるようになるわけです。

まとめ

ここまで全三回にわたって、「関数の定義」「型の定義」「出力書式の定義」の基本のきについて説明してきました。基本とはいえ、PowerShellでがっつりとちゃんとした関数を書く上で真っ先に押さえておかないといけないことばかりですし、逆にここまで必要最小限に絞った記事もあまりないかなと思い、まとめてみました。参考にしていただければ幸いです。

さて、PSアドベントカレンダー2012もこれで終わりです。皆様、よいクリスマス…はもう終わりなので、よいお年を!

※終わりと言っておきながら実は明日以降、ロスタイムとしてもうひとかたご登場の予定です。ご期待ください。


Copyright © 2005-2016 Daisuke Mutaguchi All rights reserved

mailto: mutaguchi at roy.hi-ho.ne.jp

Awards

Books

Twitter