2016/12/20
AST Visitorを使った静的解析
この記事はPowerShell Advent Calendar 2016の20日目です。
はじめに
前々回はASTの概要について述べ、最後にAST.FindAllメソッドを使って、ASTから指定のASTノードを検索する方法について説明しました。
前回はASTを再帰的に検索して、木構造を視覚化してみました。
今回もASTを検索する話なのですが、静的解析機能を実装するためのAST Visitorを用いる方法について説明します。が、あらかじめお断りしておきますが、静的解析の実装までは今回はたどり着きません。静的解析ツールをどう作るかorどう作られているか、ということを雰囲気で味わっていただければと。
Visitorパターン
AST Visitorの説明をする前に、まず、Visitorパターンについて簡単に。
Visitorパターン[Wikipedia]というのは、オブジェクト指向言語におけるデザインパターンの1つで、対象オブジェクトを巡回する「訪問者」クラスを定義するものです。Visitorクラスでは、対象クラスごとに行う処理を、個別にvisitメソッドをオーバーロードさせることで定義します。共通のVisitor抽象クラスを継承することで、異なる機能を持ったVisitorクラスを作ることができます。
一方、処理対象クラスには、Visitorオブジェクトを引数に受け取る、acceptメソッドを定義します。acceptメソッドでは、引数として受け取ったVisitorオブジェクトのvisitメソッドを呼ぶことで、処理を実行させます。
なお、処理対象クラスが子要素クラスを持つ場合には、acceptメソッド内で、子要素クラスのacceptメソッドを呼ぶようにします。こうしておくことで、Visitorは処理対象を再帰的に巡回できるようになります。
このように処理対象クラスから、実際に処理を行う機能をVisitorクラスとして分離することで、処理対象クラスに手を加えることなく、Visitorクラスを追加して、処理内容を増やしたりすることが可能になります。
AST Visitorの呼び出し
Visitorパターンを念頭において、AST Visitorの呼び出し方を見ていきましょう。Ast抽象クラスには、以下の2つのVisitメソッドが定義されています。
説明に入る前に注意点。メソッド名は"Visit"となっていますが、Visitorパターンでいうところの"accept"メソッドのことです。なぜメソッド名がAcceptじゃないのかは不明ですが…。
ともかく、AstクラスのVisitメソッドは、AstVisitor抽象クラスを継承したクラスのオブジェクトか、ICustomAstVisitorインターフェースを実装したクラスのいずれかを引数に取ることで、ASTに対する処理を実施します。
AstVisitor抽象クラスを継承、もしくはICustomAstVisitorインターフェースを実装することで、ASTの種類に応じた巡回処理を行うクラスを、自分で定義していきます。
AstVisitor抽象クラス
AstVisitor抽象クラスは、Visitorとしての基本的な機能があらかじめ実装されています。具体的には既に以下の機能は用意されています。
- ASTの種類に応じたVisitメソッドの定義
すべての種類のASTに対応するVisitメソッド(50個以上)がVirtualメソッドとして定義されています(※)。例えば、IfStatementAstに対する処理を行うための、VisitIfStatementメソッドがあります。※一般的なVisitorパターンでは、Visitメソッドを対象クラス分オーバーロードさせますが、PowerShellのAstVisitorは対象クラスに応じた別名のメソッドを定義する方式です。これも理由は分かりませんが、オーバーロードにするには多すぎるからかもしれません。
- 子ノードの再帰的な巡回
各Visitメソッドには、ASTの子ノードに対し、再帰的にVisitメソッドを呼ぶ仕掛けがあらかじめ備わっています。 - ノード巡回の停止
各VisitメソッドはAstVisitAction列挙型を返却します。以下のように返却する値によって、ノード巡回の継続、停止を制御できます。- Continue:ノード巡回を継続(デフォルト)
- SkipChildren:子ノードの巡回を行わない
- StopVisit:巡回を終了する
カスタムAstVisitorクラスを作成する
以上の基本的な機能を踏まえて、AstVisitor抽象クラスを実装したカスタムVisitorクラスを作ります。C#で書くのが一般的ですが、せっかくなのでPowerShell v5で追加された、クラス構文を使って書いてみましょう。
例えば、「利用しているコマンドのリストを取得する。ただし、コマンドのパラメータ内で別コマンドを呼び出している場合は除く。」というお題を解くことを考えます。
ASTのFindAllメソッドだと、配下に含まれるすべてのCommandAstを取得してしまうので、単純にはいきません。そこでカスタムAstVisitorクラスの出番です。
このお題を実現するVisitorクラスは以下のようになるでしょう。
using namespace System.Management.Automation.Language class GetCommandNamesVisitor : AstVisitor { [string[]]$CommandNames = @() [AstVisitAction]VisitCommand([CommandAst]$commandAst) { $this.CommandNames += $commandAst.CommandElements[0].Extent.Text return [AstVisitAction]::SkipChildren } }
PowerShellのクラス構文において、Virtualメソッドのオーバーライドは、単に同名のメソッドを定義するだけですので、ここではVisitCommandメソッドをオーバーライドします。
プロパティとフィールドの区別はないので、コマンド名の一覧を格納するCommandNamesプロパティは上記のような定義になります。メソッド内でクラスメンバを参照する際には$thisを用います。
作成したGetCommandNamesVisitorクラスをインスタンス化し、解析対象スクリプトブロックのASTのVisitメソッドに引数として渡します。
$scriptBlock = { $files = Get-ChildItem -Path (Get-Location | Split-Path -Parent) -File $files | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 5 } $visitor = New-Object GetCommandNamesVisitor $scriptBlock.Ast.Visit($visitor) $visitor.CommandNames
実行すると、結果は
Get-ChildItem Sort-Object Select-Object
のようになるかと思います。
AstVisitorクラスの具体的な実装については、PSReadLineやPowerShellEditorServicesにありますので、参考にしてみてください。
ICustomAstVisitorの実装
前項で述べた、AstVisitor抽象クラスを継承したカスタムAstVisitorクラスの場合、基本的な処理を実装する必要はないですし、目的とするASTクラスに対するVisitメソッドだけオーバーライドすればいいので、非常に簡便です。
ただ、本格的にPowerShellの構文解析を行いたい場合、ノードの巡回順だとか、その他もろもろをもっと細かく自分で実装したいケースが出てきます。
そういった場合にはICustomAstVisitorインターフェースを実装したクラスを作って対応します。ICustomAstVisitorインターフェースも、AstVisitor抽象クラス同様、各ASTクラスに応じたVisitメソッドが定義されているのですが、各VisitメソッドはAstVisitAction列挙体ではなく、object型のオブジェクトを返します。つまり、自分で好きなオブジェクトを返すように定義できるわけです。
Ast.Visit(ICustomAstVisitor)はAstVisitor抽象クラスを引数に取る場合と異なり、objectを返却するのですが、このとき返却されるのは、最初に実行されたVisitメソッドの戻り値になります。
ICustomAstVisitorはインターフェースですので、処理はすべて自分で定義しなくてはなりません(※)。ノードの再帰的探索も、必要ならもちろん自前で実装する必要があります(前回紹介した、JSON化スクリプトのような処理になるかと思います)。
※ISEだとインターフェースの実装を一発で行うリファクタリング機能はないので、今回みたく実装すべきメンバがたくさんある場合は、こんな感じのひな形を作るスクリプトを使うと良いでしょう。
今回はICustomAstVisitorインターフェースを実装したクラスの実例まではご紹介できませんでしたが、興味のある方は、PSScriptAnalyzerで用いられているので参考にしてみてください。
まとめ
PowerShellのASTについてきちんと解説している記事が英語圏を含めてもあまりないようでしたので、3回に渡って、一通りの基礎知識をまとめてみました。
普通にPowerShellを使っている分には、滅多に使うことはないと思いますが、たとえばPSScriptAnalyzerのカスタムルールを自分で作る場合には、ASTの知識は必須になってきますので、必要に応じて参考にしていただければ幸いです。
2016/12/15
ASTをツリービューで表示する
この記事はPowerShell Advent Calendar 2016の15日目です。
前回はPowerShellのASTの概要を解説しました。今回は前回の補足というか応用的な内容になります。
前回、スクリプトブロックからどのようなASTが生成されるのか、図で書きました。そもそもあの図を作るにあたって、ASTの構造を視覚的に把握したかったので、そのためのスクリプトを書きました。
PowerShellで木構造を展開表示する方法は色々ある(※)かと思いますが、今回はJSONとして出力して、表示については他のアプリに任せることにしました。
※Format-Customのデフォルトビューは意外と使える
ただし、ASTオブジェクトをそのままConvertTo-Jsonコマンドレットに渡すわけにはいきません。というのも、AST構造を再帰的に展開するには、探索の深さ(-Depth)を大きくしなければいけませんが、そうするとASTではないオブジェクトも逐一展開してしまい、現実的な時間内で終わらなくなってしまいます。
そこで、ASTオブジェクトそのものをJSONにするのではなく、必要なプロパティのみ再帰的に取得したカスタムオブジェクトを生成し、それをJSONにする方針を取りました。その成果が以下のコードです。(using namespace節を使っているので、v5以上必須です。)
using namespace System.Management.Automation.Language function GetAstInner { param([Ast]$ast) end { $base = [ordered]@{ ExtentText = $ast.Extent.Text AstName = $ast.GetType().Name } $children = [ordered]@{} $leaves = [ordered]@{} $ast.psobject.Properties | ? Name -notin Extent, Parent | %{ $type = [type]($_.TypeNameOfValue) $propValue = $ast.($_.Name) if($type.IsSubclassOf([ast])) { if($null -ne $propValue) { $children[$_.name] = GetAstInner $propValue } } elseif($type.IsGenericType -and $null -ne ($type.GetGenericArguments() | where{$_.Name -eq "Tuple``2"})) { $asts = @() foreach($next in $propValue) { if($null -ne $next) { $asts += [pscustomobject]@{ Item1 = $( if($null -ne $next.Item1 -and $next.Item1 -is [ast]) { GetAstInner $next.Item1 } ) Item2 = $( if($null -ne $next.Item2 -and $next.Item2 -is [ast]) { GetAstInner $next.Item2 } ) } } } if($asts.length -ne 0) { $children[$_.Name] = $asts } } elseif($type.IsGenericType -and $null -ne ($type.GetGenericArguments() | where{$_.IsSubclassOf([ast])}) ) { $asts = @() foreach($next in $propValue) { if($null -ne $next) { $asts += GetAstInner $next } } if($asts.length -ne 0) { $children[$_.Name] = $asts } } else { if($null -ne $propValue) { $leaves[$_.Name] += $propValue.Tostring() } } } [pscustomobject]($base + $leaves + $children) } } function Get-Ast { param([scriptblock]$ScriptBlock) end { GetAstInner $ScriptBlock.Ast } }
本来なら、50種以上あるAstクラスに応じてきちんと場合分けすべきなのですが、コードが長くなるだけなので、動的言語の強みを生かしてダックタイピング的な方法で下位ノードを再帰的に展開しています。
途中、IfStatementAstのClausesプロパティなどで用いられている、ReadOnlyCollection<Tuple<Ast, Ast>>型であることを確認するのに苦労してますが、多分もっといい方法があると思います…。他はAstオブジェクトそのものか、ReadOnlyCollection<Ast>を返すだけなのでそんなに苦労はないです。Ast抽象クラスに含まれているExtent、Parentプロパティ以外で、Astを要素に含まないプロパティに関しては、ASTの葉として解釈しています。
次にこのスクリプトを使って、スクリプトブロックをJSONとして出力します。
$scriptBlock = { param([int]$x,[int]$y) end { $out = $x + $y $out | Write-Host -ForegroundColor Red } } Get-Ast $scriptBlock | ConvertTo-Json -Depth 100 | Set-Content ast.json
サンプルとして用いるスクリプトブロックは、前回のものと同じです。これを先ほど書いたGet-Ast関数に渡して、結果をConvertTo-JsonでJSON化しています。この際、探索の深さを100としていますが、ネストが深いスクリプトブロックなどでは、もっと大きくする必要も出てくるかもしれません。
出力されたast.jsonを、JSON Viewerを使って表示してみたのが、以下のスクリーンショットになります。
色んなスクリプトのASTを表示して、楽しんでみてください。
ASTシリーズはもう少し続きます。次回はAST Visitorと静的解析のお話です。
2016/12/12
PowerShellのAST入門
この記事は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"を省略表記しています。)
このスクリプトブロックの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/12/01
2015年のPowerShellを軽く振り返ってみる
この記事はPowerShell Advent Calendar 2015の1日目です。
アドベントカレンダーは今年で5回目ですが、例年よりだいぶ参加者が少ないので、敢えて完走を目指さずまったりいきましょう。
とはいえ今年から来年にかけては、PowerShellの変革の年といっても過言ではないかと思います。
今年7月にはWindows 10のリリースとともに、WMF (Windows Management Framework) 5.0 / PowerShell 5.0 (2012R2/8.1向けにはProduction Preview)が登場しました。(私の書いたPS5.0新機能のセッション資料はこちら。)
PowerShell 5.0では特に、"Infrastructure as Code"、すなわちインフラの構成をコードで記述可能にし自動化するための機能である、PowerShell DSC周りが格段にパワーアップしています。DSCの機能増強により、Microsoft Azure等のクラウド、オンプレミスのサーバーはともに大きな恩恵を受けることが期待されます。Azure DSC Extensionも追従する形で凄まじい勢いでバージョンアップしてますね。
PowerShell 5.0の登場に合わせて、各種のPowerShell関係のモジュールやアプリケーションが新登場していますが、これらはいずれもOSS(オープンソースソフトウェア)となりました。
一例を挙げると、DSCで用いるロジック本体となる「DSCリソース」を作製する際に有用なDSCリソースキット、対応リポジトリをプロバイダという形で拡張可能であるパッケージ管理システムPackageManagement、PowerShellモジュールを専用リポジトリであるPowerShell Galleryからコマンド1発で取得可能となるPowerShellGet、PowerShellスクリプトの静的解析を行うスクリプトアナライザー、Windowsのみならず他プラットフォームの一括管理を目指すDSC for Linux等々です。
先日OSS化したばかりの、マルチプラットフォーム対応のコードエディタであるVisual Studio Codeと、PowerShellスクリプトが記述可能となるPowerShell Extensionあたりもトピックとして熱いですね。
Visual Studio 2015には、VSでPowerShellスクリプト開発を行うためのOSSなプラグインであるPowerShell Tools for Visual Studioが標準搭載されたことも記憶に新しいですね。(12/1追記)
逆に、PowerShellのテストスクリプトを記述するためのフレームワーク(DSL)であるPesterや、コンソールの入出力をパワーアップさせるPSReadlineといった、OSSで開発されている既存のPowerShellモジュールが、Windows 10に標準機能として取り込まれるなど、OSSとの関係性については大きく変化したと言えるでしょう。
そして次期Windows Serverである、Windows Server 2016の足音も聞こえてきました。現在はTP4が公開されており、試すことができます。Windows Server 2016の目玉は何といっても、Windows ContainersとNano Serverでしょう。
アプリケーションをコンテナという単位で配置し、自由なすげ替え、あるいは使い捨てが可能な環境を構築するツールであるDockerというOSSに対し、インターフェースの互換性を持たせたWindows版コンテナがWindows Containersです。
そして、コンテナ機能を最大限に活用するためにフットプリントの最小化を目指し、GUIどころかコンソールすら廃した新しいWindows Serverの形態が、Nano Serverです。
Windows ContainersとNano Serverの管理は当然、PowerShellがメインとなります。また、ようやくWindowsにやってくる、SSHクライアントとサーバーはPowerShell上で動くようです。
PowerShellは5.0に進化することで、足回りを強化しました。そして各種OSSと連携して、クラウドとオンプレミスのサーバー群の基礎を支える存在として、ますます重要性を帯びていくことでしょう。
昨今のPowerShellを取り巻く状況は、私の理解ではざっとこんな感じです。この中に興味を持たれたテーマはありませんか? もちろん、新機能以外にも、まだまだ知られていない機能や利用法も埋もれていると思います。
もしそんなテーマがあったら、PowerShell Advent Calendar 2015で共有していただければ嬉しいなあと思います。
というわけで、例年にない感じの初日記事を書いてみました。今回のアドベントカレンダーもどうぞよろしくおねがいします。
2015/08/10
PowerShellでもnameof演算子みたいなことがしたい?
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
プライバシーポリシー