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等のテキストデータ形式が存在し、有志によるモジュールを用いて扱うことが可能です。

2011/10/09

JScriptは言語単体ではSafeArrayを作ることができません。

そこでJScriptでSafeArrayが必要な場合、VBScriptを併用しVBScriptの配列(これはSafeArrayです)をJScriptに取り込む方法や、Scripting.DictionaryのItems()メソッドを使う方法などが使われているようです。

しかしこれらの方法で多次元のSafeArrayを作るサンプルをあまり見かけませんでした。Dictionaryの方法ではそもそも1次元しか無理ですしね。そんな中、この記事を発見しました→JScriptの配列とVBScriptの配列(SafeArray)を相互変換する方法(2次元編) - プログラマとSEのあいだ
この記事の二つ目の例ではExcelを使用しRegionオブジェクトが二次元配列を返す点を利用しています。これはなかなか盲点というかアイデアものではありますが、実行速度にやや難があるかな?と思いました。

注: ただしこの記事の方法は、もともとExcelで二次元配列が必要な場合があったから考案されたもののようで、その用途においてはExcelを起動するコストは考慮しなくてよいのかもしれません。

一つ目の方法ではVBScriptを併用していますが、.wsfファイルを使用してJScriptとVBScriptを混在させる形式をとっています。この方法はWSHでは問題ありませんが、複数のスクリプトエンジンを混在できないホスト環境では問題があります。

注:そんな環境ってあるのか?と聞かれそうですが、たしかにHTML/HTA/Windowsデスクトップ(サイドバー)ガジェット/WSH/classic ASPなどほとんどの環境では大丈夫そうです。ただ私が最近はまっているJScript実行環境であるところのAzureaでは無理ですね。WSHでも.jsファイルにこだわるのであれば。

そこで考えたのが、ScriptControlを使用してJScriptのコードの中でVBScriptのコードを実行させる方法です。以下のような感じになります。

function array2dToSafeArray2d(jsArray2d)
{
	var sc = new ActiveXObject("ScriptControl");
	sc.Language = "VBScript";
	var code =
'Function ConvertArray(jsArray)\n' +
'	ReDim arr(jsArray.length - 1, jsArray.[0].length - 1)\n' +
'	outerCount = 0\n' +
'	For Each outer In jsArray\n' +
'		innerCount = 0\n' +
'		For Each inner In outer\n' +
'			arr(outerCount, innerCount) = inner\n' +
'			innerCount = innerCount + 1\n' +
'		Next\n' +
'		outerCount = outerCount + 1\n' +
'	Next\n' +
'	ConvertArray = arr\n' +
'End Function\n';
	sc.AddCode(code);
	return sc.Run("ConvertArray",jsArray2d);
}

まあやっていることは本当にJScriptの配列をバラしてVBScriptの二次元配列に詰め直しているだけです。

ただいくつかポイントがあって、まずVBScriptからはJScriptのオブジェクトメンバーにドット演算子でアクセスができます。JScriptの配列はオブジェクトと同一であり、配列はオブジェクトに0,1,2...という名前のプロパティが存在することになります。しかしVBScriptで数字のメンバ名はそのままではドット演算子でアクセスできないので、[]でくくる必要があります(これ、予約語なんかもそうですね。あとVB6でもVB.NETでも同じなので覚えておくといいかも)。なので次元数2の配列の長さを調べるのにjsArray.[0]でまず内側の配列オブジェクトを取得しているわけです。

さらにポイントとして、VBScriptでJScriptの配列を含むオブジェクトメンバを列挙するのにコード例のようにFor Each Next構文が使えます。ただしFor Nextを使ってインデックスアクセスはできません。というのもjsArray.[3]とかはあくまでjsArrayオブジェクトの3プロパティの値を参照しているにすぎず、jsArray.[I]という書き方ができないからです(これだと単にIプロパティの値を見てることになる)。Eval関数を併用すれば可能ではありますが、コードの中にコードを含ませさらにその中にまたコードを含ませるのも微妙なのでここでは使ってません。

あとは紹介した記事の関数部分だけ置き換えればJScriptのVBArrayオブジェクトを用いたテストもできるかと思います。注意点はExcelオブジェクトは配列添え字が1から始まるのに対し、VBScriptの配列は0から始まる点です。LBound関数を使えばその差違は吸収できるかな、と思います。

最初は多次元配列というかn次元配列に拡張した関数を書いてやろうと企んでましたが挫折しました。ネストしたループではなく再帰呼び出しである必要がありますし、ReDimは次元数を動的に指定することができないので実行するVBScript自体を動的生成しなければいけません。興味がある方はチャレンジしてみてください。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2011/10/09/204198.aspx

Copyright © 2005-2016 Daisuke Mutaguchi All rights reserved

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

Awards

Books

Twitter