2016/12/20

この記事は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クラスの具体的な実装については、PSReadLinePowerShellEditorServicesにありますので、参考にしてみてください。

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/04/23

Japan PowerShell User Group (JPPOSH) 主催の第 6 回 PowerShell 勉強会(4/9)には多数の方にお越しいただき、ありがとうございました。

PowerShell勉強会は今後も年2回くらいのペースで続けて行きたいと思っていますので、どうぞよろしくお願い致します。

さて、私のセッション「PowerShell 5.0 新機能と関連OSSのご紹介」のスライドを公開します。前半は以前のものとだいたい同じですが、正式版対応版にアップデートしています。

今回は去年から今年にかけて、PowerShell関連ソフトウェアとしてOSS化したものを、まとめて紹介しました。以下は今回紹介したもののリストです。

またデモで用いたサンプルファイルも公開します。

このzipにも同梱してますが、PSScriptAnalyzerのカスタムルールはこんな感じで作ります。作り方は、ASTを受け取って、中身をチェックして、ルールに該当するならDiagnosticRecordを返すというのが基本になります。

using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic
using namespace System.Management.Automation.Language

Import-Module PSScriptAnalyzer

function Test-UsingVarsWithNonAsciiCharacter
{
    # 変数に半角英数字以外の文字種が含まれていると警告するカスタムルール。
    [CmdletBinding()]
    [OutputType([DiagnosticRecord[]])]
    Param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [ScriptBlockAst]
        $ScriptBlockAst
    )

    Process
    {
        [Ast[]]$variableAsts = $ScriptBlockAst.FindAll({
            param([Ast]$ast)
            $ast -is [VariableExpressionAst]
        }, $true)

        $variableAsts | 
        where {
            $_.VariablePath.UserPath -notmatch '^[a-zA-z0-9_]+$'
        }|
        foreach {
            $result = [DiagnosticRecord[]]@{
                "Message"  = "変数 `$$($_.VariablePath.UserPath) に半角英数字以外の文字種が使われています。"
                "Extent"   = $_.Extent
                "RuleName" = "AvoidUsingVarsWithNonAsciiCharacter"
                "Severity" = "Warning"
            }
            $result
        }
    }
}
Export-ModuleMember Test-UsingVarsWithNonAsciiCharacter

ついでにPesterのサンプルコードも。2つのパラメータを足し算する関数、Invoke-Additionに対するテストコードの例となります。

$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
. "$here\$sut"

Describe "Invoke-Addition" {   # テストの定義
    Context "足し算の実行" {   # テストのグループ化
        It "整数値を2個指定すると、足し算された結果が返る" {  # テストケース
            Invoke-Addition 3 5 | Should Be 8 # アサーション
        }

        It "小数値を2個指定すると、足し算された結果が返る" {
            Invoke-Addition 3.4 5.8 | Should Be 9.2
        }
    }

    Context "エラーの発生" {
        It "足し算できないものを指定するとエラー" {
            {Invoke-Addition 10 "x"} | Should Throw 
        }
    }
}

2015/12/01

この記事は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 ContainersNano 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で共有していただければ嬉しいなあと思います。

というわけで、例年にない感じの初日記事を書いてみました。今回のアドベントカレンダーもどうぞよろしくおねがいします。


Copyright © 2005-2016 Daisuke Mutaguchi All rights reserved

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

Awards

Books

Twitter