2012/12/14

本記事はPowerShell Advent Calendar 2012の14日目の記事になります。

前回(アドベントカレンダー1日目)は「PowerShellらしい関数の書き方」と題して、パイプライン内でうまく他のコマンドと連携させるための関数をどう書けばいいのか、ということについて書きました。前回の関数の例では入力型と出力型がstringだったのですが、実際は自分で定義した型を入力、出力値に取るように書くのが普通かと思います。今回は、それをするためにどうやって型を定義するのか、そしてその型を関数にどう指定するのか、という話をします。

PowerShellにはクラス定義構文がない

そもそもの話になるんですが、型を定義する、つまりはクラスを記述するためのPowerShellのステートメントやコマンドレットが無いため、PowerShell単独ではできません。なので無理です以上おしまい。…というわけにはいかないので、実際はどうするのがいいのかという話をしていきます。

方法としては大きく分けて二つあると思います。

1.C#など他の.NET言語を用いてクラスを記述する

2.ユーザー定義オブジェクトを作成する

今回は1の方法を説明します。

C#を用いてクラスを記述する

つまりはPowerShellでクラスを定義できないなら、C#を使えばいいじゃない。ということです。幸いPowerShell 2.0からはAdd-Typeというコマンドレットを用いると、C#やVBなど.NET言語のソースをその場でコンパイルしてアセンブリとして現在のセッションに読み込むことが可能です。

たとえば、論理ドライブを表すDriveというクラスを定義してみます。

Add-Type -TypeDefinition @"
    namespace Winscript
    {
        public enum DriveType
        {
            Unknown, NoRootDirectory, RemovableDisk, LocalDisk, NetworkDrive, CompactDisc, RAMDisk
        }

        public class Drive
        {
            public string Name {get;set;}
            public string VolumeName {get;set;}
            public DriveType Type {get;set;}
            public long Size  {get;set;}
            public long FreeSpace  {get;set;}
            public long UsedSpace  {get;set;}
            public string RootPath {get;set;}
        }
    }
"@ -Language CSharpVersion3

このようにC#のコードを文字列として-TypeDefinitionパラメータに与えると、コンパイルされて指定のクラス(ここではWinscript.Drive)がロードされます。

ここで-Language CSharpVersion3というパラメータは指定コードをC# 3.0としてコンパイルすることを指定するため、今回使用している自動実装プロパティなどC# 3.0の構文が利用できます。なおこのパラメータはPowerShell 3.0では不要です。ただし明示しておくとPowerShell 2.0でも正しく動作します。というのも-Languageパラメータ省略時はPowerShell 2.0ではC# 2.0でコンパイルされるのですが、PowerShell 3.0ではC# 3.0でコンパイルするためです(逆にPSv3でC#2.0でコンパイルするには”CSharpVersion2”という新しく追加されたパラメータ値を指定します)

なお、ここでは-TypeDefinitionパラメータを用いてクラス全体を記述しましたが、この例のように列挙体も定義してそれをプロパティの型にするなどせず、すべて基本型のプロパティで完結するのならば、-MemberDefinitionパラメータを使ってメンバ定義だけを行う方が記述が短くなります。以下はWinscript.Manというクラスを定義する例です。

Add-Type -Namespace Winscript -Name Man -MemberDefinition @"
    public int Age {get;set;}
    public string Name {get;set;}
"@ -Language CSharpVersion3

例のようにC#のコード内には特にロジックを記述せず、単にデータの入れ物となるクラスにとどめておくのが良いかと思います。別にロジックを書いてもいいのですが、ISEで記述する限りはC#の編集に関してはただのテキストエディタレベルの恩恵しか受けないですし、それなら最初からVisual Studio使ってC#で全部コマンドレットとして書けばいいのに、ともなりかねないので。PowerShellでは実現困難な処理などがあればそれをメソッドとして書く程度ならいいかもしれません。ただしメソッドを記述してもそれをユーザーに直接使わせるというよりも、関数でラップして使わせる形が望ましいでしょう。

さて、次はこのクラスのオブジェクトを扱う関数を記述していきます。

定義した型のオブジェクトを扱う関数の記述

ここでは3つの関数を定義しています。Get-Drive関数はシステムに含まれるすべての論理ドライブを取得、Show-Drive関数は指定のDriveオブジェクトをエクスプローラで開く、Set-Drive関数は指定のDriveオブジェクトのボリューム名(VolumeNameプロパティ)を変更するものです。

ちなみに関数の動詞部分(ここではGet, Show, Set)は、Get-Verb関数で取得できるリスト以外のものは基本的に使わないようにします。モジュールに組み込んだ場合、インポートのたびに警告が出てしまうので。

関数の基本については前回に書いているので、今回のコードはそれを踏まえて読んでみてください。

function Get-Drive
{
    [OutputType([Winscript.Drive])]
    param(
        [string[]]$Name,
        [Winscript.DriveType]$Type
    )

    Get-WmiObject -Class Win32_LogicalDisk | ForEach-Object {
        if($null -ne $Name -and $Name -notcontains $_.Name)
        {
        }
        elseif($Type -ne $null -and $_.DriveType -ne $Type)
        {   
        }
        else
        {
            New-Object Winscript.Drive -Property @{
                Name = $_.Name
                VolumeName = $_.VolumeName
                Type = [enum]::Parse([Winscript.DriveType],$_.DriveType)
                RootPath = if($_.ProviderName -ne $null){$_.ProviderName}else{$_.Name + "\"}
                Size = $_.Size
                FreeSpace = $_.FreeSpace
                UsedSpace = $_.Size - $_.FreeSpace
            }
        }
    }
}

(↑10:33 foreachステートメントではなくForEach-Objectコマンドレットを使うように修正。Get-*な関数のようにパイプラインの先頭で実行する関数でも、内部でPowerShellのコマンドレットや関数の出力を利用する場合は、配列化してforeachするよりも、ForEach-Objectで出力を逐次処理した方が良いですね。内部関数の出力がすべて完了してから一気に出力するのではなく、内部関数が1個オブジェクトを出力するたびに出力するようにできるので。)

function Show-Drive
{
    [OutputType([Winscript.Drive])]
    param(
        [Parameter(ValueFromPipeline=$true,Mandatory=$true)]
        [Winscript.Drive[]]
        $Drive,
        
        [switch]
        $PassThru
    )

    process
    {
        foreach($d in $Drive)
        {
            Start-Process $d.Name
            if($PassThru)
            {
                $d
            }
        }
    }
}
function Set-Drive
{
      param(
        [Parameter(ValueFromPipeline=$true,Mandatory=$true)]
        [Winscript.Drive]
        $Drive,
        
        [Parameter(Mandatory=$true)] 
        [string]
        $VolumeName
    )

    process
    {
        Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='$($Drive.Name)'" |
            Set-WmiInstance -Arguments @{VolumeName=$VolumeName} | Out-Null
    }
}

細かい説明は省きますが、前回説明した関数の基本フォーマットに、自分で定義した型を適用してロジックを書くとこうなる、という参考例としてとらえてください。

一つだけ前回に説明し忘れてたことがあります。それは[OutputType]属性です。これは文字通り、関数の出力型を指定するものです。この属性を指定しておくと何が嬉しいかというと、関数の出力を変数に代入したりWhere-Objectコマンドレットでフィルタをかけるコードを記述する際、関数の実行「前」にもプロパティ名をちゃんとタブ補完してくれるようになります。残念ながらこの静的解析機能はPowerShell 3.0からのものなので2.0だとできませんが、OutputType属性自体は2.0でも定義可能なので、定義しておくことを推奨します。

さて、型の定義と関数の定義をしたので実際に関数を実行してみます。

PS> Get-Drive # 全ドライブ取得

Name       : C:
VolumeName :
Type       : LocalDisk
Size       : 119926681600
FreeSpace  : 12262494208
UsedSpace  : 107664187392
RootPath   : C:\

Name       : D:
VolumeName :
Type       : LocalDisk
Size       : 500086886400
FreeSpace  : 198589583360
UsedSpace  : 301497303040
RootPath   : D:\

Name       : Q:
VolumeName :
Type       : CompactDisc
Size       : 0
FreeSpace  : 0
UsedSpace  : 0
RootPath   : Q:\

Name       : V:
VolumeName : 
Type       : NetworkDrive
Size       : 1500299390976
FreeSpace  : 571001868288
UsedSpace  : 929297522688
RootPath   : \\server\D

PS> Get-Drive | where {$_.Size -gt 1TB} # Where-Objectでフィルタ

Name       : V:
VolumeName : 
Type       : NetworkDrive
Size       : 1500299390976
FreeSpace  : 571001868288
UsedSpace  : 929297522688
RootPath   : \\server\D

PS> Get-Drive -Type NetworkDrive | Show-Drive -PassThru | ConvertTo-Csv #ネットワークドライブのみエクスプローラーで開く。取得結果はCSVとして出力。
#TYPE Winscript.Drive
"Name","VolumeName","Type","Size","FreeSpace","UsedSpace","RootPath"
"V:","","NetworkDrive","1500299390976","571001868288","929297522688","\\server\D"
PS> Get-Drive -Name D: | Set-Drive -VolumeName 新しいドライブ # D:ドライブのボリューム名を指定。(管理者権限で)

関数をきちんとPowerShellの流儀に従って記述したおかげで、このようにPowerShellの他の標準コマンドレットと同様の呼び出し方ができ、自作関数やそれ以外のコマンド同士をうまくパイプラインで繋げて実行することができています。

さて、おそらく一つ気になる点があるとすれば、ドライブの容量表示が見づらいということでしょう。容量であればGBとかの単位で表示してほしいですし、大きい数字は,で桁を区切ってほしいですよね。じゃあそういう値を文字列で返すプロパティを定義してやる必要があるというかと言えばそんなことはなく、PowerShellには型に応じた表示フォーマットを指定する方法が用意されています。次回はそのあたりを解説しようと思います。

また、C#とかめんどくさいしもうちょっと楽な方法はないのか?ということで、最初の方でちょっと触れた、ユーザー定義オブジェクトを利用する方法も、余裕があれば次回に。

さて、PSアドベントカレンダー、明日はsunnyoneさんです。よろしくお願いします!

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さんの番です。よろしくお願いします。

2011/12/02

はじめに

このたび、技術系アドベントカレンダーイベントの1つとして、PowerShell Advent Calendar 2011を企画しました。この記事はその2日目の記事となります。アドベントカレンダーについてはリンク先を参照してください。

今日のテーマはPowerShellのバックグラウンドジョブ機能の使い方についてのまとめです。

バックグラウンドジョブとは

バックグラウンドジョブ機能はその名の通り、ジョブ(具体的にはスクリプト)をバックグラウンドで非同期に実行するものです。PowerShell v2で追加された機能の一つです。インタラクティブシェルでStart-Jobコマンドレットを使用してバックグラウンドジョブ(以下、単に「ジョブ」と表記)を実行すると、新しくpowershell.exeのプロセスが起動しそのままシェルに制御が戻りユーザーは後続の処理を行うことができます。もちろんスクリプトからジョブを実行することも可能です。時間のかかる処理をバックグラウンドで走らせたり、数多くの処理を並列で実行したりするのに重宝します。

起動されたジョブは操作中のpowershell.exeとは別のジョブ用のプロセスで実行され、処理が完了すると呼び出し元でその結果をReceive-Jobコマンドレットを使って受け取ることができます。ジョブは並列して何個も同時に実行できます。なおPowerShellのジョブは1ジョブ=1プロセスです。スレッドではないので注意。

PowerShellのジョブシステムはリモート処理インフラストラクチャの上に構築されているので、たとえローカルPCでもジョブ実行するにはローカルPCをリモート用構成にしておく必要があります。詳しくはabout_Remote_Requirementsを参照のこと。

ジョブはローカルでもリモートでも走らせることができます。以下に具体的な方法を述べていきます。

ローカルコンピュータでのジョブ実行

ローカルコンピュータ上に新しくジョブを作成して開始するにはStart-Jobコマンドレットを用います。

Start-Job {ジョブとして実行したいコマンド、スクリプト}

とするとジョブを実行します。

$job=Start-Job {..}

のようにするとJobオブジェクト(System.Management.Automation.PSRemotingJob)を変数に格納してあとで利用できます。変数で受けない場合はJobオブジェクトの内容が表示されます。

存在するジョブを取得するにはGet-Jobコマンドレットを用います。

Get-Job

で現在実行中のジョブ一覧を表示します。以下に出力例を示します。

Id              Name            State      HasMoreData     Location             Command
--              ----            -----      -----------     --------             -------
1               Job1            Completed  True            localhost            "test"
3               Job3            Running    True            localhost            start-sleep -sec 120;"...

以下の表は各項目の意味です。

Id ジョブID番号
Name ジョブの名前
State

Running=実行中のジョブ

Stopped=停止したジョブ

Complete=完了したジョブ

Failed=エラーが出たジョブ

HasMoreData 返却されたデータがあるかどうか
Location ジョブが実行されているコンピュータ名
Command ジョブで実行されているコマンド、スクリプト

ジョブの終了を待つにはWait-Jobコマンドレットを用います。

Get-Job|Wait-Job

とすると実行中のジョブすべてが完了するまで待ちます。-timeoutパラメータを使うと最大待ち時間(秒)を指定できます。

Get-Job|Wait-Job -any

とすると実行中のいずれかのジョブが完了するまで待ちます。正確には「対象のジョブが一つ以上完了するまで待つ」という効果なので、完了済みのジョブが1つ以上ある場合に新たにジョブを追加した場合などは想定の動作になりません。あらかじめRemove-Jobで完了済みのジョブを削除するか、Where-ObjectコマンドレットでRunningのみ対象にするようフィルタをかけるかしてください。

ジョブを中止するにはStop-Jobコマンドレットを用います。

Get-Job -id 1|Stop-Job

とするとジョブIDが1のジョブを中止します。

$jobにJobオブジェクトが格納されている場合は

$job|Stop-Job

でもOKです。

ジョブを削除するにはRemove-Jobコマンドレットを用います。

Get-Job|where {$_.state -eq "Completed" -or $_.state -eq "Stopped"}|Remove-Job

とすると完了済みと中止したジョブを削除します。実行中のジョブは削除できませんが-forceパラメータを使って強制削除することは可能です。

ジョブの実行結果データを取得するにはReceive-Jobコマンドレットを用います。

Get-Job|Receive-Job

とすると完了済みのジョブのうち、結果を返却しているもの(HasMoreDataがTrueのジョブ)があればその結果を表示します。-keepパラメータをつければ結果データを保持しますが付けてない場合は参照後破棄します。

*-Job系のコマンドレットの多くはJobオブジェクトを返却するので、パイプラインでどんどん繋げていけます。

Get-Job|Wait-Job -timeout 10|Receive-Job

のように。

ジョブの基本的な使い方に関して詳しくはabout_jobsを参照してください。

イベントサブスクライブ

PowerShell 2.0では.NET Frameworkのオブジェクトのイベントをサブスクライブすることができます。すなわちイベントハンドラを記述することができます。このイベントサブスクライブ機能もジョブ機能を元に構築されています。

たとえばTimerオブジェクトのElapsedイベントをサブスクライブし、タイマーの実行間隔(ここでは1秒)ごとにtest.txtファイルに乱数を追記していくサンプルは次のようになります。

$timer=new-object System.Timers.Timer
$timer.Interval=1000
Register-ObjectEvent -EventName Elapsed -SourceIdentifier test -Action {get-random|add-content c:\users\daisuke\test.txt} -InputObject $timer
$timer.Enabled=$true

Register-ObjectEventの結果、新しくジョブが生成しそのJobオブジェクトが返却されます。このジョブは-EventNameパラメータで指定したイベントが発生するたび、-Actionパラメータで指定したスクリプトブロックを実行します。

なお、イベントサブスクライブを解除するには

Unregister-Event test

のように-SourceIdentifierパラメータで指定した値を指定してUnregister-Eventコマンドレットを実行することで可能です。サブスクライブを解除してもジョブ自体は削除されない(StateがStoppedになるだけ)ので、必要であればRemove-Jobで削除します。

なお.NETオブジェクトの他にPowerShellスクリプトのカスタムイベント(Register-EngineEvent)、WMIオブジェクトのイベント(Register-WmiEvent)をサブスクライブすることもできます。これらのコマンドレットも同様にイベント発生時の処理をジョブとして登録します。詳しくは各コマンドレットのヘルプを参照してください。

リモートコンピュータでのジョブ実行

最初に述べたとおりPowerShellのジョブ機能はリモートインフラストラクチャの上に構築されています。よってローカルのみならずリモートコンピュータに対してジョブを実行することができます。もちろんリモートコンピュータにもリモート構成されていることが条件です。

基本はInvoke-Commandコマンドレットを用い、

$job=Invoke-Command -ComputerName リモートコンピュータ名 {リモートで実行するコマンド、スクリプト} -asjob

となります。これで{}内の処理がリモートコンピュータ上のPowerShellインスタンスで実行されます。-asJobパラメータをつけることでジョブとして(ローカルPCから見て)非同期に処理できますが、-asJobパラメータを省略すると同期的に実行されます。この場合ジョブは作成されず、リモートでの処理が終了するまでローカル側は待機することになります。

リモートコンピュータに接続するための資格情報を別途入力する必要がある場合は-credentialパラメータを使用します。

Invoke-Command -ComputerName リモートコンピュータ名 {リモートで実行するコマンド、スクリプト} -asjob -credential ユーザー名

とするとパスワードを入力するダイアログが表示されます。なお、スクリプトで動かすときなどあらかじめ入力したパスワードを指定したい場合の方法は以前書きました

同じコマンドを複数のリモートPCで同時実行することも可能で、その場合は-computerNameパラメータにリモートコンピュータ名の配列を指定します(「,」区切り)。この場合ローカルPCで見えるジョブとしては1つですが、そのジョブにリモートコンピュータの数だけ子ジョブ(ChildJobs)が作成されています。

このように子ジョブが複数ある場合にReceive-Jobするときは

$job|Receive-Job -location リモートコンピュータ名

あるいは

$job.ChildJobs

として表示される子ジョブの名前(Name)を調べ、

Receive-Job -name 子ジョブの名前

とすることでリモートコンピュータごとに結果を取得できます。

すべての結果をまとめて取得するなら

Receive-Job $job

とします。

$job|Receive-Jobはなぜか駄目なようです。

固定セッションを用いたリモーティング

同じリモートPCに対して何度もコマンドを実行させたい場合、毎回リモートコンピュータ名を指定してセッションを張るのは非効率的なので、リモートセッションを確立したあとその固定セッションを何度も使用する方法が用意されています。新しく固定セッションを確立するにはNew-PSSessionコマンドレットを用い、

$session=New-PSSession リモートコンピュータ名

とすると固定セッションが確立され、$session変数にそのセッションオブジェクトが格納されます。あとは

Invoke-Command $session {リモートで実行するコマンド、スクリプト} -asjob

とすればそのたびにそのセッションを用いてリモートでコマンドを実行できるようになります。

ここまでの説明はリモートコンピュータでしてきましたが、ローカルコンピュータに対して固定セッションを張ることも可能です。

さらに、Enter-PSSessionコマンドレットを用いると作成したセッションに入ってリモートコンピュータ上のPowerShellを対話実行することも可能です。

Enter-PSSession $session

とすると、プロンプトが

PS カレントディレクトリ>

から

[リモートコンピュータ名]: PS カレントディレクトリ> 

に変化し、以降リモートのPowerShellをローカルPCから対話実行できます。

なおこの状態から抜けるにはexitもしくはExit-PSSessionと入力して実行します。

ジョブ実行できるそのほかのコマンドレット

これまで述べたコマンドレット以外にも、いくつかのコマンドレットはジョブ実行(ローカルorリモート)することができます。ジョブ実行するには-asJobパラメータを使用します。以下にv2の段階で-asJobパラメータが定義されているそのほかのコマンドレットを示します。

これらのコマンドレットはコマンドレット自体にジョブ実行機能がついているので、単独で実行するだけならStart-JobやInvoke-Commandを用いる必要がありません。v2ではWMIを扱うコマンドレットにのみ-asJobパラメータが存在するようです(ここに挙げたコマンドレットはすべてWMIの機能を呼び出すもの)。なお、-asJobパラメータが使用できるコマンドレットの一覧を取得するのに、fsugiyamaさんの1日目の記事の問15のスクリプトを使用させていただきました。

おわりに

PowerShell Advent Calendar 2011二日目は、PowerShellのバックグラウンドジョブ機能概要についてまとめてみました。実はバックグランドジョブ機能のTipsを書こうと思ってその前ふりとして書き始めたのですが、これだけでかなりの量になってしまったので概要だけ一記事としてまとめることにしました。おそらくPSアドベントカレンダーに私はあと何回か登場することになりそうですので、Tips編はその際に書こうと思います。

さて、明日三日目は@jsakamotoさんのご登場ですね。よろしくお願いします!

そして参加者はまだまだ募集中ですよ!→PowerShell Advent Calendar 2011

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

2007/08/01

PowerShellは何かとパイプを多用するので、いわゆるif文とかfor文とかだけを使うときに比べると可読性が落ちる傾向にあるように思います。そこで編み出した?のが次の記法です。カレントにある100KBより大きいファイルのフルパスを列挙するというものです。

Get-ChildItem | `
    ? {$_.Length -gt 100KB} | `
    % {$_.FullName}

ポイントは、

・パイプのあとに`記号(改行継続文字)を入れて改行
・スペースを4つ入れてインデント
・Where-Objectコマンドレットは?、ForEach-Objectコマンドレットは%で記述。
・|.`.?,%の前後にはスペースを入れる。

これだと1行に繋げて書くよりかなり見やすいですし分かりやすいですよね?もちろんコンソールに直で打ち込む時はこんなことしなくてもいいですが、スクリプトとして保存するときはこういうのがお勧めです。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2007/08/01/88060.aspx


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

Books

Twitter