2016/12/12

この記事はPowerShell Advent Calendar 2016の11日目です。遅刻してごめんなさい!

ASTとは

ASTとはAbstract Syntax Treeの略で、日本語では「抽象構文木」といいます。コードをパーサーが構文解析した結果から、言語の意味に関係のない要素(空白等)を除外し、木構造として構築したものです。

PowerShellでは3.0からASTの仕組みが取り入れられました。スクリプト実行時にはまずパーサーがスクリプトブロックからASTを生成し、コンパイラによってASTが解釈され、実行されるようになっています。

ASTを直接的に扱うのはコンパイラですが、実はPowerShellではパーサーが構築したASTを、PowerShellスクリプトから扱うことができます。

ASTの具体的な使い道としては、構文の静的解析が挙げられますが、その話は後でするとして、今回はまず、ASTの構成要素と構造を見ていきます。

ASTの構成要素

具体的には、{スクリプトブロック}.Astとして、ScriptblockオブジェクトのAstプロパティから、ScriptBlockAstオブジェクトにアクセスできます。このオブジェクトがASTのルートとなるノード(分岐点)を表します。このScriptBlockAstから、スクリプトブロック内部の構文要素が木構造として展開されていきます。

式(Expression)、文(Statement)といった構文要素は、各々対応したAstクラスが対応し、木構造における分岐点を形成します。また、分岐点の末端の葉では、当該の構文要素を構成するデータを示すオブジェクトが格納されます。

すべてのAstクラスは、Ast抽象クラス(System.Management.Automation.Language.Ast)を継承したクラスです。PowerShellでは50個程のAstクラスが存在します。各Astクラスは、抽象クラスで定義されている以下の2つのプロパティを持っています。

  • Parent
    親ノードを示すAstオブジェクトを返す
  • Extent
    当該のASTノードに含まれるコード文字列や、スクリプト全体から見たコード文字列の位置等の情報を持つ、IScriptExtentインターフェースを実装したクラスのオブジェクトを返す

また各Astクラスは、対象の構文要素に応じて、それぞれ異なったプロパティを持ちます。たとえばScriptBlockAstは以下のプロパティを持ちます。

子の分岐点を返すもの

  • UsingStatements
    Using節を表す、UsingStatementAstのコレクションを返す
  • Attributes
    スクリプトブロックに付与された属性を表す、AttributeAstのコレクションを返す
  • ParamBlock
    paramブロックを表す、ParamBlockAstを返す
  • BeginBlock、ProcessBlock、EndBlock、DynamicParamBlock
    各々、beginブロック、processブロック、endブロック、DynamicParamブロックを示すNamedBlockAstを返す

葉を返すもの

  • ScriptRequirements
    #Requires節の内容を表す、ScriptRequirementsを返す
ASTの構造

たとえば、

$scriptBlock = {
    param([int]$x,[int]$y)
    end
    {
        $out = $x + $y
        $out | Write-Host -ForegroundColor Red
    }
}

という、二つの整数値の和を赤字で表示するというスクリプトブロックならば、以下のようなASTが構築されます。(一部分岐点、葉は省略しています。また、分岐点のASTクラス名は、末尾の"Ast"を省略表記しています。)

PowerShell_AST

このスクリプトブロックのASTから、例えば「Red」というパラメータ値を表す、StringConstantExpressionAstまで辿るには、

$scriptBlock.Ast.EndBlock.Statements[1].PipelineElements[1].CommandElements[2]
StringConstantType : BareWord
Value              : Red
StaticType         : System.String
Extent             : Red
Parent             : Write-Host -ForegroundColor Red

のようにします。

基本的なASTの構造が頭に入っていれば、タブ補完を併用することで比較的簡単に目的のノードまで辿れますが、ASTノードの子に対し、ノード検索をかける方法もあります。

例えば、すべてのVariableExpressionAstを列挙するには、

$scriptBlock.Ast.FindAll({
    param($ast)
    $ast -is [System.Management.Automation.Language.VariableExpressionAst]
}, $true)

のように、FindAllメソッドを用います。

AST編はあと何回か続く予定です。

2015/08/10

C#6.0のnameof演算子(じんぐるさんによる解説岩永さんによる解説)が羨ましかったので、PowerShellでも似たようなことができるようにしてみました。

function nameof
{
    param([scriptblock]$s)
    $element=@($s.Ast.EndBlock.Statements.PipelineElements)[0]
    if($element -is [System.Management.Automation.Language.CommandExpressionAst])
    {
        switch($element.Expression)
        {
            {$_ -is [System.Management.Automation.Language.TypeExpressionAst]}
                {$_.TypeName.Name}
            {$_ -is [System.Management.Automation.Language.MemberExpressionAst]}
                {$_.Member.Value}
            {$_ -is [System.Management.Automation.Language.VariableExpressionAst]}
                {$_.VariablePath.UserPath}
        }
    }
    elseif($element -is [System.Management.Automation.Language.CommandAst])
    {
        $element.CommandElements[0].Value
    }
}

nameof{$PSHOME}                      # 変数名 : PSHOME
nameof{$PSHOME.Length}               # プロパティ名 : Length 
nameof{[System.Diagnostics.Process]} # クラス名 : System.Diagnostics.Process
nameof{[string]::Empty}              # フィールド名 : Empty
nameof{[DayOfWeek]::Friday}          # 列挙体メンバー名 : Friday
nameof{Get-Command}                  # コマンド名 : Get-Command

原理的には、変数やプロパティ等をスクリプトブロックに格納し、生成されるAST(抽象構文木、abstract syntax tree)を解析して、含まれる変数名やプロパティ名を抽出しています。(なので、PowerShell 3.0以上でないと動作しないと思います)

そもそも、どういうシチュエーションで使うの?という話ですが、実はあえとすさんのPowerShell コマンドを C# で書くときに便利な拡張メソッド - 鷲ノ巣という記事を見て、じゃあPSでコマンド(高度な関数)を書く時にも同じことが出来るといいかな?と思ったのがきっかけです。

function Get-Test
{
    [CmdletBinding()]
    param([int]$Number)
    if($PSBoundParameters.ContainsKey((nameof{$Number})))
    {
        "-$(nameof{$Number})パラメータが指定された"
    }
}

こういう風に、"Number"という文字列をコード中に書かずに、-Numberパラメータ指定の有無を確認できるようになる、というわけです。

(この例の場合だと、Get-Test -Number 12 のようにすると、if文の中身が実行されます。)

ただ作ってはみたものの、使う意味がどれほどあるのか疑問に思えてきました。一応、ISEでは変数名やメンバ名に入力補完が効くので、実際の変数名を文字列で手打ちしなくて済むというメリットはなきにしもあらず、ですか。

しかし所詮は動的言語なので、存在しない変数名やメンバ名を入れても実行前にエラーは出ないですからね。(Set-StrictModeによるストリクトモードは編集時ではなくあくまで実行時(正確には変数やメンバを参照した瞬間)にエラーを出すためのもの)

それとISEのリファクタリング機能は弱い(というか無い)ので、リファクタリングに追従できるという本家nameof演算子に存在するメリットは、現状のところISEを使っている限りは享受できません。

PowerShell 5.0からの新要素、Script Analyzerによる静的解析を組み合わせればあるいは意味が出てくるのかもしれないですが、まだ確認できてないです。

というわけで、書いてはみたもののなんか微妙ですが、せっかくなんで公開しときます。



Copyright © 2005-2018 Daisuke Mutaguchi All rights reserved
mailto: mutaguchi at roy.hi-ho.ne.jp
プライバシーポリシー

Books

Twitter