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編はあと何回か続く予定です。

2014/04/30

はじめに

IE6〜11まで、要するに現行のIEすべてを対象とするやばげなゼロデイ脆弱性がみつかり、パッチ公開までIE以外のブラウザを使いましょうという通達が出たり出なかったりしている昨今のようです。

その文脈で、IE以外のWebブラウザをダウンロードするのにIEを使うしかない!もう死ぬしか…みたいな(本気なのか冗談なのか判断が付きかねる)反応を散見します。

実際のところはWebブラウザを探してダウンロードする位はIE使えばいいと思いますが、IE使わないでいかにWebブラウザをダウンロードするか、を考えるのが、Twitter等で一種の大喜利のようになっています。

ftp.exeを使う、wgetなどのコマンドラインツールを使う、ペイントのファイルダイアログを使う(?)等々いろいろな案がありますが、皆なぜPowerShellを使わないんだ。ということで、書きます。

なお、WebブラウザのダウンロードURLはご自分でお調べください。こちらの方の記事が参考になるかと。

Invoke-WebRequestコマンドレットを使う

一番普通のやり方としては、Invoke-WebRequestコマンドレットを使う方法ですね。

Invoke-WebRequest https://アドレス/setup.exe -OutFile setup.exe

これを実行するとファイルがダウンロードされて、カレントディレクトリにsetup.exeというファイルが生成されます。フルパス指定でももちろんOKです。

なお、Invoke-WebRequestコマンドレットはHTMLのDOMパース時のみIEコンポーネントを用いるのですが、-OutFileパラメータ使用時はパースしないのでおそらくIEとは無関係で実行できると思います。(注:IEコンポーネントによるDOMパースを抑制するには、-UseBasicParsingパラメータを付加します)

Invoke-WebRequestコマンドレットはPowerShell 3.0から追加されたコマンドレットなので、3.0が同梱されているWindows 8、4.0が同梱されているWindows 8.1では特に何もせずに利用可能です。

ちなみにInvoke-WebRequestコマンドレットはデフォルトエイリアスとしてiwrが定義されているので、iwrでも呼び出せます。PowerShell 4.0ではそれに加えてwget、curlもエイリアス定義されていたりします。(このエイリアス定義は賛否両論ですけどね)

Windows 7の場合はPowerShellのデフォルトのバージョンは2.0なので、3.0以上を追加で入れる必要があります。Vistaは2.0までしか入らないので残念でした。

Start-BitsTransferコマンドレットを使う

Windows 7以上であればBITS(バックグラウンド インテリジェント転送サービス)の機能がPowerShellコマンドレットから利用できます。BITSはその名の通り、ネットワーク帯域の空き部分を有効活用してファイルを転送するかしこいサービスで、Windows Update等で用いられています。ファイル転送のプロトコルとしてはSMBとHTTP(S)をサポートしてるので、Webサイトからファイルをダウンロードするという用途で使うことができます。

ダウンロードを開始するには、Start-BitsTransferコマンドレットを使います。

Import-Module BitsTransfer
Start-BitsTransfer https://アドレス/setup.exe setup.exe

Windows 7標準のPowerShell 2.0だと、Cmdlet Auto Discoveryの機能が働かないため、上記のようにImport-Moduleコマンドレットによる明示的なモジュールロードが必要ですが、PowerShell 3.0以降では不要です。

BitsTransferモジュールには他にもコマンドレットがあり、非同期転送等できたりするので興味のある方はヘルプをみてください。こちらの記事も参考になるかと:PowerShell: ◆Bits転送2

WebClientオブジェクトを使う

今回の大喜利(?)でPowerShellを用いてファイルダウンロードする方法というのもいくつか見かけたのですが、WebClientを使う方法が殆んどだったように思います。まあ今回の記事を書いた動機としては、今はもうWebClient使わなくても標準のコマンドレットでできるよ!ということなんで敢えてここでは書きません。次のツイートを参照して下さい:Twitter / tanakh: IEを使わずにFirefoxをダウンロードする方法、Powe ...

おまけ:chocolateyを使う

あまりPowerShellとは関係ないのですが、そもそもWindowsにはapt-getみたいなパッケージ管理システムはないんかい、という意見をみかけたので紹介します。ChocolateyというNuGetベースのWindows用アプリケーションのパッケージ管理ツールとリポジトリです。

このツール自体のインストールはコマンドプロンプトに、サイトトップに書いてあるコマンドをコピーペーストして実行するだけです。(このコマンド内でPowerShellを呼び出しているので関係あるっちゃある…)

あとはコマンドプロンプトで cinst GoogleChrome とかしてやるとダウンロードとインストールをしてくれると思います。

ところでchocolateyは現在のところコマンドプロンプトベースであり外部ツールですが、次期バージョンのPowerShell 5.0ではOneGetと呼ばれるパッケージ管理システムが追加され、コマンドレットでchocolateyをはじめとする様々なリポジトリからファイルを取得してインストールできるようになる予定なのでご期待ください。

2013/02/13

【改訂新版】Windows PowerShellポケットリファレンス:書籍案内|技術評論社 [表紙]【改訂新版】Windows PowerShellポケットリファレンス

というわけで、2/23に拙著「【改訂新版】Windows PowerShell ポケットリファレンス」が発売となります。初版は2008年に出版され、ご愛顧いただいていたのですが、このたびPowerShell 3.0 / Windows 8 / Windows Server 2012に対応して改訂新版として装いも新たに再登場となります。

今回はPowerShellの基礎知識や文法、リモート接続方法等を項目ごとに解説した「Part1 PowerShellの基礎」、PowerShell 3.0に含まれる全コアコマンドレットを分野別に分類し、その構文、パラメータと使用例を含め解説した「Part2 コマンドレット」、PowerShellから利用する頻度の高い.NET Frameworkクラスに含まれるメソッドとプロパティを解説した「Part3 .NETクラス」、Windows 8 / Server 2012に含まれる全モジュールとそれに含まれる全コマンドレット(2000以上!)のリスト(コマンドレットごとにも一言説明文付き)を掲載した「Part4 モジュール」の4部構成となっております。

PowerShell 1.0/2.0/3.0全対応で、2.0以上で対応の機能、3.0で対応の機能にはそれぞれ分かりやすくマークを入れてあります。

今回もいくつかサンプルスクリプトを掲載しており、それはサポートページでダウンロードできるようにする予定です。

392ページだった初版から大幅増ページで592ページとなり、ポケットリファレンスといいつつ本当にポケットに入るのか?というボリュームとなっていますが、きっと皆様のPowerShellライフのお役に立てるものと思います。書店等で見かけた際は是非、お手に取ってご覧になっていただきたいと思います。

2012/12/01

今日から、PowerShell Advent Calendar 2012が始まりました。初日は私が担当させていただきます。お題は旬の話題、PowerShell 3.0の新機能!…ではなく、初心に返って、PowerShellの「関数」ってどう書くのがいいのかというお話をします。PowerShell 3.0どころか、大部分はPowerShell 1.0から変わっていない基本の話です。

これは今までずっと書きたかったネタですがなかなか書く暇がなくて放置してたものです。3.0の話はきっと他の皆さんが書いて下さるはず!私もまた順番が回ってきたら書こうと思います。

PowerShellの関数は従来言語とだいぶ違う

PowerShellを使いこなすようになってくると、他の言語を使う時と同じで、定型処理は関数として一つにまとめたくなってきます。ところが他の言語と同じような感覚で関数を書くと、どうもうまくいかないのです。

たとえば引数にフォルダパスとフォルダ名を指定すると、指定フォルダが存在すればFalseを返し、存在しなければ作成してTrueを返す関数を書いてみました。

function MakeDir($path,$name)
{
    $newDirPath = Join-Path $path $name
    if((Test-Path $newDirPath))
    {
        return $false
    }
    else
    {
        New-Item -ItemType Directory -Path $newDirPath 
        return $true
    }
}

実行は

MakeDir("C:\test","NewFolder")

と、メソッド風に呼び出すことはできないので、コマンドレット風に

MakeDir C:\test NewFolder

と呼び出せばいいんですが(まあ最初はここもつまづきポイントではありますが)、この実行結果は以下のようになります。

    ディレクトリ: C:\test

Mode                LastWriteTime     Length Name 
----                -------------     ------ ----
d----        2012/12/01      7:51            NewFolder
True

フォルダが作成されてTrueが返却されることを想定していたのに、なんか余計な出力が混じってしまっています。なんでしょうこれは?

実はPowerShell関数内で値が出力されると、returnキーワードがついてなくてもすべて呼び出し元に出力されるという仕様なのです。そしてPowerShellにおけるreturnキーワードの効果は「後続処理を打ち切って呼び出し元に戻る。ただしreturnの後に値が指定してあればそれを最後の値として戻す」となります。そのため、呼び出し元に返したくない出力が関数内にある場合は、すべて[void]にキャストしたり|Out-Nullとしてリダイレクトするなどして出力を破棄する必要があるのです。このMakeDir関数の場合はNew-Itemコマンドレットが作成したフォルダのFolderInfoオブジェクトを出力するので、これをNew-Item -ItemType Directory -Path $newDirPath | Out-Null のように破棄してやる必要があるわけです。

パイプラインの動作

先ほどの例を見ると、「いやいやなんでそんな訳のわからない仕様なんだよ、returnあるときだけ値返せよ」とお思いかと思います。しかしこれはPowerShellの特長の一つである、コマンドのパイプラインによる連携を行うための仕様なんです。

ここでコマンドを繋ぐパイプラインがどういう動作をしてるか、おさらいします。

Get-Process | where {$_.Handles -ge 500} | foreach {$_.Path}

これはハンドル数が500以上のプロセスのメインモジュールファイルのパスを取得するというコマンドで、別に何の変哲もありません。ところが、このコマンドがやっている処理を、次のように誤解してませんでしょうか?

@ 稼働中のすべてのプロセスの一覧を配列として取得する。
A @で取得した配列を走査して、Handlesプロパティの値を調べる。Handlesが500以上のオブジェクトだけ抽出した配列を生成する。
B Aで生成した配列を列挙して、{}内のスクリプトをそれぞれ実行する。

しかし、これは間違いです。

正しくは

@ 稼働中の1つのプロセスオブジェクトを取得して次のコマンドへ送る。
A そのプロセスのハンドル数が500以上なら、次のコマンドへ送る。そうでないなら@に戻る。
B そのプロセスに対して{}内のスクリプトを実行する。まだ未取得のプロセスが残っていれば@に戻る。

という動きをしています。つまり、パイプラインの手前で一旦すべての処理を終えてから、出力オブジェクトがまとめて配列という形で次のコマンドに送られるのではなく、オブジェクトがパイプラインの先頭から末尾に向けて1つずつ通過していき、それが先頭コマンドの出力オブジェクト数だけ繰り返される、という動作をしているのです。

これがPowerShellのパイプライン処理が、従来の処理系での関数と決定的に違うところで、パイプラインによって複数のコマンドが、あたかももとからあった単一のコマンドのように密に連携するわけです。

(この処理、.NETのLINQにちょっと似てると思う方もいらっしゃると思います。しかしLINQとは全然違うものです。なんせPowerShellはLINQより先に世に出てますし! しかし類似点も多いのでいずれ比較なんかを書きたいと思ってます)

パイプラインで連携可能な関数の書き方

さて、先ほどのパイプラインの話ではコマンドレットを連携させていました。しかしPowerShellにおいてはコマンドレットも関数も、それが.NETのクラスかPowerShellのスクリプトなのかの違いがあるだけで、基本は同じ「コマンド」です。なので、関数もコマンドレットと同様、適切な記述をおこなえば、パイプラインでコマンド同士を連携させることが可能です。

以下に、Get-Repeatという関数の例を挙げます。この関数は-Textパラメータに文字列を指定し、-Countパラメータに回数を指定すると、指定文字列を指定回数分連結した文字列を出力する、という何の変哲もない関数です。しかしパイプラインからの入力を受け付け、次のパイプラインへ出力することを想定した作りになっています。

function Get-Repeat
{
    param(
        [Parameter(ValueFromPipeline=$true,Mandatory=$true)]
        [string[]]
        $Text,
        
        [int]
        $Count=2
    )

    begin
    {
    }

    process
    {
        foreach($s in $Text)
        {
            $s * $count
        }
    }

    end
    {
    }
}

以下は実行例です。

PS> Get-Repeat -Text ab -Count 2
abab
PS> "ab" | Get-Repeat -Count 2
abab
PS> Get-Repeat -Text ab,cd -Count 2
abab
cdcd
PS> "ab","cd" | Get-Repeat -Count 2
abab
cdcd

このように、パラメータに値を指定してもパイプラインから入力しても、スカラー値(配列ではない単一のオブジェクト)でも配列でも、正しく処理されています。

この関数をポイントごとに見ていきましょう。

PowerShellの正式な関数はparam節、beginブロック、processブロック、endブロックに分かれます。param節にはパラメータを指定します。beginブロックにはパイプラインで連携した際、最初の1回だけ実行される初期化処理、endブロックには最後の1回だけ実行される後始末処理を記述します。beginとendは今回の例では内容を省略しています。processブロックには、パイプラインから入力された1つのオブジェクトに対してその都度実行される処理を記述します。

ちなみに、

コマンド@|コマンドA|コマンドB

とある場合、各コマンドにおけるbegin,process,endブロックは次のような順番で呼び出されます。

コマンド@begin→コマンドAbegin→コマンドBbegin→{コマンド@process→コマンドAprocess→コマンドBprocess→コマンド@process…}→コマンド@end→コマンドAend→コマンドBend

processブロックでの処理は、通常はパイプラインだけではなくパラメータからも値を入力できるようにしておきます。そのためにはparam節に記述するパラメータに「このパラメータはパイプラインから値を入力することもできる」を意味する[Parameter(ValueFromPipeline=$true)]という属性を指定します(この属性はPowerShell 2.0から利用可)。今回のパラメータには「このパラメータは必須である」を意味するMandatory=$trueもあわせて指定しています。

先述の通り、パイプラインから入力される場合は配列ではなくオブジェクトが単体で渡されるのですが、パラメータから入力される場合はスカラー値と配列値、どちらの可能性もあるため、[string[]] のようにパラメータの型を配列型にしておくことで、どちらを指定しても処理できるようにしています。

processブロックではパラメータ経由で配列値が渡された場合に、各要素に対して処理を行うためforeachループを設けています。ちなみにスカラー値が渡された場合もforeachは問題なく処理します。

processブロック内では、returnは記述しません。returnするとその時点で関数が終了してしまうので正しくすべての出力ができなくなってしまいます。

特にこの例の関数のように入力型と出力型が同一の場合は、processブロックでは1オブジェクトの入力に対して、1オブジェクトを出力するようにしておくと、他のコマンドと連携させやすくなります。ただしWhere-Objectコマンドレットのようにフィルタ処理を行う関数の場合は、条件によっては何も出力しないようにします(空の配列とか$nullを返すのではないことに注意)。もちろん入力オブジェクトから何らかの配列値を出力する場合もありえます。

最低限、これらのポイントを押さえて関数を記述すると、他のコマンドとパイプラインで連携しやすい、PowerShellらしい関数を書くことができると思います。

まとめ

PowerShellでは従来言語と同じ感覚で関数を書くと、うまくいかないことが多いです。もっとも単に処理をひとまとめにしたいというニーズだけならばそれでも問題ないのですが、関数同士を組み合わせたいときに問題が顕在化します。

パイプラインの真の動作を理解し、パイプラインの中に組み込んで動作させることを想定した関数を記述すると、他のコマンドレットあるいは自作関数と連携しやすくなり、PowerShellの真の力を解放することができると思います。

PowerShell Advent Calendar 2012の1日目にしてはえらい固いネタかもですが、基本をおさらいするのも大事ですよね。

さて、明日は@jsakamotoさんの番です。よろしくお願いします。

2012/06/10

4/24のWindows Developer Days (WDD) 2012と、6/9のCommunity Open Day (COD) 2012で私が行ったPowerShell 3.0のセッション資料とビデオが公開されました。ご参加いただいた方は復習用に、ご参加いただけなかった方には参考資料としてお役にたてれば幸いです。

WDD2012とCOD2012、そして6/2のわんくま同盟大阪勉強会#49にお越しいただいた皆様、ありがとうございました。改めて御礼申し上げます。

2012/04/24 Windows Developer Days 2012

非 Windows ユーザーにもお勧め Windows PowerShell 3.0 概説

Windows 8 および Windows Server "8" には Windows PowerShell 3.0 を含む Windows 管理フレームワーク 3.0 が組み込まれ、システム管理機能が大幅に強化されました。このセッションでは PowerShell 3.0 を用いた Windows サーバー管理・自動化の概要をデモを交えてご紹介します。

※PowerShell 3.0 を用いたリモートサーバー管理の話とデモが中心です。

セッション資料・ビデオ (Windows Developer Days 2012 | Channel 9で公開されています)

2012/06/09 Community Open Day 2012

PowerShell 3.0 概要

PowerShellはWindows Serverの管理、自動化の要となるシェル・スクリプト環境です。今年中にもリリースされるWindows Server 2012とWindows 8の登場とともに、PowerShellはバージョン3.0にアップデートされます。今回のセッションではPowerShellの基本を軽くおさらいし、3.0になって追加された新機能と改善点をご紹介します。

※PowerShell 3.0 の新機能と改善点を網羅したセッションです。

セッション資料 (SlideShare) セッションビデオ (Ustream)

2012/05/14

来月6/2と6/9にPowerShell3.0のセッションをしますので告知です。

PowerShell 3.0 を使ったリモート管理(2012/06/02 わんくま同盟 大阪勉強会 #49

2月のセッションではPowerShell3.0新機能を網羅しましたが、今回はその中でも特に重要なPSリモーティングの強化ポイントを、実際に Windows8クライアントからWindows Server2012のリモートサーバーを管理するデモを交えてご紹介します。
PowerShellの基本概念をおさらいするパートもやりますので、この機会に「PowerShell再入門」してみませんか?

6/2のわんくま大阪#49では、jz5さんによるWeb APIのセッション、あおおにくんによるMonoのセッション、さおさんによるMetro+DirectXのセッションのほか、私によるPowerShell3.0のセッションがあります。PowerShell3.0を使ったリモート管理を実際のデモをまじえてご紹介します。

PowerShell 3.0 概要(2012/06/09 Community Open Day 2012 大阪会場

PowerShellはWindows Serverの管理、自動化の要となるシェル・スクリプト環境です。今年中にもリリースされるWindows Server 2012とWindows 8の登場とともに、PowerShellはバージョン3.0にアップデートされます。今回のセッションではPowerShellの基本を軽くおさらいし、3.0になって追加された新機能と改善点をご紹介します。

6/9のCommunity Open Day 2012 (COD2012)は全国のIT系コミュニティで共催されるイベントです。マイクロソフト社員の方によるキーノートから始まり、各コミュニティのスピーカーが各会場でセッションを行います。大阪会場では私は森理麟さんとわんくま同盟大阪から登壇させていただき、PowerShell3.0の新機能をすべて網羅してご紹介します。内容はbeta(間に合えばRelease Preview)対応版となります。森理麟さんはWSHなどを使ったWindowsクライアントの自動化の話、私はPowerShellでサーバー管理・自動化の話となり、わんくま大阪としては「Windows自動化」をテーマにCOD2012に参加します。

私のセッションに関しては、6/2と6/9のどちらか一方、あるいは両方聴いても楽しめるようにしたいと思っています。

ご興味のある方はぜひお越しください。会場はどちらもマイクロソフト関西支店です。

2012/03/07

3/24(土)に広島の日本マイクロソフト株式会社中四国支店で第27回.NET 勉強会 / ヒーロー島が開催されます。今回のテーマは「ハンズオンでPowerShellに触れてみよう」ということで、丸一日かけてPowerShellセッションとハンズオンが行われます。

.NET 勉強会 / ヒーロー島 - 第27回勉強会

私はこのうちハンズオンのスピーカー(進行?)を担当させていただきます。ハンズオンの前に行われるセッションにはマイクロソフトエバンジェリストの安納さんがスピーカーをされ、PowerShell概要、そして3.0のお話をしていただきます。

ハンズオンというのは通常のセッションのようにスピーカーの話を聴くのがメインのスタイルではなく、講師の指示にしたがいながら実際に自分のノートPCで操作を行い、学習を進めていく方式になります。今回はPowerShellの基本操作を学ぶハンズオンにしていきたいと考えています。

ハンズオンに参加していただくにはノートPCが必須になります。ぜひ、ご自分のノートPCをお持ちいただきますようお願いします。Windows 7でしたら特に前準備は必要ありません。Vista, XPの場合は.NET Framework 3.5 sp1以上とPowerShell 2.0をあらかじめインストールしておいていただきますようお願いいたします。Windows 8 のプレビュー版やPowerShell 3.0 beta版をさっそくインストールされている方もそのままご参加いただけます。

ハンズオン内容に関しては現在構成中ですが、もし「これをやってみたい!」というのがありましたらコメント欄やTwitterなどでご連絡いただければ、参考にさせていただきたいと思います。

お近くの方でご興味がございましたら、ぜひご参加ください!丸一日PowerShell漬けの勉強会というのはなかなかないですので!スタートメニューにある謎の(?)プログラム、この機会に触ってみませんか!それとエバンジェリストによるPowerShell 3.0話を聴けるめったにないチャンスですよ!

というわけでみなさまのご参加、お待ちしております。

2012/03/06

次期Windows Server OSであるWindows Server “8”のベータ版が3/1よりWindows 8 Consumer Preview版と同時に提供が開始されました。今回のリリースでは日本語版も提供されています。

それと同時にPowerShell 3.0 betaを含むWindows Management Framework (WMF) 3.0 betaの提供も始まりました。こちらは英語版のみの提供で、日本語環境にインストールするには英語言語パッケージをダウンロードし適用する必要があります。

まだ評価を初めて間もない段階ですが、きづいた点をいくつか書いてみます。

PowerShell ISEがDeveloper Preview/CTP版から大きく変わっており、コマンドを入力する「コマンドペイン」と結果が表示される「出力ペイン」が統合され、「コンソールペイン」となりました。image

こんな感じでコンソールペインはその名の通りコンソール版のPowerShellと見た目や使用感が似ている上にISEならではの機能(インテリセンスなど)が使用可能になっており、ISEはスクリプト編集のみならずインタラクティブシェルとしてpowershell.exeの代わりに使用する場合でもより便利になったと言えるでしょう。なおISEのキーワード色分け設定は細かく指定できたりします。

Server8ではActive Directory管理センターの大幅な機能増強が目に留まります。Active Directory管理センターには「Windows PowerShell 履歴」ペインが追加され、GUIで操作した履歴がそのまま、再実行可能なPowerShellスクリプトとして表示されるようになりました。

たとえばユーザーを作成する作業をGUIでやります。

image

するとその作業内容がこのように履歴ペインに表示されます。

image

履歴はまさにPowerShellスクリプトそのままなので、これを「コピー」してISEに貼りつけるともうスクリプトファイルが完成します。

image

あとは必要に応じてパラメータを変更したり、ループを設けて繰り返し処理をしたり、自由自在に再利用ができます。ここではユーザー名を”testuser2”に変えて実行してみました。最初にGUIで作ったユーザーと同じ設定を用いて複数のユーザーを作成することが簡単に行えます。ユーザー名を記載したCSVファイルを読み込んでそのユーザーを一括作成することなどもできますね(PowerShellにはImport-CsvというCSVファイルを扱うコマンドレットもあります)。

スクリプトファイルにして保存すれば何回でも実行可能ですし、PSScheduledJobモジュールを使ってタスクスケジューラに登録すれば定期実行も可能です。

履歴スクリプトのうち、どこからどこまでが「今やった作業」なのか分かりにくいこともあると思いますが、そういうときには履歴ペインの「タスクの開始」をクリックし、今からやりたい作業名をまずメモします。

image

そしてGUIで作業を行い、終わったら「タスクの終了」をクリックします。

image

作業単位でこれを繰り返します。すべてが終わったら履歴をすべてコピーしてISEに貼りつけてみます。

image

するとこのようにタスク名がコメント行として挿入され、スクリプトのどこからどこまでが、どの作業に対応しているかが明確になるわけです。

このように管理GUIがPowerShellモジュール(コマンドレット)のフロントエンド的存在となり、GUIでの操作によりPowerShellコマンドレットが実行され、その履歴はスクリプトとして再利用が可能というMicrosoftが提唱する新しい管理方式が、ついにWindows Serverの本丸的存在Active Directoryに採用されたということになります。2008R2のActive Directory管理センターも内部的にはそういう構造だったのですが、履歴をスクリプトとして取り出す方法がなかったのですが、Server8からは可能になったということになります。

これまではこの構造が完全に組み込まれていたのはExchange Serverなど限られた製品のみでしたが、今後はこちらの構造が主流になると思われます。GUIとCUIの「いいとこどり」ができるこの構造はなかなか理想的なシステムなんじゃないかと思います。

PowerShellは3.0になってますますWindows OS管理の中核として重要度が高まっており、それに答えるように様々な機能増強が行われています。今回紹介したのはその一面ですが、特にServer8においてPowerShellがいかに重要かはWindows Server "8" に関するテクニカル プレビューの各項目の記述内容の多くにPowerShellに関する記載があることでも分かるかと思います。

2011/12/19

はじめに

PowerShell Advent Calendar 2011の19日目の記事、そしてこれが私の記事では3回目となります。今回も前々回前回からの引き続きでバックグラウンドジョブについての話題です。前回までは現行バージョンであるPowerShell 2.0におけるバックグラウンドジョブの機能の使い方を解説してきましたが、今回はPowerShellの次期バージョンである3.0に追加される予定の機能のうち、ジョブ関係のものをピックアップしてみます。現在PowerShell 3.0を含むWindows Management Framework(WMF)3.0のCTP2が公開されています。またWindows 8 Developer Preview / Windows Server 8 Developer PreviewにはWMF3.0 CTP1相当のPowerShell 3.0が含まれています。

注意:本記事で取り上げた内容は製品のプレビュー版をもとに記述しています。そのためリリース版では内容が一致しない可能性があることをご承知おきください。

using:ラベル

前回、ジョブに値を渡す方法について解説しましたが、-argumentListに引数として渡すというのは正直めんどうです。呼び出し元のグローバル変数を直接ジョブ側から参照したいですよね。そこでPowerShell v3では新たに変数に付けるusing:ラベルというのが追加されました。このラベルをジョブのスクリプトブロック内で使うと、呼び出し元の変数を参照することができます。具体例。

$test="PowerShell 3.0"
Start-Job {$using:test}|Wait-Job|Receive-Job

とすると、「PowerShell 3.0」と表示され、たしかにジョブのスクリプトブロックから呼び出し元の変数を参照できていることがわかります。これは便利ですね。ただし残念ながらこの方法を使ってもスクリプトブロックをジョブに渡すことはできないようです。相変わらず文字列にキャストされてしまいました。

Receive-Jobコマンドレットの変更点

前々回に、Invoke-Command -asJobで複数リモートコンピュータに対してジョブを走らせた場合、そのジョブに対して$job|Receive-Jobがなぜか機能しない、と書きましたがこの問題が解決されています。そもそもなんでこの問題が発生していたのか、面白いのでちょっと解説します。

実はReceive-Jobコマンドレットの-locationパラメータに「パイプライン入力を許可する   true (ByPropertyName)」フラグがついていたのが原因でした。複数コンピュータに対して実行したジョブは子ジョブを複数持ちますが、親ジョブ自体は配列ではありません。そしてそのLocationプロパティには子ジョブが実行されているコンピュータ名が"remote01,remote02,remote03"のようなカンマ区切りの文字列として格納されています。よってこのジョブオブジェクトをパイプラインを通じてReceive-Jobコマンドレットに渡すと、ValueFromPipelineByPropertyName属性が付いている-locationパラメータにジョブオブジェクトのLocationプロパティの値が渡されますが、その値はカンマ区切りの文字列なので正しく解釈されず、結果として期待の動作をしなかったわけです。

v3ではReceive-Job -locationのValueFromPipelineByPropertyName属性が取り除かれ、問題なく動作するようになりました。

他の変更点としてはReceive-Jobにジョブが完了するまで待つための-waitパラメータが追加されました。が、$job|Wait-Job|Receive-Jobと違いが分からないかも…。

Get-Jobコマンドレットの変更点

Get-Jobに-filterパラメータが追加されました。連想配列でジョブにフィルタをかけられるものです。

Get-Job -filter @{State="Completed";Location="localhost"}

where-objectを使わずともフィルタできるので便利、かも。しかし個人的には-filterパラメータはいろんなコマンドレットで定義されているものの、使い方がそれぞれ異なるのがとてもとてもイヤです。まず覚えられないのでヘルプを引くところから始まっちゃいますので。パフォーマンスの関係上、Where-Objectを使うよりコマンドレット内部でフィルタしたほうが速くなるというのはわかるのですが、もう少しフィルタ方式に統一性を持たせられなかったんだろうかとか思いますね。

Get-Jobにはほかに-afterと-beforeというパラメータが追加されています。これは後述するPSScheduledJobの完了時刻をDateTimeで範囲指定し、フィルタするものです。

PowerShell Workflow

PowerShell3.0というかWMF3.0のおそらく目玉機能の一つがPowerShell Workflowです。文字通り、PowerShellでワークフローが記述できるようになります。

Workflowは関数の一種なのですが、長時間を要するタスクやリモート実行や並列実行などで使うことを主目的としているようです。functionキーワードの代わりにworkflowキーワードでワークフローを定義すると、自動的に実行対象コンピュータ名や資格情報といったパラメータが複数定義されるので、これらのパラメータを特に定義なしで利用することができます。またworkflow内ではparallelブロックを定義でき、その中に記述された各行は並列に実行されます。またfor/foreachステートメントで-parallelパラメータが利用可能になり、繰り返し処理やコレクションの列挙を並列して行うことができるようになります。

自動定義されるパラメータに-asJobがあり、これを利用するとworkflowをジョブとして実行できます。このジョブは通常のジョブとは違い、新たに追加されたSuspend-JobコマンドレットとResume-Jobを使うことによって、ジョブの一時中断と再開ができます。このジョブの中断と再開は、リモートコンピュータ上でワークフローを走らせてるときでも可能ですし、中断後リモートセッションが切断されたあとに再開することもできますし、リモートコンピュータがシャットダウンしても再起動後にジョブを再開することまでできてしまいます。これらはWMFにおけるリモート基盤を支えているWinRMの最新バージョン、WinRM3.0が実現している機能です。このようにセッションを再接続してもタスクを継続できるような接続をrobust(堅牢な), resilient(弾力性のある、障害から容易に回復する) connectionと称しているようです。

PowerShell WorkflowはWindows Workflow Foundation(WF)と密接な関係があり、WFのデザイナで作ったxamlをPS Workflowに変換したり(逆もできる?)、Invoke-Expressionでxamlを実行したりできるらしいです。WF側でもPowerShellの多くの機能がアクティビティとして使用できたりして、WFとPowerShellがWMFというシステム管理フレームワークの主要なパーツとして密に連携していくようです。このあたりの話はWFの専門家であるAhfさんがPSアドベントカレンダーの23日目にしてくださる予定なので、楽しみですね!

なおPS Workflowは従来のPSスクリプトとは異なった利用状況を想定しているため、あるいはWFの機能と合わせるため、PSスクリプトではできるのにPS Workflowではできないことがとてもたくさんあります。forの中でbreakやcontinueステートメントが使えないとかStart-Sleepは-Secondパラメータしか指定できない(ミリ秒単位でスリープかけられない)とか色々あります。そのうちPS WorkflowとPSスクリプトの違いというドキュメントが公開されるんじゃないかと思います。

ちなみにWinRM3.0のおかげでワークフローではない通常のリモートジョブでも、New-PSSessionで作成したセッションの中でジョブを実行した場合、そのジョブが動作しているコンピュータへのセッションを切断(Disconnect-PSSession)したあと、セッションに再接続(Connect-PSSessionやReceive-PSSession)すればジョブの結果を取得したりすることができます。またセッションを作製したインスタンス(powershell.exe)でそのセッションを切断すると、それ以降は別のインスタンスやコンピュータからそのセッションにConnect-PSSessionで接続することができます。

ScheduledTasksモジュール

PowerShell3.0が含まれる次期Windowsでは大量のモジュールが追加され、それらのモジュールに含まれるコマンドレットの総数はWindows 8でも2000を超える膨大な量になります。これはWindows 8やWindows Server 8では従来のコマンドプロンプトから実行するコンソールexeコマンドのほとんどすべてをPowerShellコマンドレットに置き換える措置のためです。もちろん従来のコマンドは互換性のために残されますが、netsh.exeなど一部のコマンドではPowerShellへの移行を促すメッセージが表示されたりするようになるようです。参考:Window 8の機能の概要 − @IT

ScheduledTasksモジュールというタスクスケジューラを扱うモジュールもWindows 8 / Windows Server 8に新しく追加されるモジュールの一つで、schtasks.exeを置き換えるものとなります。これまでPowerShellでタスクスケジューラを扱うにはschtasks.exeを使うか、WMIのWin32_ScheduledJobを使う必要があり面倒でしたが、このモジュールに含まれるコマンドレットを用いるとそれが容易に行えるようになります。たとえば「notepad.exeを毎日朝10:00に起動する。バッテリ駆動のときでも実行」というタスクを「test」という名前で登録するには、

$action = New-ScheduledTaskAction -Execute "notepad.exe"
$trigger = New-ScheduledTaskTrigger -At "10AM" -Daily
$setting = New-ScheduledTaskSettings -AllowStartIfOnBatteries 
New-ScheduledTask -action $action -trigger $trigger -setting $setting|Register-ScheduledTask -TaskName test

とすれば可能であるはずです。実はServer 8 Developer Preview版ではこのコードは機能しません。タスクのトリガを作成するNew-ScheduledTaskTriggerコマンドレットが正しいオブジェクトを作ってくれないのです。これは将来のバージョンできっと修正されるかと思います。ただトリガを定義する部分をはずせば(あんまり意味はないですが)このコードは動作するので、やり方はたぶんあってると思います。

Register-ScheduledTaskコマンドレットには-asJobパラメータがあり、タスクスケジューラへの登録をジョブとしてバックグラウンドで行うことができます。ScheduledTasksモジュールはWMIを利用してタスクスケジューラを操作するので、ほかのWMI関係のコマンドレットと同様ですね。

なおScheduledTasksモジュールはデフォルトでは読み込まれていないので、使用するには本来Import-Moduleコマンドレットを使用しなければならないところですが、PowerShell3.0のCmdlet Discoveryという機能によりImport-Moduleは実行しなくてもScheduledTasksモジュールに含まれるコマンドレットを利用することができます。Cmdlet Discoveryとは現在読み込まれていて実行可能なコマンドレットの中にない、未知のコマンドレットを実行しようとしたとき、Modulesフォルダに存在するモジュールから同名のコマンドレットが定義されているものを探し出し、発見できたらそのモジュールを読み込んだうえでコマンドレットを実行するという優れた機能です。初回だけモジュールの検索とロードの手順が実行されるので待たされますが、一度Cmdlet Discoveryによってモジュールがシェルに読み込まれればあとは快適にコマンドレットを実行できるようになります。

PSScheduledJobモジュール

ScheduledTasksモジュールは-asJobパラメータが定義されているくらいで実はそれほどPowerShellのジョブとは関係ないのですが、ScheduledTasksモジュールが内包しているPSScheduledJobモジュールはPowerShellのジョブ機能と大いに関係があります。

従来PowerShellスクリプトをタスクスケジューラに登録するにはコマンドラインに"powershell.exe"を、引数に"-file hoge.ps1"を指定して、みたいなまわりくどいことをする必要がありました。しかし新しく追加されるPSScheduledJobモジュールに含まれるコマンドレット群はこの問題を解消します。PowerShellスクリプト(.ps1)あるいはスクリプトブロックをPSScheduledJobとして直接タスクスケジューラに登録できるようになり、PowerShellとタスクスケジューラのシームレスな連携を実現します。こちらはWindows 8/Server 8に付属のモジュールではなく、PowerShell 3.0に付属のモジュールなので、Win7などでも使用可能になる予定です。

使用例を見ていきましょう。

$triggers = @()
$triggers += New-JobTrigger -at "2012/01/01 11:11:10" -Once
$triggers += New-JobTrigger -at "10:00" -Daily

$sb = {
    "This is Scheduled Job."
    Get-Date
}

Register-ScheduledJob -ScriptBlock $sb -Trigger $triggers -Name ScheduledJobTest1

まずNew-JobTriggerコマンドレットによってトリガー(具体的には実行時刻など)を定義します。ここでは決められた時刻に1回実行するものと、毎日同じ時刻に実行するものの2つを定義してみました。そしてこれらの時刻に実行したい内容をスクリプトブロックに記述し、これらをRegister-ScheduledJobコマンドレットで登録してやります。

するとこのスクリプトブロックはタスクスケジューラに登録され、指定時刻になると指定したスクリプトブロックの内容が実行されます。このタスクは「タスクスケジューラ― ライブラリ\Microsoft\Windows\PowerShell\ScheduledJobs」に登録されています。

このタスクのアクションは具体的には次のようになっています。

powershell.exe -NoLogo -NonInteractive -WindowStyle Hidden -Command "Import-Module PSScheduledJob; Start-Job -DefinitionName 'ScheduledJobTest2' -DefinitionPath 'C:\Users\Administrator\AppData\Local\WindowsPowerShell\ScheduledJobs' -WriteToStore | Wait-Job"

これによると、指定時刻に実際にタスクスケジューラによって実行されるのはpowershell.exeであり、Start-Jobコマンドレットを使って登録したスケジュールをPowerShellのジョブとして実行していることがわかります。Start-Jobコマンドレットの-DefinitionNameパラメータなどはPSScheduledJobのために追加されたもので、これによりRegister-ScheduledJobが出力したPSScheduledJob定義をファイルから読み込んでジョブとして実行できるようになっています。PSScheduledJob定義とジョブの出力は-DefinitionPathで指定されているフォルダの下にxmlファイルとして保存されているので興味がある方は覗いてみるといいかもしれません。

さて、スケジュールしたジョブの実行結果はどうやって受け取ればいいのでしょうか。実はこれはすごく簡単で、PSScheduledJob(ここではScheduledJobTest1という名前で定義しました)がタスクスケジューラによって一度以上実行された後は、

$job=Get-Job -name ScheduledJobTest1

とすることでJobオブジェクトとして取得することができるようになります。あとは通常のジョブと同じ取り扱いができるので、

$job|Receive-Job

などで実行結果を取得できます。

ちなみにPSScheduledJobはそれを定義したインスタンス以外でも参照することができます。具体的にはpowershell.exeでジョブをスケジューリングして終了→また別のpowershell.exeを立ち上げてimport-module PSScheduledJobしたあとGet-Job|Receive-JobしてPSScheduledJobの結果を参照、みたいなことができます。

ここで紹介した一連の操作ではスクリプトブロックをPSScheduledJobにしましたが、Register-ScheduledJobコマンドレットの-FilePathパラメータを用いれば.ps1ファイルをPSScheduledJobとして登録することも可能です。

現行バージョンのPowerShellはとにかく起動が遅いため、タスクスケジューラにスクリプトを登録しても実行が始まるまで何十秒も待たされるなどはざらでしたが、PSv3は起動がずいぶん速くなり、スペックや状況にもよるとは思いますがpowershell.exeの起動後ほんの数秒でスクリプトが走り始めます。この速度のおかげもあってPSScheduledJobはきっととても有効に機能するんじゃないかと思います。

おわりに

今回はPowerShell 3.0で増強されるバックグラウンドジョブ関係の機能をまとめてみました。これらの新機能のおかげで、時間のかかる処理や定期実行する処理を扱うのが飛躍的にやりやすくなりそうです。PowerShell 3.0で追加される機能は他にもたくさんあって、このブログでもいつか全部紹介したいと思ってるのですが、今回取り上げたジョブ関係はその中でもかなり重要な機能増加を多く含んでいると言えるでしょう。PowerShell 3.0やWindows 8/Server 8のリリースに備えてジョブ関係から予習しておくのは悪くないと思いますよ。

なんか25日のアドベントカレンダーのうち3回もバックグラウンドジョブネタをやって、PSアドベントカレンダーというより私だけ一人でPSジョブアドベントカレンダーをやってる感じでちょっと申し訳ないんですが、どうか許してください。そして前回は今回で終了するって言ってたんですが、実はまだジョブ関係の小ネタが残ってるので最終日25日にさせてください。では今日のところはこのへんで。明日はwaritohutsuさんの登場です。よろしくお願いします。

2011/05/16

昨日の記事で取り上げたJavaScriptSerializerを用いると、連想配列から楽にJSONを作成できることが分かったのですが、この記事を書いていて思ったのは、「PowerShellの連想配列って意外に使えるな」という点でした。

現在のところ、PowerShellは独自のクラスを記述する方法がありません(Add-Typeコマンドレットを用いてC#などでクラスを書いて利用することはできますが)。Add-Memberコマンドレットを用いると、既存のオブジェクトに対し、任意のプロパティやPowerShellスクリプトで記述したメソッドを追加することはできます。素のオブジェクトであるPSObjectをNew-Objectして作ったオブジェクトでもこれは可能なので、一応ユーザー定義オブジェクトを作ることは可能です。ですが、Add-Memberコマンドレットを使うのはちょっとめんどくさいです。

Windows PowerShellインアクション」ではAdd-Memberを使いPowerShellの関数を駆使してクラス定義構文のようなものを実装した例はありますが、いささか大仰な感は否めません。

しかし連想配列をユーザー定義オブジェクト代わりに使うと、簡単にできますしそこそこ便利に使えます。

連想配列をオブジェクトの代わりにすることのメリットとデメリット

連想配列をオブジェクトの代わりにすることのメリットは以下の三点があるかと思います。

  1. 簡単な記述(連想配列のリテラル)でオブジェクトが作成できる
    PowerShellの連想配列リテラル@{}を使うことで、簡単に記述できます。またそれを配列化するのも@()を使うと容易です。
    $pcItems=
    @(
        @{
            code=25;
            name="ハードディスク2TB";
            price=7000;
        },
        @{
            code=56;
            name="メモリ8GB";
            price=8000;
        }
    )
  2. ドット演算子で値の参照、設定ができる
    PowerShellの連想配列は「連想配列[キー名]」のほかに、「連想配列.キー名」でもアクセスできる。
    Write-Host $pcItems[1].name # 値の参照
    $pcItems[1].name = "test" # 値の設定
    $pcItems[0].maker = "Seagate" # 要素の追加
    
  3. 連想配列の配列に対してWhere-Objectコマンドレットでフィルタをかけることができる
    これは2とも関係しているのですが、通常のオブジェクトと同様にWhere-Objectコマンドレットでのフィルタ、ForEach-Objectコマンドレットでの列挙が可能です。
    $pcItems|?{$_.price -gt 7000}|%{Write-Host $_.name}

このようにメリットはあるのですが、本物のオブジェクトではないのでそれに起因するデメリットがいくつかあります。

  1. 要素(プロパティ)をいくらでも自由に追加できてしまう
    これはメリットではあるのですが、デメリットでもある点です。後述するデメリットのせいで、同じキーをもつ連想配列の配列を作ったつもりでも、どれかのキー(プロパティ名)を間違えていた場合、それを検出するのが困難です。
  2. メソッドがうまく記述できない
    連想配列要素にスクリプトブロックを指定し、&演算子で実行することでメソッド的なことはできます。しかしこのスクリプトブロック内では$thisが使えず、オブジェクトのプロパティにアクセスすることができないのでいまいちです。
    $pcItem= @{
        name="ハードディスク2TB";
        price=7000;
        getPrice={Write-Host $this.price};
    }
    &$pcItem.getPrice # 何も表示されない。$thisが使えない
    # getPrice={Write-Host $pcItem.price}ならOKだが…
  3. Get-Member、Format-List、Format-Tableなどが使えない
    これらのコマンドレットはあくまで連想配列オブジェクト(Hashtable)に対して行われるので、意図した結果になりません。たとえば$pcItems|Format-Listした場合、
    Name  : name
    Value : ハードディスク2TB
    
    Name  : code
    Value : 25
    
    Name  : price
    Value : 7000
    
    Name  : name
    Value : メモリ8GB
    
    Name  : code
    Value : 56
    
    Name  : price
    Value : 8000
    こんな表示になってしまいます。
連想配列をユーザー定義オブジェクトに変換する関数ConvertTo-PSObject

このように、連想配列の記述のお手軽さは捨てがたいものの、いくつかの問題点もあるのが現実です。そこで連想配列のお手軽さを生かしつつ、ユーザー定義オブジェクトの利便性も取るにはどうすればいいか考えました。結論は、「連想配列を変換してユーザー定義オブジェクトにする関数を書く」というものでした。それが以下になります。

#requires -version 2
function ConvertTo-PSObject
{
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [System.Collections.Hashtable[]]$hash,
        [switch]$recurse
    )
    process
    {
        foreach($hashElem in $hash)
        {
            $ret = New-Object PSObject
            foreach($key in $hashElem.keys)
            {
                if($hashElem[$key] -as [System.Collections.Hashtable[]] -and $recurse)
                {
                    $ret|Add-Member -MemberType "NoteProperty" -Name $key -Value (ConvertTo-PSObject $hashElem[$key] -recurse)
                }
                elseif($hashElem[$key] -is [scriptblock])
                {
                    $ret|Add-Member -MemberType "ScriptMethod" -Name $key -Value $hashElem[$key]
                }
                else
                {
                    $ret|Add-Member -MemberType "NoteProperty" -Name $key -Value $hashElem[$key]
                }
            }
            $ret
        }
    }
}

ご覧のようにコード的には割にシンプルなものが出来ました。連想配列またはその配列をパラメータにとり、またはパイプラインから渡し、連想配列要素をプロパティまたはメソッドに変換してPSObjectにAdd-Memberしてるだけです。-recurseパラメータを付けると連想配列内に連想配列がある場合に再帰的にすべてPSObjectに変換します。

それでは実際の使用例を挙げます。

# 一番単純な例。パラメータに連想配列を渡すとPSObjectに変換する。
$book = ConvertTo-PSObject @{name="Windows PowerShell ポケットリファレンス";page=300;price=2000}
Write-Host $book.name # 「Windows PowerShell ポケットリファレンス」と表示
$book.name="test" # プロパティに値をセットする
Write-Host $book.name # 「test」と表示
#$book.size="A5" # 存在しないプロパティに値を代入しようとするとエラーになる

# 連想配列をコードで組み立てていく例。
$mutaHash=@{} # 空の連想配列を作る
$mutaHash.name="mutaguchi" # キーと値を追加
$mutaHash.age=32
$mutaHash.introduce={Write-Host ("私の名前は" + $this.name + "です。")} # スクリプトブロックを追加
$mutaHash.speak={Write-Host ($args[0])} # パラメータを取るスクリプトブロックを追加
$muta = $mutaHash|ConvertTo-PSObject # 連想配列はパイプラインで渡すことができる
$muta.introduce() # 「私の名前はmutaguchiです。」と表示
$muta.speak("こんにちは。") # 「こんにちは。」と表示

# 連想配列の配列→PSObjectの配列に変換
$stationeryHashes=@()
$stationeryHashes+=@{name="鉛筆";price=100} 
$stationeryHashes+=@{name="消しゴム";price=50}
$stationeryHashes+=@{name="コピー用紙";price=500}
$stationeryHashes+=@{name="万年筆";price=30000}
$stationeries = ConvertTo-PSObject $stationeryHashes
# "200円以上の文具を列挙"
$stationeries|?{$_.price -ge 200}|%{Write-Host $_.name} # 「コピー用紙」と「万年筆」が表示

# 連想配列の配列をリテラルで一気に記述する
$getPrice={Write-Host $this.price} # 共通のメソッドを定義
$pcItems=
@(
    @{
        code=25;
        name="ハードディスク2TB";
        price=7000;
        getPrice=$getPrice
    },
    @{
        code=56;
        name="メモリ8GB";
        price=8000;
        getPrice=$getPrice
    },
    @{
        code=137;
        name="23インチ液晶ディスプレイ";
        price=35000;
        getPrice=$getPrice
    }
)|ConvertTo-PSObject
$pcItems[1].getPrice() # 「8000」と表示
$pcItems|Format-List
<#
表示:
name  : ハードディスク2TB
code  : 25
price : 7000

name  : メモリ8GB
code  : 56
price : 8000

name  : 23インチ液晶ディスプレイ
code  : 137
price : 35000
#>

# 連想配列の中に連想配列を含めたもの→PSObjectをプロパティの値に持つPSObject
$blog=
@{
    utl="http://winscript.jp/powershell/";
    title="PowerShell Scripting Weblog";
    date=[datetime]"2011/05/16 00:25:31";
    keywords=@("スクリプト","PowerShell","WSH"); # 配列を含めることもできる
    author=@{name="mutaguchi";age=32;speak={Write-Host "ようこそ私のブログへ"}} # 連想配列を含める
}|ConvertTo-PSObject -recurse # -recurseパラメータを指定すると再帰的にすべての連想配列をPSObjectに変換する
$blog.author.speak() # 「ようこそ私のブログへ」と表示
Write-Host $blog.keywords[1] # 「PowerShell」と表示
# ※配列要素に連想配列以外の値が含まれている場合は展開しない

このように簡単な関数一つで、連想配列にあった問題点をすべて解消しつつ簡単な記述で独自のオブジェクトを記述できるようになりました。おそらくかなり便利だと思いますのでぜひ使ってみてください。

余談:ScriptPropertyを使う場合

余談ですが、今回使用したNotePropertyはプロパティに代入できる型を指定したり、リードオンリーなプロパティを作ったりすることができません。そういうのを作りたい場合はScriptPropertyを使います。Add-Memberコマンドレットの-valueパラメータにゲッターを、-secondValueパラメータにセッターをそれぞれスクリプトブロックで記述します。

しかしこいつはあまりいけてないです。これらのスクリプトブロック内で参照するフィールドを別途Add-MemberでNotePropertyを使って作成する必要があるのですが、これをprivateにすることができません。よってGet-Memberでもばっちり表示されてしまいますし、フィールドを直接書き換えたりもできてしまいます。

また今回のように連想配列をPSObjectに変換する場合はprivateフィールド名も自動生成する必要があるのですが、それをScriptProperty内のゲッター、セッターから取得する方法がなく、たぶんInvoke-Expressionを使うしかありません。

これらを踏まえて元の連想配列要素の値の型を引き継ぎ、それ以外の型を代入できないようにしたScriptPropertyバージョンも一応書いてみました。ConvertTo-PSObject関数のelse句の部分を以下に置き換えます。

#$ret|Add-Member -MemberType "NoteProperty" -Name ("_" +$key) -Value $hash[$key]
"`$ret|Add-Member -MemberType ScriptProperty -Name $key -Value {[" + $hash[$key].gettype().fullname + "]`$this._" + $key + "} -SecondValue {`$this._" + $key + "=[" + $hash[$key].gettype().fullname + "]`$args[0]}"|iex

まあこれはいまいちなんで参考程度に。

2012/08/23追記
この記事を書いた時は知らなかったのですが、実は単にNotePropertyだけを持つユーザー定義オブジェクトを作成するのであれば、もっと簡単な方法があります。

# PowerShell 1.0
$o=New-Object PSObject|Add-Member noteproperty Code 137 -pass|Add-Member noteproperty Name 23インチ液晶ディスプレイ -pass

# PowerShell 2.0
$o=New-Object PSObject -Property @{Code=137;Name="23インチ液晶ディスプレイ"}

# PowerShell 3.0
$o=[pscustomobject]@{Code=137;Name="23インチ液晶ディスプレイ"}

3つのコードはほぼ等価です。PowerShell 2.0と3.0では連想配列リテラルを用いて簡単にカスタムオブジェクトを作れるようになりました。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2011/05/16/199086.aspx

Copyright © 2005-2016 Daisuke Mutaguchi All rights reserved

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

Awards

Books

Twitter