2017/12/10

この記事はPowerShell Advent Calendar 2017の10日目です。

PowerShellはオブジェクトを扱うシェルですが、別にテキストデータを扱えない訳ではありません。むしろ、PowerShellで取得したデータをテキストファイルとして保存したり、スクリプトで用いるデータをテキストファイルで保存しておくことは日常的に行われることだと思います。

ただし、PowerShellで扱うデータはオブジェクトであり、テキストファイルは文字通り文字列であることから、コマンドレットを用いる等、何らかの手段で変換が必要になります。また、テキストデータ形式にも様々な種類があり、それぞれメリット、デメリットが存在します。今回の記事では、PowerShellで用いるデータを保持しておく際のテキストデータ形式について比較をしてみます。

プレーンテキスト

プレーンテキスト、すなわち書式なしのテキストファイルです。もっともシンプルな使い方をする場合、文字列配列の各要素に含まれる文字列が、テキストファイルの1行と対応します。

書き出し
$lines | Set-Content -LiteralPath file.txt -Encoding UTF8

$linesは文字列変数です。

特に理由がなければ文字コードはUTF-8で良いと思います。

追記
$lines | Add-Content -LiteralPath file.txt -Encoding UTF8

Add-Contentは実行のたびにファイルを開いて、書き込んでから閉じるという動作をするので、1行ずつforeachで実行するのはNGです。

読み込み
$lines = @(Get-Content -LiteralPath file.txt -Encoding UTF8)
メリット
  • 文字列配列をテキストファイルに書き出すのは多分これが一番楽だと思います。
  • 書き出したデータは人間にも読みやすい。 編集もしやすい。
デメリット
  • 文字列だけを保存しておきたいというケースがそもそも少ない。
CSV

コンマ等の特別な文字で区切り、1行あたりに複数のデータを保存できる形式です。

PowerShellのコマンドレットで扱う場合、オブジェクトが持つプロパティがヘッダ列名に対応します。各行にオブジェクト配列1要素のプロパティ値が、カンマ区切りで保持されます。

書き出し
$objects | Export-Csv -LiteralPath file.csv -NoTypeInformation -Encoding Default

$objectsは任意のオブジェクト配列です。必要であればSelect-Objectコマンドレットを併用して、プロパティを絞り込みます。

文字コードはExcelでそのまま読み込み/書き出しができるDefault(日本語環境ではShift_JIS)がお勧めです。(最近のExcel2016ならUTF8も一応読めますが)

追記
$objects | Export-Csv -LiteralPath file.csv -Append -NoTypeInformation -Encoding Default
読み込み
$objects = Import-Csv -LiteralPath file.csv -Encoding Default
メリット
  • オブジェクトのプロパティ値が、すべて数値あるいは文字列で表現できる値を持つ場合に最も適合する。
  • 人間にも読みやすく、ある程度は編集もできる。
  • Excelで開ける。
デメリット
  • オブジェクトのプロパティが、数値と文字列以外のオブジェクトである場合、すなわち、階層構造を持つデータの保存には適さない。
  • 数値も文字列として読み込まれてしまうので、数値として扱いたい場合は変換が必要になる。
  • Export-CsvとImport-Csvで扱うCSVファイルはヘッダが必須。つまり、ヘッダなしのCSVファイルが既にあって、それを読み書きするという用途には適さない。(できなくはないが)
  • 書き出し時の列順を制御することができない。つまり、PowerShellで書き出したCSVを、列順が固定であるとの想定である他のプログラムで読み込むことは基本NG。
  • 書き出し時、1つ目の要素に存在しないプロパティは、2つ目以降では存在しないものとして扱われる。同種のオブジェクトで構成される配列なら通常は問題ないのだが、要素によって動的に追加されるプロパティがあったりなかったりすると厄介。(ADでありがち)
JSON

JavaScriptのような表記でデータを保持するデータ形式です。データの受け渡しに様々な言語で利用できます。Web APIでもよく利用されます。

PowerShellではv3からJSONを扱うコマンドレットが提供されています。

書き出し
$objects | ConvertTo-Json | Set-Content -LiteralPath file.json -Encoding UTF8
読み込み
$objects = Get-Content -LiteralPath file.json -Encoding UTF8 -Raw | ConvertFrom-Json
メリット
  • CSVと異なり、階層構造を持ったデータでも扱える。
  • CSVと異なり、数値は数値型のまま読み書き可能。 (整数値はint、小数値はdecimal)
  • 人間にもまぁまぁ読めるし、頑張れば編集できなくもない。
デメリット
  • -Depthパラメータによりプロパティを展開する階層の深さを指定はできるが、プロパティに応じて深さ指定を変化させるというようなことはできない。基本的には、自分で構築したPSCustomObjectを使うか、JSON化する前に自分で元オブジェクトを整形しておく必要がある。
  • 直接ファイルに書き出し、追記、ファイルから読み込みするコマンドレットはない。
  • 実は細かい話をしだすと色々と罠があります…。
CLIXML

PowerShellではPSリモーティング等、プロセス間でオブジェクトのやり取りを行う際に、CLIXML形式を介してシリアライズ/デシリアライズが実行されます。シリアライズ対象によっては、完全に元のクラスのオブジェクトに復元されます。(復元されないオブジェクトにはクラス名にDeserialized.との接頭辞が付与され、プロパティ値のみ復元される)

ユーザーもコマンドレットを用いて、任意のデータをCLIXML形式でシリアライズし、XMLファイルとして保存することができます。

書き出し
$objects | Export-Clixml -LiteralPath file.xml
読み込み
$objects = Import-Clixml -LiteralPath file.xml
メリット
  • 元のオブジェクトの構造、プロパティ値と型情報を含めてほぼ完全にテキストファイルに保存できる。
  • 復元したオブジェクトはプロパティ値を参照できるのはもちろん、オブジェクト全体が完全にデシリアライズされ、元の型に戻った場合には、メソッドを実行することも可能。
  • 例え元の型に戻らず、Deserialized.との接頭辞が付いた状態でも、コンソールに表示する場合は元の型のフォーマットが使われるので見やすい。
デメリット
  • すべてのオブジェクトが元の型に戻せるわけではない。戻せるかどうかは確認が必要。
  • 人間が読み書きするようなものではない。

ちなみに、ConvertTo-Xmlという似たようなコマンドレットがありますが、出力形式はCLIXMLではない上、復元の手段もなく、かといって別に読みやすいXMLというわけでもなく、正直何のために使うのかよく分かりません(適切なxsltでも用意すればいいのかな?)。まだConvertTo-Htmlの方が使えそうです。

psd1

psd1は「PowerShellデータファイル」で、モジュールマニフェストやローカライズデータに使われるファイル形式です。スクリプトファイルの1種ですが、数値や文字列リテラル、配列、連想配列、コメントなど基本的な言語要素のみ使用可能です。PowerShell 5.0以降ではImport-PowerShellDataFileコマンドレットを用いて、任意のpsd1ファイルのデータを読み込み、変数に格納することが可能です。

書き出し

書き出し用のコマンドレットはありません。

読み込み

例えば以下のような内容をbackup_setting.psd1として保存しておきます。ルート要素は必ず連想配列にします。

@{
	Directories = @(
		@{
			From = "C:\test1"       # コピー元
			To = "D:\backup\test1"  # コピー先
			Exclude = @("*.exe", "*.dll")
			Recurse = $true
		},
		@{
			From = "C:\test2"
			To = "D:\backup\test2"
			Exclude = @("*.exe")
			LimitSize = 50MB
		},
		@{
			From = "C:\test3"
			To = "D:\backup\test4"
		}
	)
	Start = "0:00"
}

なお、dataセクションで全体を括ってもいいですが、psd1で許容される言語要素はdataセクションより更に制限がきついので、敢えてしなくてもいいんじゃないかと思います。

このファイルは以下のように読み込めます。

$setting = Import-PowerShellDataFile -LiteralPath backup_setting.psd1

$settingには連想配列が格納され、以下のように値が参照できます。

$setting.Directories | foreach {Copy-Item -Path $_.From -Destination $_.To}
メリット
  • PowerShellの構文でデータを記述できる。
  • 通常のps1ファイルを呼び出すのとは異なり、式の評価やコマンド実行などはされない分、セキュアである。
  • 配列と連想配列の組み合わせにより、JSONライクな階層構造を持てる。型情報も保持される。
  • JSONとは違い、コメントが入れられる。
デメリット
まとめ

PowerShellで扱うデータをテキストファイルとして保存する際には、各テキストデータ形式の特性を理解し、メリット、デメリットを踏まえて選定する必要があります。

また、当然ながらテキストファイルに保持することが不適切なデータもありますので、そこは注意してください。(画像データを敢えてBase64とかでエンコードしてテキストファイル化する意味があるのか、とかですね)

個人的には…

ちょっとした作業ログ等を記録しておきたい→プレーンテキスト

.NETオブジェクトの一部のプロパティだけ抜き出してファイル化したい→CSV

自分で構築したPSCustomObjectをファイル化したい→JSON

.NETオブジェクト全体をファイル化したい→CLIXML

スクリプトで使う設定データを用意したい→psd1

みたいな感じでなんとなく使い分けていると思います。psd1はまだ採用例はないですが…。

今回はビルトインのコマンドレットで扱えるもののみ取り上げましたが、他にもyaml等のテキストデータ形式が存在し、有志によるモジュールを用いて扱うことが可能です。

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

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



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

Twitter

Books