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と静的解析のお話です。
プライバシーポリシー