2017/12/24

この記事には「 独自研究 」に基づいた記述が含まれているおそれがあります。

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

一般的なプログラミング言語では、文(statement)と式(expression)の違いは、値を返すのが式で、返さないのが文、という説明がされることが多いと思います。しかし、PowerShellではこの説明は成り立たたず、文が値を返したりしてるように見えて良く分かりません。そこでPowerShellにおける文と式とはそもそも何なのかということを、仕様書(PowerShell 3.0のものですが)やAST(ShowPSAstモジュールが便利!)を眺めながら考えてみたので軽くまとめようと思います。

文(statement)と式(expression)の定義

PowerShellでは言語要素として、文(statement)と式(expression)が明確に定義されています。すなわち、言語要素の何が文であって、何が式であるかという定義は仕様できちんと決まっていて、ある言語要素が、状況によって文になったり式になったりと変化する、ということはありません。

パイプライン、代入、ifやforやfunctionなどは文です。

変数、数値/文字リテラル、オブジェクトのメンバ呼び出し、スクリプトブロック、単項または二項算術演算子で構成される式、カンマ演算子で構成される配列などは式です。

ただ、仕様書での定義と、実際に構築されるASTに齟齬があることはあります。

例えば「$a=1」のような代入については、ASTではAssignmentStatementAstとなります。一方、言語仕様上はassignment-expressionと書かれています。厳密には、言語仕様書のgrammer節によれば、assignment-expressionはexpressionではなくpipelineであるということになっています(お前は何を言っているんだ)。いずれにせよパイプラインは文であるので、AST通り、代入は文であるという解釈で良いと思います。

※仕様書にはassignment expressionとはっきり書いてあるんだから代入は式だろ!という意見を否定するものではないです。が、代入は文であると考えたほうが他の文法と整合性を取りやすいので、そういう立場をとりました。

しばたさんが9日目に書かれた記事で取り上げられているように、配列に関しても同様の齟齬があります。いずれにせよ「1,2,3」のような配列は、式と考えて良いと思います。

文と式の構造

PowerShellには文と式が存在することは分かりました。では文と式は何が違うのか? それを考察するために、具体的にいくつかの文と式の構造を取り上げて見ていきます。

パイプライン(文)

パイプラインといえば「Get-Process | Where-Object Handles -gt 100 | Select-Object ProcessName」みたいな|で繋ぐやつのことでしょ、と思われがちですが、言語仕様上は以下のようなものはすべてパイプラインです。

Get-Process -Name PowerShell # @コマンド1つだけ
1 # A数値リテラルだけ
1 + 1  # B算術演算子で構成される算術式
$a = 1 # C代入
gps | where Handles -gt 100 | select ProcessName # Dパイプ記号でコマンドを連結したもの
1 + 1 | Write-Host # E算術式をパイプラインでコマンドと繋げたもの

要はパイプラインというのは、一般的なプログラミング言語で、;で終わる一文に相当するものと考えればだいたい間違いないと思います。ただしPowerShellだと文末の;は必須ではなく、改行でもOKです。

パイプラインは以下のような構造を取ります。[]は省略可能を意味します。

パイプライン要素1 [ | パイプライン要素2] [ | パイプライン要素3] ...

または、

代入文

すなわち、代入文(後述)を除くパイプラインは1個以上のパイプライン要素から構成されており、複数のパイプライン要素が存在する場合はパイプ演算子|で連結されます。

パイプライン要素には式とコマンド(Get-Processとか)が存在します。ただし式は1つ目のパイプライン要素にのみ許可されます。

以上を踏まえると、@、Dはコマンドのみで構成されるパイプライン、A、Bは単独の式のみで構成されるパイプライン、Eは1つの式とコマンドで構成されるパイプライン、Cは代入文であることが分かります。

代入文

代入文はパイプラインなので、文です。代入文は以下のような構造を取ります。(ここでは+=などの複合代入演算子については省略)

式 = 文

ただし、左辺の式は、変数やプロパティなど、代入が可能な式である必要があります。

よって以下のような記述が可能です。

$a = $b # @変数
$a = 1 + 1 # A算術式
$a = Get-Process # Bパイプライン(コマンド1つ)
$a = gps | where Handles -gt 100 # Cパイプライン(コマンド複数)
$a = if($true){"a"}else{"b"} # Dif文
$a = $b = 1 # E代入文の結果を更に代入

言語仕様上、代入文の右辺には文であれば何でも書けるのですが、実際に代入が行われるのは、パイプライン、if文、for文、switch文といった、パイプラインに値を出力する文と代入文に限られます。ちなみにDのようにパイプラインと代入文以外の文を右辺に指定できるようになったのは、PowerShell2.0からです。

ところで上記@やAは右辺が式です。代入文の右辺は文じゃなかったの?と思われると思いますが、パイプラインの節で述べた通り、単独の式もパイプラインであり、パイプラインは文なので、三段論法でいくと式は文として扱われることになります。

※AST上では$a = $bの右辺はPipelineAst/CommandExpressionAst/VariableExpressionAstではなく、いきなりCommandExpressionAst/VariableExpressionAstとなっているので、この説明はASTの実装とはかみ合わないかもしれません。AssignmentStatementAst.Rightは確かにStatementAstを取るのですが、CommandExpressionAstはStatementAstから派生しているクラスなので、式の代入は問題なく行えます。

代入文は上記Eのようなことができることから分かる通り、値を返す文ですが、パイプラインには値を出力しません。値は返すがパイプライン出力がないものは、インクリメント演算子で構成される式($a++等)も同様です。

if文

パイプラインと代入文以外の文は色々あるわけですが、代表的なものとしてif文を取り上げます。一番シンプルなif文はこういう構造です。

if (パイプライン) {
    文1
    文2
....}

おそらく多くの人が誤解しているのではないかと思いますが、条件節に書くのは式ではなくパイプラインです。パイプラインを実行した結果、出力値がtrue、またはboolに型変換してtrueになる場合に、ブロック内の複数の文が実行されます。

よって以下のような記述が可能です。

if ($true) {} # @変数
if ($a -eq 1) {} # A論理演算子で構成された式
if ("a.txt" | Test-Path) {} # Bパイプライン
if ($a = 1) {} # C代入文

@とAは普通の書き方ですが、実際には、1つの式のみ有するパイプラインを実行し、出力される値が判定されています。

条件節はパイプラインなので、当然Bのような書き方もできるわけです。また、代入文もパイプラインであるので、Cの書き方もできてしまい、注意を要します。

条件節に指定できるのはパイプラインだけで他の文は許容されないので、
if (if($true){}){}
というような書き方はできません。

※といっても実はこう書くと文法上はvalidであり、条件節内は「ifコマンド、パラメータ値1($true)、パラメータ値2(スクリプトブロック)」という解釈になってしまいます。

また、条件節にはパイプラインは1つのみ指定可能で、複数文を書くことはできないので、
if ($a;$b) {}
という書き方はできません。(パーサーもエラーを出す)

丸括弧式

さて、普通のプログラミング言語だと、()はグループ化や演算子の優先順を変更するのに用いられるものの、別に文法そのものに影響を与えるものではないと思います。多くの場合、ASTでも()の情報はそぎ落とされます。

ところがPowerShellでの丸括弧()は文法的な意味を有しており、ASTにもParenExpressionAstとして存在する、立派な式です。丸括弧式の構造は以下の通りです。

(パイプライン)

これは要するに、「パイプラインに()を付けると式になる」、ということです。()内のパイプラインで出力された値が返される式となります。具体的にどういうところで使うのかを示します。

2 * (1 + 3) # @数値演算の優先順を変更する
($a = 1) # A値を返すがパイプラインには出力しない代入文の値を出力させる
$a[(Get-Hoge)] # B式は許容するがパイプラインは許容しない構文で、式に変換する
Get-Process -Name (Get-Hoge) # Cコマンドのパラメータにコマンド実行結果を指定する

@の使い方は普通です。ただし、()はパイプラインを生成するので、「(1+3)」は「1つの式のみ有するパイプラインを実行し、パイプラインに値を出力し、その値を返す」という見た目より複雑な処理になります。

※少なくともAST上はそうなりますが、実際は何らかの最適化処理が入ってる可能性はあります。

代入を重ねる場合にはAのような書き方は必要ないのですが、代入した結果をパイプラインの出力としたい場合は()を付ける必要があります。この場合、$aに1が代入され、コンソールにも1が出力されます。

Bで挙げている、式を許容するがパイプラインは許容しない言語要素というのは実はあまりないです。前述した通りif文の条件節は式じゃなくて、パイプラインを取るといった案配です。ただ、たとえば配列や連想配列の要素を取得するインデックス演算子[]は、式のみ許容されます。なのでコマンドなどのパイプラインの出力値を指定したい場合は()が必須となるわけです。

ちなみに、()内にはパイプライン以外の文(if文等)は指定できません。また、複数のパイプラインも指定できず、あくまで1つだけです。

※任意の文あるいは複数の文を式としたい場合には、部分式演算子$()または@()を用います。両者とも内部の文がパイプラインに出力した値を返す、「部分式」となります。両者とも複数値が出力されると配列になりますが、@()は出力値が1つでも要素数1の配列を返す点が異なります。

まとめ?

PowerShellの文と式は厳密に定義されています。文は複数の文と式で構成されるし、式は複数の文と式で構成されています。文や式の構成要素が取る文や式の種類についても、各々、きちんと定義されています。

ただし、PowerShellにおいて「値を返すか返さないか」、「パイプラインに出力されるかされないか」、「式であるか文であるか」という概念はすべて独立しています。そのため、PowerShellの文とは何である、式とは何である、ということを一言で説明することは難しいんじゃないかと思います。

なので、本記事でこれまで述べてきたとおり、「パイプラインは文で、要素として式やコマンドを取りますよ」とか、「ifは文で、条件節にはパイプラインを取りますよ」みたいな、各論でしか表現できないのではないかなぁと、私はそういう結論に至りました。

しかしここまで書いてちゃぶ台をひっくり返すようなことを言いますが、ある言語要素が文であるか式であるか、ここまで仕様書を読んだりASTを追ったりして把握するのは、まあ楽しくなくはないですが、知らなくても別に大丈夫だと思われます。別に、ifの条件節には条件式を書くのだと理解していても不都合は特にないかと。

効用があるとすれば、例えば「if ((Test-Path a.txt)) {}」とか「foreach ($i in (1..5)) {}」とかの、余分な()を取り除くのには文法の知識が役立ちます。それもまぁ、心配だから怪しい所には常に()付けておく or 何か変だったら()付けてみる とかでもそれ程問題にはならないかもしれません。

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

2016/12/20

この記事はPowerShell Advent Calendar 2016の20日目です。

はじめに

前々回はASTの概要について述べ、最後にAST.FindAllメソッドを使って、ASTから指定のASTノードを検索する方法について説明しました。

前回はASTを再帰的に検索して、木構造を視覚化してみました。

今回もASTを検索する話なのですが、静的解析機能を実装するためのAST Visitorを用いる方法について説明します。が、あらかじめお断りしておきますが、静的解析の実装までは今回はたどり着きません。静的解析ツールをどう作るかorどう作られているか、ということを雰囲気で味わっていただければと。

Visitorパターン

AST Visitorの説明をする前に、まず、Visitorパターンについて簡単に。

Visitorパターン[Wikipedia]というのは、オブジェクト指向言語におけるデザインパターンの1つで、対象オブジェクトを巡回する「訪問者」クラスを定義するものです。Visitorクラスでは、対象クラスごとに行う処理を、個別にvisitメソッドをオーバーロードさせることで定義します。共通のVisitor抽象クラスを継承することで、異なる機能を持ったVisitorクラスを作ることができます。

一方、処理対象クラスには、Visitorオブジェクトを引数に受け取る、acceptメソッドを定義します。acceptメソッドでは、引数として受け取ったVisitorオブジェクトのvisitメソッドを呼ぶことで、処理を実行させます。

なお、処理対象クラスが子要素クラスを持つ場合には、acceptメソッド内で、子要素クラスのacceptメソッドを呼ぶようにします。こうしておくことで、Visitorは処理対象を再帰的に巡回できるようになります。

このように処理対象クラスから、実際に処理を行う機能をVisitorクラスとして分離することで、処理対象クラスに手を加えることなく、Visitorクラスを追加して、処理内容を増やしたりすることが可能になります。

AST Visitorの呼び出し

Visitorパターンを念頭において、AST Visitorの呼び出し方を見ていきましょう。Ast抽象クラスには、以下の2つのVisitメソッドが定義されています。

説明に入る前に注意点。メソッド名は"Visit"となっていますが、Visitorパターンでいうところの"accept"メソッドのことです。なぜメソッド名がAcceptじゃないのかは不明ですが…。

ともかく、AstクラスのVisitメソッドは、AstVisitor抽象クラスを継承したクラスのオブジェクトか、ICustomAstVisitorインターフェースを実装したクラスのいずれかを引数に取ることで、ASTに対する処理を実施します。

AstVisitor抽象クラスを継承、もしくはICustomAstVisitorインターフェースを実装することで、ASTの種類に応じた巡回処理を行うクラスを、自分で定義していきます。

AstVisitor抽象クラス

AstVisitor抽象クラスは、Visitorとしての基本的な機能があらかじめ実装されています。具体的には既に以下の機能は用意されています。

  • ASTの種類に応じたVisitメソッドの定義
    すべての種類のASTに対応するVisitメソッド(50個以上)がVirtualメソッドとして定義されています(※)。例えば、IfStatementAstに対する処理を行うための、VisitIfStatementメソッドがあります。

    ※一般的なVisitorパターンでは、Visitメソッドを対象クラス分オーバーロードさせますが、PowerShellのAstVisitorは対象クラスに応じた別名のメソッドを定義する方式です。これも理由は分かりませんが、オーバーロードにするには多すぎるからかもしれません。

  • 子ノードの再帰的な巡回
    各Visitメソッドには、ASTの子ノードに対し、再帰的にVisitメソッドを呼ぶ仕掛けがあらかじめ備わっています。
  • ノード巡回の停止
    各VisitメソッドはAstVisitAction列挙型を返却します。以下のように返却する値によって、ノード巡回の継続、停止を制御できます。
    • Continue:ノード巡回を継続(デフォルト)
    • SkipChildren:子ノードの巡回を行わない
    • StopVisit:巡回を終了する
カスタムAstVisitorクラスを作成する

以上の基本的な機能を踏まえて、AstVisitor抽象クラスを実装したカスタムVisitorクラスを作ります。C#で書くのが一般的ですが、せっかくなのでPowerShell v5で追加された、クラス構文を使って書いてみましょう。

例えば、「利用しているコマンドのリストを取得する。ただし、コマンドのパラメータ内で別コマンドを呼び出している場合は除く。」というお題を解くことを考えます。

ASTのFindAllメソッドだと、配下に含まれるすべてのCommandAstを取得してしまうので、単純にはいきません。そこでカスタムAstVisitorクラスの出番です。

このお題を実現するVisitorクラスは以下のようになるでしょう。

using namespace System.Management.Automation.Language

class GetCommandNamesVisitor : AstVisitor
{
    [string[]]$CommandNames = @()

    [AstVisitAction]VisitCommand([CommandAst]$commandAst)
    {
        $this.CommandNames += $commandAst.CommandElements[0].Extent.Text
        return [AstVisitAction]::SkipChildren
    }
}

PowerShellのクラス構文において、Virtualメソッドのオーバーライドは、単に同名のメソッドを定義するだけですので、ここではVisitCommandメソッドをオーバーライドします。

プロパティとフィールドの区別はないので、コマンド名の一覧を格納するCommandNamesプロパティは上記のような定義になります。メソッド内でクラスメンバを参照する際には$thisを用います。

作成したGetCommandNamesVisitorクラスをインスタンス化し、解析対象スクリプトブロックのASTのVisitメソッドに引数として渡します。

$scriptBlock = {
    $files = Get-ChildItem -Path (Get-Location | Split-Path -Parent) -File
    $files | 
        Sort-Object -Property LastWriteTime -Descending | 
        Select-Object -First 5
}

$visitor = New-Object GetCommandNamesVisitor
$scriptBlock.Ast.Visit($visitor)
$visitor.CommandNames

実行すると、結果は

Get-ChildItem
Sort-Object
Select-Object

のようになるかと思います。

AstVisitorクラスの具体的な実装については、PSReadLinePowerShellEditorServicesにありますので、参考にしてみてください。

ICustomAstVisitorの実装

前項で述べた、AstVisitor抽象クラスを継承したカスタムAstVisitorクラスの場合、基本的な処理を実装する必要はないですし、目的とするASTクラスに対するVisitメソッドだけオーバーライドすればいいので、非常に簡便です。

ただ、本格的にPowerShellの構文解析を行いたい場合、ノードの巡回順だとか、その他もろもろをもっと細かく自分で実装したいケースが出てきます。

そういった場合にはICustomAstVisitorインターフェースを実装したクラスを作って対応します。ICustomAstVisitorインターフェースも、AstVisitor抽象クラス同様、各ASTクラスに応じたVisitメソッドが定義されているのですが、各VisitメソッドはAstVisitAction列挙体ではなく、object型のオブジェクトを返します。つまり、自分で好きなオブジェクトを返すように定義できるわけです。

Ast.Visit(ICustomAstVisitor)はAstVisitor抽象クラスを引数に取る場合と異なり、objectを返却するのですが、このとき返却されるのは、最初に実行されたVisitメソッドの戻り値になります。

ICustomAstVisitorはインターフェースですので、処理はすべて自分で定義しなくてはなりません(※)。ノードの再帰的探索も、必要ならもちろん自前で実装する必要があります(前回紹介した、JSON化スクリプトのような処理になるかと思います)。

※ISEだとインターフェースの実装を一発で行うリファクタリング機能はないので、今回みたく実装すべきメンバがたくさんある場合は、こんな感じのひな形を作るスクリプトを使うと良いでしょう。

今回はICustomAstVisitorインターフェースを実装したクラスの実例まではご紹介できませんでしたが、興味のある方は、PSScriptAnalyzerで用いられているので参考にしてみてください。

まとめ

PowerShellのASTについてきちんと解説している記事が英語圏を含めてもあまりないようでしたので、3回に渡って、一通りの基礎知識をまとめてみました。

普通にPowerShellを使っている分には、滅多に使うことはないと思いますが、たとえばPSScriptAnalyzerカスタムルールを自分で作る場合には、ASTの知識は必須になってきますので、必要に応じて参考にしていただければ幸いです。

2015/12/10

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

はじめに

前編では、Invoke-WebRequestコマンドレットやWebClientクラスを用いて、WebページからHTMLの文字列を取得するところまで説明しました。

後編の今回は、取得したHTML文字列をパースして、オブジェクトとして利用可能しやすい形に変換する話です。

IEエンジンによるHTMLパース(DOM)

前編でも触れましたが、Invoke-WebRequestコマンドレットは、レスポンス文字列を取得すると同時に、HTMLをパース(構文解析)し、結果をオブジェクトとして構造化してくれます。

実はこのHTMLパース、内部的にInternet Explorerのエンジンを呼び出すことで実現されています。(ちなみに後で説明しますが、-UseBasicParsingパラメータを付与すると、IEエンジンを使わずごく基本的なパースのみ行うようになります。)

Invoke-WebRequestコマンドレットの出力であるHtmlWebResponseObjectオブジェクトのParsedHtmlプロパティを経由することで、HTMLパースされたオブジェクトを、DOM(Document Object Model)に従ってアクセスすることができます。(-UseBasicParsing指定時は不可)

HTMLのtable要素を切り出し、table各行を1オブジェクト、各セルをプロパティとして、オブジェクト配列化する例を以下に示します。

$response = Invoke-WebRequest http://winscript.jp/powershell/301

# DOMを利用して1つ目のtable要素を取得
$table = $response.ParsedHtml.getElementsByTagName("table")| select -First 1

# tableの1行目をプロパティ名として取得
$properties = ($table.rows| select -first 1).Cells| foreach {$_.innerText}

# tableの残りの行に対して、各セルのinnerTextをプロパティ値としてオブジェクト化
$objs = foreach($row in ($table.rows| select -skip 1))
{
    $row.Cells| foreach -Begin {
        $index = 0
        $obj = [ordered]@{}
    } -Process {
        $obj += @{$properties[$index] = $_.innerText}
        $index++
    } -End {
        [pscustomobject]$obj
    }
}

$objs| Format-List

ところで前編で軽く触れましたが、IEエンジンによるパースは、Invoke-WebRequestコマンドレットを用いずとも、以下のようにして直接IEのCOMインターフェースを呼ぶことで利用可能です。

$client = New-Object System.Net.WebClient
$content = $client.DownloadString("http://winscript.jp/powershell/301")
$parsedHtml = New-Object -com "HTMLFILE"
$parsedHtml.IHTMLDocument2_write($content)
$parsedHtml.Close()
$table = $parsedHtml.getElementsByTagName("table")| select -First 1
# 以下同様…

というより実際に試すと直接IEエンジンを呼び出す方がずっと速いです。理由はよく分かりませんが…。

HTML要素コレクションの取得

Invoke-WebRequestコマンドレットを用いると、DOMとは別に、すべての要素(AllElementsプロパティ)、input要素(InputFieldsプロパティ)、img要素(Imagesプロパティ)、a要素(Linksプロパティ)、script要素(Scriptsプロパティ)を含むコレクションを、HtmlWebResponseObjectオブジェクトの対応するプロパティからそれぞれ取得することができます。

コレクションに含まれる各要素は、innerText(タグ内の文字列)、innerHTML(タグ内のHTML)、tagName(タグ名)等のプロパティが共通して利用可能です。また要素の属性(たとえばa要素ならリンク先を示すhref属性)に、プロパティとしてアクセス可能となります。

以下はBingでWeb検索した結果から、ページタイトルとURLを抜き出す例です。HtmlWebResponseObjectのLinksプロパティでa要素の配列を取ってきて、次に検索結果では無いっぽいURLを、hrefプロパティの値を見てwhereで除外し、最後にinnerTextプロパティとhrefプロパティをTitle、Urlとリネームしてから値を出力しています。泥臭い処理が混じってますが、この泥臭さがスクレイピングなのかもなぁと思います。

$searchWord = "PowerShell 配列"
$notSearchResults = "/","#","javascript:","http://go.microsoft.com/"
$response = Invoke-WebRequest "https://www.bing.com/search?q=$([Uri]::EscapeDataString($searchWord))"
$response.Links | 
    where {
        $href = $_.href
        !($notSearchResults|? {$href.StartsWith($_)})
    }|
    select @{L = "Title"; E = "innerText"}, @{L = "Url"; E = "href"}|
    Format-List

form要素についてもほぼ同様にFormsプロパティからコレクションを取得できますが、このコレクションにはFormObjectという特別なオブジェクトが含まれます。FormObjectのFieldsプロパティは、Key=パラメータ名、Value=パラメータ値が格納された連想配列となっています。この連想配列は書き替えが可能なので、前編で説明した、ログオンを要するWebサイト等で用いると便利かと思います。

以下に、HtmlWebResponseObjectオブジェクトのプロパティをまとめます。(×印は使用不可を表す)

プロパティ名 説明 -UseBasicParsing
指定時
AllElements 本文に含まれるすべての要素のコレクション ×
Forms フォーム(form要素)のコレクション ×
InputFields 入力フィールド(input要素)のコレクション  
Images 画像(img要素)のコレクション  
Links リンク(a要素)のコレクション  
Scripts スクリプト(script要素)のコレクション ×

このように、一部のプロパティについては-UseBasicParsing指定時でも利用可能です。サーバーOS等でIEエンジンが利用できない場合には-UseBasicParsingパラメータが必須となりますが、その場合でも最低限のパースはしてくれるわけです。

HTML要素のコレクションを利用する方法は、DOMを使う方法に比べると自由度は少ないですが、「ページから画像のリストを取得したい」等の処理は簡便に行うことができます。

その他のHTMLパース手法

最後に、Invoke-WebRequestコマンドレットとIEエンジン以外のHTMLパース手法について軽くご紹介します。

XMLとしてパース(XHTML限定)

XHTMLというのはごくかいつまんで言うと、HTMLをXMLで定義したものです。XHTMLはXMLなので、XMLとしてパースして用いることができます。

PowerShellは[xml](XmlDocument)型アクセラレータと型アダプタにより、XML要素への簡便なアクセス手段を提供しています。以下のように、[xml]型アクセラレータを用い、取得したXHTML文字列を[xml]型に変換すると、以降は型アダプタの機能により、ドット演算子で要素を辿っていくことができます。

$client = New-Object System.Net.WebClient
$content = $client.DownloadString("XHTMLなページ")
$xml = [xml]$content
$xml.html.body.h2.'#text'

ただ世の中のWebページ上のXHTML文書が、すべてXML文書としてvalidなものであるかと言われると、現実はかなり厳しいです。そしてXML文書としてエラーがある場合は、型アクセラレータの処理は容赦なく失敗します。なのでこの手法は「使えたら強いが、大抵使えない」レベルのものと思って頂ければいいと思います。

SgmlReader

標準機能にこだわらなければ、.NET製のHTMLパーサーを使うのが楽かと思います。SgmlReaderは通常のHTML文書(当然、XHTMLに限らず)をXmlDocumentへとパースしてくれるので、PowerShellと相性が良いのではないかと思います。

以下にサンプルを載せておきます。

Add-Type -Path .\SgmlReaderDll.dll

function Get-HTMLDocument
{
    param([uri]$Uri)
    $sgmlReader = New-Object Sgml.SgmlReader -Property @{
        Href = $Uri.AbsoluteUri
        CaseFolding = [Sgml.CaseFolding]::ToLower
    }
    $doc = New-Object System.Xml.XmlDocument
    $doc.Load($sgmlReader)
    $doc
}

$xml = Get-HTMLDocument http://winscript.jp/
$xml.html.body.div|? id -eq outer|% div|? id -eq main|% {$_.p.innerText}

ぎたぱそ氏も以前SgmlReaderを取り上げておられるので、そちらも参考にして下さい。:Html Agility Pack と SgmlReader を使って PowerShell でスクレイピングしてみる - tech.guitarrapc.cóm

正規表現等で自前パース

これまではHTMLパースを既存のコマンドやライブラリを用いて行ってきましたが、対象のHTMLが非常にシンプルである場合とか、HTMLですらなく単なるテキストの場合だとか、対象ページは分量が多いものの必要箇所はごくわずかで、かつピンポイントに取得可能な場合等々は、むしろ自前でパースするコードを書いた方が手っ取り早いこともあります。

例えばYAMAHAのルーターで、管理Webのシステム情報レポートからグローバルIPアドレスを取ってくる、みたいなことは、

$response = Invoke-WebRequest http://サーバー/detail/status.html -UseBasicParsing -Credential $credential
if($response.Content -match "PP IP Address Local\: (.+)\,")
{
    $ipAddress = $Matches[1]
}

のようなコードで十分かと思います。

ConvertFrom-String

これはまだ検証してないんですが、PowerShell5.0の新機能、Auto-Generated Example-Driven Parsingの実装であるConvertFrom-Stringコマンドレットを用いて、HTMLパースができないかな、と考えています。

ConvertFrom-Stringについては過去記事参照:[v5] Auto-Generated Example-Driven Parsing について - PowerShell Scripting Weblog

まとめ

前後編に渡って、PowerShellでのWebスクレイピングの手法について解説しました。スクレイピングはWeb APIが用意されていない場合の苦肉の策ですが、背に腹は代えられない場合というのは稀によくあると思います。そういうときに今回の記事が参考になれば幸いです。

次回あたりには、Web APIがちゃんと用意されてる場合に、PowerShellから利用する話をやろうかと思います。

2015/11/09

前編(前々回)中編(前回)の続きです。

分かち書きとは

中編で作ったGet-JpYomi関数は、JapanesePhoneticAnalyzerクラスの読み仮名取得機能にフォーカスを当てたラッパー関数でした。

今回は、JapanesePhoneticAnalyzerクラスの最大の使用目的と思われる、「分かち書き」を目的とした関数を作成します。分かち書きとは、文章を文節単位で分割することと考えて頂いて良いかと思います。

前々回作ったGet-JpWordは単語単位の分割を行うものでしたが、読みやすさや発音のしやすさを目的として文章を分割表記する場合は、単語単位では細かすぎると言えます。

よって単語単位ではなく、文を意味のあるまとまりとして区切ることのできる最小の単位である、文節単位で分割する方法を考えてみます。

Split-JpText関数

前編で作ったGet-JpWord関数をラップし、分かち書きに特化した関数Split-JpTextを作成しました。まずは以下にコードを示します。

function Split-JpText
{
    param(
        [parameter(ValueFromPipeline=$true)]
        [PSObject[]]
        $InputObject,

        [ValidateSet("Text", "Yomi", "Detail")]
        [string]
        $Format = "Text",

        [ValidateSet("ByWord", "ByPhrase")]
        [string]
        $SplitMode = "ByPhrase",

        [string]
        $Separator = " ",

        [switch]
        $ToArray
    )

    begin
    {
        if($Format -eq "Detail"){$ToArray = $true}
    }

    process
    {
        foreach($o in $InputObject)
        {
            $o.ToString() | Get-JpWord | 
            foreach -Begin {
                $phrases = @()
                $phrase = $null
            } -Process {
                if($_.IsPhraseStart)
                {
                    if($phrase){$phrases += $phrase}
                    $phrase = New-Object psobject |
                        Add-Member -MemberType ScriptProperty -Name Text -Value {
                            -join $this.Words.DisplayText} -PassThru |
                        Add-Member -MemberType ScriptProperty -Name Yomi -Value {
                            -join $this.Words.YomiText} -PassThru |
                        Add-Member -MemberType NoteProperty -Name Words -Value @() -PassThru
                }
                $phrase.Words += $_
            } -End {
                if($phrase){$phrases += $phrase}

                if($SplitMode -eq "ByPhrase")
                {
                    $out = switch($Format)
                    {
                        "Text"   {$phrases.Text}
                        "Yomi"   {$phrases.Yomi}
                        "Detail" {$phrases}
                    }
                }
                else
                {
                    $out = switch($Format)
                    {
                        "Text"   {$phrases.Words.DisplayText}
                        "Yomi"   {$phrases.Words.YomiText}
                        "Detail" {$phrases.Words}
                    }
                }

                if($ToArray)
                {
                    $out
                }
                else
                {
                    $out -join $Separator
                }
            }
        }
    }
}
パラメータの説明
パラメータ名 説明
InputObject 任意の型 入力テキスト。文字列以外の型の場合は文字列に変換して評価される。パイプライン入力可能。
Format string Text(デフォルト):文字列のみ出力する。
Yomi:文字列ではなく読みを出力する。
Detail:文節の文字列、読み、各文節に含まれる単語の配列を含んだオブジェクトの配列を出力する。(SplitMode=ByPhraseの時のみ)
SplitMode string ByPhrase(デフォルト):文を文節単位で分割する。
ByWord:文を単語単位で分割する。
Separator string 分割文字を指定。デフォルトは" "(半角スペース)。(Format=DetailもしくはToArray指定時には無効)
ToArray switch 指定すると、単一の文字列ではなく、文字列の配列を出力する。
使用法
  • 分かち書き(文節)
    例:Split-JpText "今日はいい天気ですね。"
    出力:今日は いい 天気ですね 。
  • 分割文字指定
    例:Split-JpText "今日はいい天気ですね。" -Separator /
    出力:今日は/いい/天気ですね/。
  • 分かち書き(単語)
    例:Split-JpText "今日はいい天気ですね。" -SplitMode ByWord
    出力:今日 は いい 天気 です ね 。
  • 文節単位で読み仮名を表示
    例: Split-JpText "今日はいい天気ですね。" -Separator / -Format Yomi
    出力:きょうは/いい/てんきですね/。
  • 分かち書きした文節を文字列配列として変数に格納
    例:$phrases = Split-JpText "今日はいい天気ですね。" -ToArray
解説

ちょっと長めの関数ですが、ポイントはJapanesePhoneticAnalyzerクラスのGetWordsメソッドが返すJapanesePhonemeオブジェクトのIsPhraseStartプロパティです。

IsPhraseStartプロパティは、当該単語(Phoneme)が文節(Phrase)の開始部分にあたる単語であればTrueを返します。すなわち、JapanesePhonemeコレクションを文頭から文末まで列挙していったとき、IsPhraseStartプロパティがFalseからTrueに変わる部分が文節の境界になるわけです。

Split-JpText関数では、単語を列挙していき、文頭もしくは文節の境界に遭遇すると、文節に含まれる文字列(Textプロパティ)とその読み(Yomiプロパティ)と単語の配列(Wordsプロパティ)を格納するオブジェクトを新たに作成し、$phrase変数に代入します。一方で$phrase変数に元々入っていたオブジェクトは、$phrases配列に追加します。

$phraseオブジェクトのWordsプロパティには、列挙中の単語を都度、追加していきます。

なお、$phraseオブジェクトのTextプロパティとYomiプロパティはスクリプトプロパティとして定義しておき、必要時に値を取得するようにしてあります。

まとめ

3回に渡って、JapanesePhoneticAnalyzerクラスの使用法を具体的なラッパー関数を作成して紹介しました。

個人的には、PowerShellからなら中編で挙げた、読みの取得が一番使いでがあるかな?と思いました。今回取り上げた分かち書きは、意外と応用例が思いつきませんでした。

前編のGet-JpWord関数を使って、何らかの文書群の単語リストをあらかじめインデックスとして出力しておき、単語検索コマンドを実装するのも面白そうですね。

ただ、残念ながら品詞情報が取れないので、JapanesePhoneticAnalyzerをmecabとかの形態素解析エンジンの代替にするのはちょっと厳しいかもしれないです。まあ、標準機能のみでちょっとしたものを作れるのは大きいかと思います。何か日本語の文章を解析する必要があるときには使ってみてはいかがでしょうか。

2015/09/07

Twitterでこんな問題を出してみました。

以下、解答になります。

@ &{}
結果

何も出力されません。

解説

空のスクリプトブロック{}を実行演算子&で実行しています。空なので何も出力はありません。

A &{process}
結果

「'process' の後にステートメント ブロックがありません。」というパーサーのエラーになります。

解説

PowerShellのスクリプトブロックは、beginブロック、processブロック、endブロックを内包します。スクリプトブロック直下にparamブロック、DynamicParamブロック、beginブロック、processブロック、endブロック(他にもあったかも)以外のステートメントを記述すると、Endブロック内に記述されたものと暗黙的に解釈されます。

この場合、スクリプトブロック直下にprocess…と書き始めたので、パーサーはprocessブロックが開始されたと判断しますが、続くステートメントブロック{}(≠スクリプトブロック)の記述がないため、構文エラーとなります。

B &{process{}}
結果

何も出力されません。

解説

パーサーはAのように解釈しますが、今回はステートメントブロック{}がきちんと記述されているので、エラーなく解釈されます。

processブロックは、パイプライン入力がない場合でも1回実行されますが、この場合、中身は空なので、@と同様、何も出力はありません。

C &{process{process}}
結果

Get-Processコマンドレットが実行され、プロセス一覧が表示されます。

解説

・パーサーの挙動

Bまでの解説の通り、&{process{…}}とすると、…の部分が1回実行されます。今回はprocessブロック内に「process」と記述しているので、Aのようなパーサーエラーは発生せず、「process」がステートメントとして実行されます。

さて、PowerShellのステートメント(文)には「For」とか「If」とかと並列して、「パイプライン」が存在します。「パイプライン」には1つの「式」もしくは複数の「コマンド」が含まれます。

たとえば、「Get-ChildItem | Select-Object Name」というパイプラインには「Get-ChildItem」と「Select-Object Name」という2つのコマンドが含まれます。

(ちなみに、「式」とは「$x+1」とかの、値を返すもののことです。PowerShellではパイプラインの最初の要素にのみ、「コマンド」ではなく「式」を記述することができます。)

今回のお題では、「process」はprocessブロック下に記述されており、ForやIf等のステートメントではないのでパイプラインとして扱われます。このパイプラインには1つの要素のみ含まれていますが、式ではないので、コマンドとして解釈されます。

・コマンド探索の挙動

PowerShellの「コマンド」は、関数、コマンドレット、ワークフロー、Configuration、ファイル(実行ファイル、スクリプトファイルを含む)、&演算子で実行するスクリプトブロック等が挙げられます。

コマンドの探索は、まずコマンドへのエイリアスを探します。ない場合は、関数名orコマンドレット名を探します。それでもない場合は、実行ファイルやスクリプトファイルの拡張子(.exe、.ps1等)を付与してパスの通ったディレクトリを探します(ちなみにカレントディレクトリにあったとしても、相対パスor絶対パス表記でない場合は実行しません)。

さて、ここからが「本当は怖い」ところなんですが、ここまで探索してコマンドがなかった場合、与えられたコマンド名に"Get-"を付与してもう一度探索します。

今回のお題では、processという名前のコマンドを探して、もしパスが通ったフォルダにprocess.exeとかがあればそれが実行されますが、ない場合はGet-Processというコマンド名を探します。

もちろん、Get-Processというコマンドレットは標準で存在するので、それが実行されてしまう、というわけでした。

(ちなみにPowerShell 3.0以降なら、Get-付与で見つからない場合、さらにCmdlet Auto Discoveryにより未ロードのモジュールを探します。)

コマンド探索の詳細な挙動は、Trace-Command -Expression {コマンド}  -Name CommandDiscovery -PSHost とすると調べられるので、見てみるのもいいかもしれません。

D &{process{process{}}}
結果

「Get-Process : パラメーター 'Name' を評価できません。その引数がスクリプト ブロックとして指定され、入力が存在しないためです。スクリプト ブロックは、入力を使用せずに評価できません。」というParameterBindingExceptionが発生します。

解説

・パーサーの挙動

Get-Processが実行され(ようとす)る理由についてはCまでの理解でOKでしょう。

さて、Get-Processコマンドレットには-Nameという、プロセス名を指定する位置パラメータが存在します。位置パラメータは、パラメータ名を指定せずパラメータ値のみを指定しても、指定順にパラメータにバインドしてくれる機能を持ちます。

たとえば、Get-Process powershell とすると、「Get-Process -Name powershell」が実行されます。

今回のお題「process{}」は、パーサーによってまず、コマンド名「process」と、パラメータ値「{}」(空のスクリプトブロック)に分割されます。

(ちなみにコマンド名に「{}」を含めることができないわけではなく、そういうコマンドを実行したい場合は、`でエスケープするか、&"command{}name"のように&演算子を用いれば可能です。)

今回の場合、パラメータ名の指定はありませんが、位置パラメータ-Nameに空のスクリプトブロックがバインドされることになるわけです。

・コマンドパラメータバインドの挙動

さて、-Nameパラメータの型は、System.String[]であり、scriptblockではありません。もちろんscriptblockからSystem.String[]への暗黙の型変換はありません。でもエラーメッセージ的には、スクリプトブロックを与えたこと自体は咎めていないように思えますね。

実はこれ、スクリプトブロックパラメータと呼ばれてる機能です。詳しくはスクリプトブロックパラメータのススメを見ていただくとして、要はコマンドへのパイプライン入力を、指定のスクリプトブロックで処理し、その出力結果をパラメータ値としてバインドする機能ですね。

今回エラーになった理由は、スクリプトブロックパラメータとして解釈しようとしたが、そもそも入力がなかったから、ということになります。

あまり意味はないですが、以下のように入力を与えてやれば、スクリプトブロックパラメータとして動作します。

"powershell" | Get-Process -Name {$_}

この場合パイプライン入力が追加されるので、-Nameパラメータの指定位置がずれることになるので、パラメータ名が必要になります。また、スクリプトブロックが空だと、「パラメーター 'Name' を評価できません。その引数の入力によって出力が作成されなかったためです。」というエラーをご丁寧に出してくれます。Trace-CommandでParameterBindingソースをトレースしてみるのも一興でしょう。

ちなみにあまり関係ない余談ですが、-NameパラメータにはValueFromPipelineByPropertyName属性が付いているので、実は以下のような指定もできます。

[PSCustomObject]@{Name="PowerShell"} | Get-Process

まとめ

PowerShellパーサーと飲むとき、話の肴にどうですかね。

See also: 本当は怖いPowerShell その1

2015/05/10

第5回PowerShell勉強会でのセッション資料を公開します。

公開されたばかりのWindows Management Framework 5.0 Preview April 2015 、Windows 10 Insider Preview、Windows Server 2016 Technology Preview 2、そしてBuild 2015とMicrosoft Ignite期間中に発表になった情報をなるべく盛り込んでみました。

以下は今回資料を作るのに参考にしたサイト、記事群の一部です。

Windows Management Framework 5.0 Preview April 2015 何はなくともリリースノート。ただし全然まとまっていないので読みづらいです。

What's New in Windows PowerShell いわゆる更新履歴

Windows PowerShell Blog - Site Home - MSDN Blogs 特に直近の記事はすべて重要です

Windows Server Blog - Site Home - TechNet Blogs 上に同じく

Software Inventory Logging Cmdlets in Windows PowerShell SILコマンドレットのリファレンス

Jeffrey Snover | Speakers | Channel 9 PowerShell設計者であるJeffrey Snover氏のIgniteでの講演ビデオ、セッション資料

PowerShell v5 Undocumented Features List Altriveさんの作成されたundocumentedな新機能リスト

Windows Management Framework 5.0 Preview April 2015 がリリースされました - tech.guitarrapc.com ぎたぱそさんのまとめ記事

Windows Server 2016 Technical Preview 2 インストールと GUI の追加 村嶋さんの記事

Microsoft Ignite 2015 Keynote | ブチザッキ No.1によるIgniteキーノートまとめ

Nano Server関係は以下。ちなみにコンテナのPreviewは今夏だそうです。

Getting Started with Nano Server Technet公式記事だが、この手順通りでは動かないらしい

Create Nano Server VHDX file from WIM Altriveさん作成のNano Server構築スクリプト

Nano Server 取りあえずやってみた | ブチザッキ Nano Server構築

vNextに備えよ! 次期Windows Serverのココに注目(14):Windows Server 2016の新プレビュー版に「Nano Server」が搭載、その導入と管理方法 - @IT

山市良のえぬなんとかわーるど: 山市良版 Getting Started with Nano Server 山市良さんのNano Server記事

2014/12/24

はじめに

この記事はPowerShell Advent Calendar 2014の24日目の記事です。

今回は、Windows 8から追加されたOSの機能である、「Windows 位置情報プラットフォーム」をPowerShellから呼び出して、位置情報(緯度、経度)を取得してみよう、という話になります。

Windows 位置情報プラットフォームとは

Windows 8から、「Windows 位置情報プラットフォーム」という機能が追加され、アプリケーションから現在位置情報(緯度、経度など)をAPIで取得できるようになっています。

Windows 位置情報プラットフォームでは、位置情報をGPSがあればGPSから、なければWi-Fiの位置情報あるいはIPアドレスなどから推定して取得します。すなわち、GPSがない場合でも位置情報を取得できる、いわば仮想GPSの機能がデフォルトで備わっているのがミソです。

(注:Windows 7にも「Windows センサー&ロケーションプラットフォーム」というのがありましたが、OSデフォルト機能としては仮想GPSはありませんでした。今は亡き、Geosense for Windowsというサードパーティー製アプリを追加すると仮想GPS使えたんですけどもね。あとWindows Phone?知らない子ですね…

PowerShellでWindows 位置情報プラットフォームを利用する

さて、Windows 位置情報プラットフォームをPowerShellで使うには、.NET4.0以上に含まれている、System.Device.Location名前空間配下に含まれるクラスの機能を用います。アセンブリ名としてはSystem.Deviceとなります。

以下のような関数Get-GeoCoordinateを定義します。

Add-Type -AssemblyName System.Device

function Get-GeoCoordinate
{
    param(
        [double]$Latitude,
        [double]$Longitude
    )

    if(0 -eq $Latitude -and 0 -eq $Longitude)
    {
        $watcher = New-Object System.Device.Location.GeoCoordinateWatcher
        $sourceId = "Location"
        $job = Register-ObjectEvent -InputObject $watcher -EventName PositionChanged -SourceIdentifier $sourceId
        $watcher.Start()
        $event = Wait-Event $sourceId
        $event.SourceEventArgs.Position.Location
        Remove-Event $sourceId
        Unregister-Event $sourceId
    }
    else
    {
        New-Object System.Device.Location.GeoCoordinate $Latitude,$Longitude
    }
}

関数実行前に、まずAdd-Type -AssemblyName System.Deviceを実行して必要なアセンブリをロードする必要があります。

関数本体ではまず、System.Device.Location.GeoCoordinateWatcherオブジェクトを生成します。このオブジェクトのStartメソッドを実行すると、Windows 位置情報プラットフォームにアクセスして、位置情報の変化を監視します。位置情報の変化を感知すると、PositionChangedイベントが発生し、取得した位置情報を、イベントハンドラの引数にGeoPositionChangedEventArgs<T>オブジェクトとして返します。

さて、PowerShellでは、.NETクラスのイベントを取得するには、Register-ObjectEventコマンドレットを用い、イベントを「購読」します。

イベントが発生するたびに何かの動作をする、というような場合では、Register-ObjectEvent -Action {処理内容}のようにして、イベントハンドラを記述するのが一般的です。が、今回は位置情報の変化の最初の一回(つまり、初期値の取得)さえPositionChangedイベントを捕まえればOKなので、-Actionは使用しません。

代わりにWait-Eventコマンドレットを用い、初回のイベント発生を待機するようにしています。Wait-Eventコマンドレットは、当該イベントを示すPSEventArgsオブジェクトを出力します。

PSEventArgsオブジェクトのSourceEventArgsプロパティには、当該イベントのイベントハンドラ引数の値(ここではGeoPositionChangedEventArgs<T>オブジェクト)が格納されているので、あとはそこから.Position.Locationと辿ることで、位置情報を格納したGeoCoordinateオブジェクトが取得できます。

(注:あとで知ったんですけど、GeoCoordinateWatcherクラスには、同期的に位置情報を取得するTryStartメソッドというのがあって、これを使えばイベント購読は実は不要でした…まぁいっか)

なお、関数のパラメータとしてLatitude(緯度)、Longitude(経度)を指定すると、現在位置ではなく、指定の位置を格納したGeoCoordinateオブジェクトを生成するようにしています。

Get-GeoCoordinate関数の使い方

事前にコントロール パネルの「位置情報の設定」で「Windows 位置情報プラットフォームを有効にする」にチェックを入れておきます。

あとはGet-GeoCoordinateをそのまま実行するだけです。

Latitude           : 34.799999
Longitude          : 135.350006
Altitude           : 0
HorizontalAccuracy : 32000
VerticalAccuracy   : NaN (非数値)
Speed              : NaN (非数値)
Course             : NaN (非数値)
IsUnknown          : False

このように現在位置が表示されるかと思います。といっても、緯度、経度が表示されたところでちゃんと取得できてるのかよく分からないので、以下のような簡単な関数(フィルタ)を定義しておきます。

filter Show-GoogleMap
{
    Start-Process "http://maps.google.com/maps?q=$($_.Latitude),$($_.Longitude)"
}

このフィルタを使うと、指定の緯度経度周辺の地図を、標準のWebブラウザで開いたGoogleマップ上に表示してくれます。使い方はこんな感じ。

Get-GeoCoordinate | Show-GoogleMap

現在位置が表示されましたでしょうか? 位置測定に用いたソースによってはkmオーダーでズレると思いますが、それでも何となく、自分がいる場所が表示されるのではないかと思います。

なお、先ほども書いたように、パラメータで任意の緯度、経度を指定することも可能です。この関数だけではあんまり意味を成しませんが…

Get-GeoCoordinate 35.681382 139.766084
まとめ

PowerShellでも「Windows 位置情報プラットフォーム」を使って現在位置が取れるよ、という話でした。あんまりPowerShellでSystem.Device.Locationとかを使っているサンプルを見かけないので、何かの参考になれば幸いです。あとPowerShellでのイベントの扱い方についても復習になるかと。

ところでこうやって取得した位置情報を使って、Web APIを呼び出して活用しよう、というようなネタを書くつもりだったんですが、長くなったんでまたの機会としましょう。ではでは。

2014/12/22

はじめに

この記事はPowerShell Advent Calendar 2014の22日目の記事です。

前回はMMLをパースし、音楽をBeepで再生するところまで作りました。

MMLはテキストデータなので、テキストエディタで入力しても良いのですが、どうせなら、PowerShellでエディタも作ってしまいましょう。

コード

いきなりですが、エディタの本体コードを。

function Enter-MusicEditor
{
    $keyMap=@{
        A="<G+4>"
        Z="<A4>"
        S="<A+4>"
        X="<B4>"
        C="C4"
        F="C+4"
        V="D4"
        G="D+4"
        B="E4"
        N="F4"
        J="F+4"
        M="G4"
        K="G+4"
        ","="A4"
        L="A+4"
        "."="B4"
        "/"=">C4<"
        ":"=">C+4<"
        "\"=">D4<"
        "]"=">D+4<"
        "R"="R4"
    }

    $mml=@()
    cls
    while($true)
    {  
        $k = [System.Console]::ReadKey($true)

        if($k.Key -eq [System.ConsoleKey]::Escape -or $k.Key -eq [System.ConsoleKey]::Enter)
        {
            cls
            break
        }
        elseif($k.Key -eq [System.ConsoleKey]::Backspace)
        {
            if($mml.length -eq 1)
            {
                $mml=@()
            }
            elseif($mml.length -ge 2)
            {
                $mml=$mml[0..($mml.length-2)]
            }
            cls
            [console]::write((-join $mml))
        }
        else
        {
            $key=$k.KeyChar.ToString().ToUpper()
        
            if($keyMap.Contains($key))
            {
                $mml+=$keyMap[$key]
                [console]::write($keyMap[$key])
                Invoke-Mml $keyMap[$key]
            }
        }
    }
    -join $mml
}
使い方

PowerShellコンソール上(ISEは不可)で、前回公開した、ConvertFrom-MML、Invoke-Beep、Invoke-Mmlの3つの関数をまず読み込み、続いて上記のEnter-MusicEditor関数を読み込みます。

この状態でコンソール上でEnter-MusicEditorを実行すると、MMLエディタモードに入ります。

MMLエディタモードでは、PCのキーボードを鍵盤代わりにして入力できます。

たとえば、Cキーを押下すると、「ド」の音がBeepで鳴り、コンソールに"C4"と出力されます。同様にFキー押下で「ド♯」が鳴り"C+4"と出力されます。どのキーがどの音に対応しているかは、上記コードの$keyMap変数に格納された連想配列を参照してください。

(なんとなくですけど、楽器の鍵盤の配置と合わせてあります)

Backspaceキーを押下すると、直前の入力を削除できます。

EnterキーかEscapeキーを押下すると、エディタモードを終了します。

出力したMMLは、コンソール上に表示が残るので、あとはコピーしてInvoke-Mml関数に渡してやると演奏することができます。

また、$mml=Enter-MusicEditorとしてやると、入力したMMLをそのまま変数に格納できます。この場合だとInvoke-Mml $mmlと実行すれば演奏できます。

解説

技術的には全然大したことをしてないですが、一応解説。

まず、[System.Console]::ReadKey()で入力したキーコードを判別します。引数に$trueを指定すると、入力したキー名をそのままコンソールに出力するのを抑制できます。

ReadKeyメソッドはConsoleKeyInfoオブジェクトを返します。アルファベットキーについてはKeyCharプロパティの値、特殊キーについてはConsoleKey列挙体を返すKeyプロパティの値で調べることができます。

あとは入力キーから対応する音名を連想配列$keyMapから取ってきて、その音名を[System.Console]::Write()でコンソール出力すると同時に、Invoke-Mml関数でその音をBeepとして鳴らしているだけです。

まとめ

入力も再生もできるようになったので、これはもう完全にシーケンサーですね! いや、やはり無理があるか…。

しかし、他のMMLコンパイラと併用して、ちょっと演奏しながらMML入力したいな、というときにもしかすると役に立つかもしれません。

ちょっと機能が少なすぎるんで、せめて↑で半音上げ↓で半音下げ、→で伸ばす←で短く、くらいはそのうち実装してみたいですね。

まあ、MML作成にはあんまり役に立たないかもしれませんが、PowerShellでコンソールの入出力を制御するのは、こういう単純なものなら意外と簡単にできるという、サンプルにはなるかと思います。

2014/12/01

はじめに

この記事はPowerShell Advent Calendar 2014の1日目の記事です。

次期バージョンのPowerShell 5.0について、そろそろ情報が出回ってきました。現在のところWindows Management Framework 5.0 Preview November 2014、もしくはWindows 10 Technical PreviewWindows Server Technical Previewに同梱のもので試すことができます。

v5での新機能、改善点は多岐に上ります。OneGet / PowerShellGet / クラス定義 / DSC機能増強 / ODataエンドポイントのコマンドレット化 / zipファイル / シンボリックリンク 等々。詳しくは、リリースノートが一番充実しているかと思います。日本語だとぎたぱそ氏の記事がまとまっているかと思ます。

さて、ここまで挙げた新機能や改善点は、とても順当でまっとうな進化点なのですが、v5にはちょっと異彩を放つ新機能がしれっと追加されています。それが、Auto-Generated Example-Driven Parsing です。

Auto-Generated Example-Driven Parsing とは

CSV、JSON、XMLのような既知のフォーマットではないが、何らかの法則性のあるテキストデータがあるとします。そんなテキストデータは(不幸なことに)割と世の中にあふれていますが、そのままでは(人が読む以外には)利用できないので、データとして扱うには、解析し、レコード(プロパティ:プロパティ値)として再構築する必要があります。

しかしながらフォーマットが既知のものではないため、既存のパーサーを使って解析することはできません。

従来のアプローチだと、このようなデータに対しては、まずユーザー(人間)がデータの法則性を読み取り、その法則をコンピュータに分かる表現(コードや正規表現など)に変換してやる必要がありました。

Auto-Generated Example-Driven Parsing とは、事前にユーザーがテキストデータの一部分のみを取り出し、各項目に対してプロパティ名を指示したデータ(テンプレート)を用意しておくと、元のテキストデータとユーザーが用意したテンプレートから法則性を解析し、元のテキストデータ全体を自動的にテキストデータからオブジェクトに変換してくれる機能です。

Auto-Generated Example-Driven Parsing とはもともとMicrosoft Research で研究されているFlashExtract というデータ解析手法の PowerShell コマンドレット(ConvertFrom-String)による実装になります。ConvertFrom-StringData じゃないですよ。全然別物です。これうっかりしてるとスルーしてしまいそうです。

具体例

たとえば、こんなデータがあったとします。

山内 佳乃 (やまうち よしの)
生年月日...1982/1/27 (32歳)、女性
田畑 真帆 (たばた まほ)
生年月日...1966/4/14 (48歳)、女性
三好 一樹 (みよし かずき)
生年月日...1972/7/10 (42歳)、男性
酒井 幸平 (さかい こうへい)
生年月日...1954/3/1 (60歳)、男性
藤島 恵子 (ふじしま けいこ)
生年月日...1969/5/4 (45歳)、女性
加藤 美優 (かとう みゅう)
生年月日...1986/12/8 (27歳)、女性
金谷 康文 (かなや やすふみ)
生年月日...1983/10/7 (31歳)、男性
岸本 紗季 (きしもと さき)
生年月日...1984/5/16 (30歳)、女性
永野 ケンイチ (ながの けんいち)
生年月日...1961/7/8 (53歳)、男性
小関 三郎 (こぜき さぶろう)
生年月日...1975/1/22 (39歳)、男性
山岸 光 (やまぎし ひかる)
生年月日...1939/2/13 (75歳)、女性
黒谷 恵麻 (くろたに えま)
生年月日...1949/2/13 (65歳)、女性

名前や生年月日が書かれたデータで、一応、法則性はあるようです。が、これをまともにパースしようと思うと、2行ごとに切り出して、正規表現を書いて…と、ちょっと面倒ですね。

ちなみにこのダミーデータ作成にはなんちゃって個人情報を使わせていただきました。CSVで出力した後、以下のようなスクリプトでわざわざ醜く変形しました。

Import-Csv -Encoding Default -Path dummy.cgi|%{"$($_.名前) ($($_.ふりがな))`n生年月日...$($_.誕生日) ($($_.年齢)歳)、$($_.性別)性"}|set-content -Encoding UTF8 -Path dummy.txt

さて、このテキストデータに対し、Auto-Generated Example-Driven Parsingで用いるテンプレートを書いてやりましょう。たとえば、以下のように適当に3件(ここでは3〜5個目のレコード)抜き出して、プロパティ名をつけてやります。赤字が、手動で元データに付与した文字列です。

{Name*:三好 一樹} ({Furigana:みよし かずき})
生年月日...{BirthDay:1972/7/10} ({Age:42}歳)、{Sexuality:}{Name*:酒井 幸平} ({Furigana:さかい こうへい})
生年月日...{BirthDay:1954/3/1} ({Age:60}歳)、{Sexuality:}{Name*:藤島 恵子} ({Furigana:ふじしま けいこ})
生年月日...{BirthDay:1969/5/4} ({Age:45}歳)、{Sexuality:}

みて頂ければ分かると思いますが、基本は、各データ項目に対して、{プロパティ名:データ}のように指定してやるだけです。主キーとなるデータ項目にはプロパティ名の後に「*」をつけてやります。こうやって作ったテンプレートをtemplate.txtと名前を付けて保存しましょう。

元データとテンプレートが揃ったので、あとは以下のようにしてConvertFrom-Stringコマンドレットを実行するだけです。

image

テンプレートを元に、元テキストに含まれるすべてのデータが、プロパティ値を持ったオブジェクトデータに変換されていることが分かるかと思います。これちょっとすごくないですか?

まとめ

Auto-Generated Example-Driven Parsingは個人的には、非常に面白い機能だと感じています。コンピュータに対して、「手本見せるよ、これはこう、これはこう。わかった? じゃ、あとは同じようにまとめといてね!」というのができるようになったわけで、ちょっと未来を感じました。

研究所レベルの研究成果を、製品として実装した初の例が、PowerShellだったというのも面白味を感じます。

ただ、CSVでもJSONでもXMLでもない、わけのわからない謎フォーマットで保存されたテキストデータを解析しなきゃならない事態というのは、そもそも不幸な状況であることも、また事実かと思います。

ConvertFrom-Stringは、そんな訳の分からないものを撲滅して、今度こそまともなフォーマットのデータに変換して保存するための、最終兵器のようなものかもしれません。

なお、Auto-Generated Example-Driven Parsingでは他にもプロパティに型を指定したり、部分的に正規表現を用いたり、階層構造を持つデータにも対応してたりと、かなり色々なことができるようになっています。ぜひ、v5環境を整えて、ConvertFrom-Stringを試してみてください。

さてさて、PowerShell Advent Calendar 2014、今年は参加者が少なく、完走はかなり危ぶまれますが、できるところまで行きたいですね! これをお読みのあなたの記事が読みたいです! ぜひ、ご参加いただけると幸いです。


古い記事のページへ |


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

Books

Twitter