2016/12/15

この記事は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を使って表示してみたのが、以下のスクリーンショットになります。

スクリーンショット 2016-12-15 09.44.06

色んなスクリプトのASTを表示して、楽しんでみてください。

ASTシリーズはもう少し続きます。次回はAST Visitorと静的解析のお話です。

2012/12/02

Japan SharePoint Group :: [大阪] JapanSharePointGroup 勉強会 #5(2012/12/01)で行ったセッションのスライドです。

SharePointユーザーの方が集まるイベントだったので、「SharePoint2013管理者シェル」についてと、これを利用するために必要となるPowerShellの基礎知識について解説しました。

非常に熱心に聞いていただいた印象で、嬉しく思います。やはりIT Proの方にとって、サーバー設定をするためのスクリプトをテスト環境で作成し、それを本番環境で一気に流して展開する、というシナリオは魅力的に思われるようです。

PowerShell 3.0やWindows Server 2012、そしてSharePoint2013などの各種サーバー製品の登場で、こういったシナリオが現実的なものとなりつつあり、以前に増してPowerShellへの関心が高まっているのを感じます。

2007/05/21

.NET Frameworkをバリバリ使ってC#ライクに書けばわりと見やすいスクリプトが書けるが冗長である。コマンドレットをパイプでつないだり各種演算子を使うと見にくいが簡潔に書ける。
これが本題。

しかし前者のようなスクリプトはC#で書いちゃえばいいじゃんという話だ。csファイルに、コンパイルして実行するVBSでも関連付けておけばよいでしょう。js(JScript.NET)ならクラスを書かないでも大丈夫。COMを使うならスクリプトはWSHでもいい。

そう考えるとやはりPowerShellはコマンドレットとパイプを駆使するのが本道のように思う。ただ、どこまで込み入ったものを書くのか。重要なのは可読性と簡潔性のバランスだ。パイプを多用すれば簡潔だが読みにくい。パイプを使わなければ冗長になる。そのあたりをうまく調整していくのがPowerShell使いの課題だろう。

さて、込み入ったスクリプトの究極例は、込み入ったことを一行のスクリプト(ワンライナーという)にすることだ。これには通常のプログラミングとは違い、頭の体操が必要になってくる。しかも後でみると自分でもわからなかったりする。

なので、保存して実行するならワンライナーにする必要はないように思う。冗長でもわかりやすく書くほうがいい。ワンライナーはあくまでコンソール上で手で打ち込むことに意義があるのではないか。

だが、そのスキルを管理者は身につけるべきなのか?

現実的には、コンソール上でコマンドレットを2,3個のパイプで繋ぐ程度が限界なのではないかと思う。ワンライナーをその場で書くのは相当なスキルが要求されるように思う。ただ、幸いなことにPowerShellにはfunctionやfilterを記述したスクリプトファイルをプロファイルとして読み込むことができるので、込み入った処理はあらかじめ関数化、フィルタ化しておくとよいだろう。そのときはワンライナーである必要はないのは先に述べたとおり。

でもPowerShellのワンライナーを極めた人を見てみたい気もする。UNIX系ではいますよね。私?無理ですよw

というわけでPowerShellスクリプトの考察でした。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2007/05/21/77517.aspx


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

Books

Twitter