2012年12月24日月曜日

about_SessionState - Things That Are Precious To a PowerShell Librarian -

PowerShell Advent Calendar 2012、24 日目です♪

今年ももうすぐ終わり・・・...( = =)、PowerShell Advent Calendar 2012 も残りわずかとなりましたね。
昨日は @MasayaSawada さんの『Windows Server 2012 Essentials の Powershell』でした。


さて街は「クリスマス」一色で煌びやかな 24 日。こちらも負けじと、PowerShell の華(と私が勝手に思っている)、セッションのお話をさせていただければと。

セッション。それは、ヘルプ トピック上、いたるところで登場し、構成要素を追加されたり、検索されたり、削除されたり、インポートされたり、エクスポートされたりする領域。個人的には、PowerShell の超中心的概念だと思うのですが、驚くべきことに、これを説明するヘルプ トピックはありません。代わりに、スコープやモジュール、PSSession、関数、変数のトピックを見て回ることにより、それが PowerShell が持つ環境そのものであり、なるべく問題領域を局所化し、他の部分へ影響を与えにくくするための仕組みであることが、朧げながら見えてくるように思います。

まあ、イマドキのプログラミング言語であれば、このような仕組みを何かしら持っているはずで、目新しいことは無いのですが・・・。ただ、PowerShell に限らず、IDE 側のサポートが少なくなりがちな動的型付け言語では、このような言語側の仕組みに頼るだけでなく、なるべく扱いやすい規模でファイルを分割したり、状態を制御できるように保ったり、といったことを、人間側で、ある程度お膳立てしないといけないのだなー、と感じる今日この頃。先日の記事に登場するライブラリ、PSAnonym も、この辺りの考え方を盛り込み、今後なるべく機能の追加や拡張をしやすい形に整備しました。

そんな試行錯誤から、今回は、about_SessionState と銘打ち、セッションが何なのかということを、自分の理解の整理も兼ねて、できるだけ俯瞰的に解説できればと思います。Tips のような、これは便利!というものではないのですが、PowerShell で、これからある程度まとまったものを作ろうと考えられている方へ参考になれば幸いです。

それではぼちぼち始めましょう~。

※今回の記事で扱うのは、PowerShell v2 の情報となります。PowerShell v3 での情報は、また別の機会に検証させていただければと思います <(_ _)>


こちらの情報を参考にさせていただきました。いつもお世話になっております! (`・ω・ )ゝ
Best Practices for Windows PowerShell
PowerShellでプロトタイプベースのオブジェクト指向を記述する方法 - 趣味の無い人生は虚しい
Parallel LINQ (PLINQ)
Writing your own PowerShell Hosting App (Part 1. Introduction) PowerShell Station
How to Host PowerShell in a WPF Application
snoopshell The marriage of Snoop WPF and PowerShell





目次

PS1 means Pathologically Suppleness no.1!?
「PowerShell でまとまったものなんて作らないよ」・・・(ToT)
いきなりそんなことおっしゃらずに・・・いやいや、奥さん、ちょっとこんなデータがあるんですよ。

まとまったものを作る時にプログラマの誰しもが考えるのが、書くコードの一貫性、すなわちコーディングスタイル。
いくつかのプログラミング言語の状況を比べてみますと、2012/12/24 現在、Bing での検索結果は以下のようなものになりました:

  "JavaScript Coding Style" で検索: 約 1,460,000 件 / "JavaScript" で検索: 約 819,000,000 件 ⇒ 約 0.2 %
  "Scala Coding Style" で検索: 約 67,300 件 / "Scala" で検索: 約 14,200,000 件 ⇒ 約 0.5 %
  "Basic Coding Style" で検索: 約 2,490,000 件 / "Basic" で検索: 約 314,000,000 件 ⇒ 約 0.8 %
  "PHP Coding Style" で検索: 約 2,150,000 件 / "PHP" で検索: 約 282,000,000 件 ⇒ 約 0.8 %
  "Ruby Coding Style" で検索: 約 626,000 件 / "Ruby" で検索: 約 71,700,000 件 ⇒ 約 0.9 %
  "Java Coding Style" で検索: 約 1,460,000 件 / "Java" で検索: 約 94,000,000 件 ⇒ 約 1.6 %
  "C++ Coding Style" で検索: 約 1,020,000 件 / "C++" で検索: 約 22,200,000 件 ⇒ 約 4.6 %
  "C# Coding Style" で検索: 約 860,000 件 / "C#" で検索: 約 18,600,000 件 ⇒ 約 4.6 %
  "Perl Coding Style" で検索: 約 776,000 件 / "Perl" で検索: 約 11,800,000 件 ⇒ 約 6.6 %
  "F# Coding Style" で検索: 約 75,600 件 / "F#" で検索: 約 735,000 件 ⇒ 約 10.3 %

  "PowerShell Coding Style" で検索: 約 99,900 件 / "PowerShell" で検索: 約 496,000 件約 20.1 %

ちょっとスゴいことになっていますね。この割合を鵜呑みにするならば、PowerShell をやられている方の 5 人に 1 人がコーディングスタイルに悩まれていることになります。
個人的には、柔軟な書き方ができる言語(1 つのことをするのに何通りもやり方がある言語)ほど、このような議論が発生しやすいと思っているのですが、あの C++ や Perl、他の Microsoft 製ごった煮言語、C#/F# を抑えての堂々の 1 位とは・・・。また、これだけ高い割合で、コードの書き方に悩まれる方がいらっしゃる、ということは PowerShell で何かしら作ろうとされている方が多いことの表れとも考えられるのではないでしょうか。・・・えっ、無理やり過ぎるですって?そこは発信者特権ということで (^_^;)

この結果を信じるかどうかはさておき、コーディングスタイルで悩むことの 1 つに、「1 ファイルの行数は何行?」「1 メソッドの行数は何行?」といったものがあるかと思います。近頃は関数型なエッセンスを取り込んでいる言語も少なくなく、1 行の裏側で色んなことが行われていますので、上記のような制限を掛けるまでもなくどんどん簡素化される印象があります。
しかしながら、特に低レイヤな仕組みを構築するに当たり、処理が増えていった場合にどのようにして分割すると問題が起こりにくくなるかをあらかじめ検討しておくことは、後々気を病まずに済むことになるでしょう。
PowerShell において、このような分割を検討するに当たり、覚えておく必要があるのが、セッションということになります。




about_Scopes
そのものずばりなヘルプ トピックは首記の通り無いのですが、比較的俯瞰された説明があるのが about_Scopes になります。セッションに触れられるのはちょこっとなのですが、PowerShell のセッションに似た概念であるスコープは 1 つトピックが切られており、それに類似する概念も解説されています。
ざっと書き出してみましょう:

  ・スコープは以下のルールにより、変更してはいけない項目を誤って変更することを防止する:
    ・スコープ内に作成できる項目は、変数、エイリアス、関数、スクリプトまたは PowerShell ドライブである。
    ・スコープ内に作成された項目は、明示的にプライベートとして宣言した場合を除き、項目が作成された
     スコープ内およびその子スコープ内でのみ可視になる。
    ・スコープ内に作成した項目は、他のスコープを明示的に指定した場合を除き、項目が作成されたスコープ内でのみ変更できる。
  ・スコープには以下の種類がある:
    ・グローバル
    ・ローカル
    ・スクリプト
    ・プライベート
    ・番号付きスコープ
  ・スコープは以下のタイミングで生成される:
    ・スクリプトまたは関数を実行したとき
    ・PSSession を作成したとき
    ・PowerShell の新しいインスタンスを起動したとき
  ・スコープに類似した概念に、以下のような自己完結型の環境がある:
    ・PSSession
    ・モジュール
    ・入れ子になったプロンプト

ここにセッションを絡めてみます。文字ばかりだとややこしいですので、図でまとめてみるとこんな感じになるかと思います。


・・・ややこしいのは相変わらずですが、少しは俯瞰しやすくなったでしょうか (^_^;)

ちょっと補足しますと、PowerShell のヘルプ トピックでセッションと言った場合、「.NET におけるアプリケーションのドメイン(AppDomain)」を同時に指している場合や、「PowerShell の新しいインスタンスを起動すると自動的に開始される環境」を単に指している場合、「コンピューター毎に作成できる PSSession」を同時に指している場合・・・というパターンがあるようなのです。

一括りにすると混乱すると感じましたので図でも分けてありますが、この記事では、単に「セッション」と言った場合、基本的には 2 番目のものを指すものとしたいと思います。PowerShell のヘルプ トピック上で単に「セッション」とあっても、最初のものを指していると思われる場合は、AppDomain + セッションとし、3 番目を指していると思われる場合は、PSSession + セッションとしています。

また、モジュールがセッションを持っていることにしています。これは about_Modules トピックには記載はないのですが、PowerShell の振る舞いから便宜上そうしているものです。スコープについても、基本的にセッション + スコープという形で動作しています。

あとは、about_Scopes トピックの関連項目にちらっと記載があるだけの環境変数や作業フォルダなどの OS の環境ですが、変数としては特別に扱われるため、注意が必要です。

さて、外側の領域から、OS の環境/PSSession + セッション、AppDomain + セッション、セッション + スコープと、コマンドを叩きながら代表的な構成要素の動きを見ていきたいと思います。




OS の環境/PSSession + セッション
OS の環境である環境変数や作業フォルダですが、PSSession では新しいものが 1 つ切られ、プロンプトでは起動元のプロンプトのものが引き継がれます。
PSSession はコンピューター毎に作成されるものですし、環境変数などが起動元のプロンプトのものが引き継がれるのは標準のコマンド プロンプトと同じ動きと同じですので、これは問題無さそうですね。
C:\>:: 標準のコマンド プロンプトで環境変数を変更。
C:\>set EnvironmentVariable=Environment Variable

C:\>echo %EnvironmentVariable%
Environment Variable

C:\>:: PowerShell プロンプトを実行。環境変数が引き継がれる。
C:\>powershell -nol -nop -c "& { $Env:EnvironmentVariable }"
Environment Variable

C:\>:: 入れ子になったPowerShell プロンプトを実行。環境変数が引き継がれる。
C:\>powershell -nol -nop -c "& { powershell -nol -nop -c '& { $Env:EnvironmentVariable }' }"
Environment Variable

C:\>:: PSSession では新しい OS の環境が用意される。
C:\>powershell -nol -nop -c "& { icm { iex '$Env:EnvironmentVariable' } -session (nsn localhost) }"

C:\>
 

PSSession って実際は何なの?と思って調べてみると、以下のような結果に。WinRM サービスから起動される、PowerShell.exe とは別の PowerShell ホスティング プロセスのようです。
PS C:\> Enter-PSSession localhost
[localhost]: PS C:\Users\User\Documents>  [Diagnostics.Process]::GetCurrentProcess() | Format-List


Id      : 1292
Handles : 279
CPU     : 1.6224104
Name    : wsmprovhost



[localhost]: PS C:\Users\User\Documents>
 





AppDomain + セッション
PowerShell に限らず、.NET を扱うプロセスでは、基本的に AppDomain の呪縛からは逃れられませんので、無理に隠ぺいして良いことはないと思うのですがどうなんでしょう・・・? (^_^;)
ヘルプ トピックでは単にセッションという記載しか無いのにも関わらず、実際には AppDomain + セッションを指しているものは、Add-PSSnapin や Add-Type、Assembly 指定版の Import-Module が該当します。無理している感じは、例えば、Add-PSSnapin と対になる Remove-PSSnapin のヘルプ トピックにある、「スナップインは、現在のセッションから削除した後も読み込まれた状態のまま」という説明の苦しさからもわかります。読み込まれた状態ってどこに読み込まれたままなのー???と疑問に思う方は少なくないと思いますが、その答えが AppDomain になります。
PS C:\> # 現在のプロセスの AppDomain に読み込まれている Assembly の数を表示する。
PS C:\> [AppDomain]::CurrentDomain.GetAssemblies().Length
21
PS C:\> # スナップインを追加してみる。
PS C:\> Add-PSSnapin SqlServerCmdletSnapin100
PS C:\> # コマンドは増えた?
PS C:\> (gcm).Length
414
PS C:\> # もう一度 AppDomain に読み込まれている Assembly の数を表示する。
PS C:\> [AppDomain]::CurrentDomain.GetAssemblies().Length
23
PS C:\> # コマンド、読み込まれている Assembly、共に 2 つ増えた!スナップインを削除してみる。
PS C:\> Remove-PSSnapin SqlServerCmdletSnapin100
PS C:\> # コマンド、読み込まれている Assembly を確認すると、コマンドは減ってるけど
PS C:\> # Assembly は減っていない。
PS C:\> (gcm).Length
412
PS C:\> [AppDomain]::CurrentDomain.GetAssemblies().Length
23
PS C:\> 
 

同じ AppDomain に、同一名称の別の型は読み込めません。標準の PowerShell ホスティング プロセスは、基本的にはシングル ドメインしか扱いませんので、一度 Add-Type した型を定義し直したい場合などは、プロンプトもしくは PSSession を使って新しいプロセスを開始する必要があります。
PS C:\> # 入れ子になったプロンプトの開始。
PS C:\> powershell -nol -nop
PS C:\> # 型の追加。
PS C:\> Add-Type NewType -m 'public static string NewTypeMethod() { return "New Type"; }' -names $null
PS C:\> [NewType]::NewTypeMethod()
New Type
PS C:\> # ここでは再定義はできない。
PS C:\> Add-Type NewType -m 'public static string NewTypeMethod() { return "New Other Type"; }' -names $null
Add-Type : 型を追加できません。型名 '.NewType' は既に存在しています。
発生場所 行:1 文字:9
+ Add-Type <<<<  NewType -m 'public static string NewTypeMethod() { return "New Other Type"; }' -names $null
    + CategoryInfo          : InvalidOperation: (.NewType:String) [Add-Type]、Exception
    + FullyQualifiedErrorId : TYPE_ALREADY_EXISTS,Microsoft.PowerShell.Commands.AddTypeCommand

PS C:\> # 入れ子になったプロンプトの終了。
PS C:\> exit
PS C:\> # 入れ子になったプロンプトの再起動。
PS C:\> powershell -nol -nop
PS C:\> # 再定義。
PS C:\> Add-Type NewType -m 'public static string NewTypeMethod() { return "New Other Type"; }' -names $null
PS C:\> [NewType]::NewTypeMethod()
New Other Type
PS C:\>
 





セッション + スコープ
変数、エイリアス、関数、スクリプトブロック、PowerShell ドライブ、スクリプト、モジュールは、親の構成要素のセッションに追加される形で動作します。
変数、エイリアス、関数、スクリプトブロック、PowerShell ドライブ、スクリプトは、それ自身のセッションを持たない代わりにスコープの制御が行われます。モジュールは逆に、それ自身のセッションを持つ代わりにスコープの制御は行われません。

違いが分かりやすい例としてスクリプトとモジュールを取り上げてみましょう。スクリプトをプロンプトのセッションで実行します。変数はスコープを抜けると、セッションから削除されます。
PS C:\> # スクリプト内で、変数を宣言(変数は、プロンプトのセッションに、
PS C:\> # スクリプト スコープのものとして追加される)
PS C:\> @"
>> `$Script1 = 'New Script 1'; gv Script*
>> "@ > Script1.ps1
>>
PS C:\> @"
>> `$Script2 = 'New Script 2'; gv Script*
>> "@ > Script2.ps1
>>
PS C:\> # 各スクリプトの実行が終わると、スクリプト スコープの変数は、
PS C:\> # プロンプトのセッションから削除される。
PS C:\> .\Script1.ps1

Name                           Value
----                           -----
Script1                        New Script 1


PS C:\> gv Script*
PS C:\> .\Script2.ps1

Name                           Value
----                           -----
Script2                        New Script 2


PS C:\> gv Script*
PS C:\> ri .\Script1.ps1; ri .\Script2.ps1
PS C:\>
 

スクリプトはセッションを持ちませんので、スクリプト内でモジュールをインポートした場合、プロンプトのセッションに追加されます。
PS C:\> # スクリプト内で、モジュールをインポート(スクリプトはセッションを持たないため、モジュールは、
PS C:\> # プロンプトのセッションにインポートされる)
PS C:\> @"
>> 'Module1' | nmo { function Module1Func { 'Module 1 Func' } } | ipmo
>> "@ > Script1.ps1
>>
PS C:\> @"
>> 'Module2' | nmo { function Module2Func { 'Module 2 Func' } } | ipmo
>> "@ > Script2.ps1
>>
PS C:\> # モジュールはスコープを持たないため、インポートしたセッションが終了もしくは
PS C:\> # 明示的に削除が行われるまでは残ったまま。
PS C:\> .\Script1.ps1
PS C:\> gmo

ModuleType Name                      ExportedCommands
---------- ----                      ----------------
Script     Module1                   Module1Func


PS C:\> .\Script2.ps1
PS C:\> gmo

ModuleType Name                      ExportedCommands
---------- ----                      ----------------
Script     Module1                   Module1Func
Script     Module2                   Module2Func


PS C:\> rmo *; ri .\Script1.ps1; ri .\Script2.ps1
PS C:\>
 

逆にモジュールはセッションを持ちます。モジュール内でモジュールをインポートした場合、入れ子になったモジュールはモジュールのセッションに追加されることに注意しなければなりません。
特にまとまったものを作る場合、モジュールの中にモジュールを定義したくなる場面が多々あるかと思いますが、このことを忘れなければ混乱せずに済むでしょう。
PS C:\> # モジュール内で、モジュールをインポート(入れ子になったモジュールは、モジュールのセッションに
PS C:\> # インポートされる)
PS C:\> @"
>> 'NestedModule1' | nmo { function NestedModule1Func { 'Nested Module 1 Func' } } | ipmo
>> "@ > Module1.psm1
>>
PS C:\> @"
>> 'NestedModule2' | nmo { function NestedModule2Func { 'Nested Module 2 Func' } } | ipmo
>> "@ > Module2.psm1
>>
PS C:\> # プロンプトのセッションには入れ子になったモジュールは無く、モジュールのセッションにインポート
PS C:\> # されていることがわかる。
PS C:\> $m1 = ipmo .\Module1.psm1 -pa
PS C:\> gmo

ModuleType Name                      ExportedCommands
---------- ----                      ----------------
Script     Module1                   NestedModule1Func


PS C:\> & $m1 { gmo }

ModuleType Name                      ExportedCommands
---------- ----                      ----------------
Script     NestedModule1             NestedModule1Func
Script     Module1                   NestedModule1Func


PS C:\> $m2 = ipmo .\Module2.psm1 -pa
PS C:\> gmo

ModuleType Name                      ExportedCommands
---------- ----                      ----------------
Script     Module1                   NestedModule1Func
Script     Module2                   NestedModule2Func


PS C:\> & $m2 { gmo }

ModuleType Name                      ExportedCommands
---------- ----                      ----------------
Script     NestedModule2             NestedModule2Func
Script     Module1                   NestedModule1Func
Script     Module2                   NestedModule2Func


PS C:\> # ただし、同時に親セッションにメンバーがエクスポートされているため(NestedModule1 -> Module1 -> 
PS C:\> # プロンプト、NestedModule2 -> Module2 -> プロンプト)、関数は呼べる。
PS C:\> NestedModule1Func
Nested Module 1 Func
PS C:\> NestedModule2Func
Nested Module 2 Func
PS C:\> rmo *; ri .\Module1.psm1; ri .\Module2.psm1
PS C:\>
 





終わりに
PowerShell Advent Calendar 2012 の 24 日目、about_SessionState。いかがでしたか。願わくは、いつか PowerShell v3 のヘルプ トピックのどこかに、この辺りの話が載ればと思うばかりです。

ただ、まとめてみて思いましたが・・・ある意味「苦理済ます」にふさわしい、己の心を一人っきりで見つめる修行の日にぴったりの内容になってしまったかも・・・ <(ToT)>
お堅い話ばかりで申し訳ないです。本当は、もう少し実際のライブラリの中を歩きながら解説をしようとも思ったのですが、そちらの進捗が芳しくないですので、出来上がりの頃に改めて記事を書かせていただければと。

あ、話は変わるのですが、こちらばかりじゃなく、本体である Prig も早いところ形にしたいところだったりします。5 年近く同じテーマをやっていてなかなかモノが出てこないのは切ないですし。メモリ周りやビルドの問題も大方片付いたはずですので、近いうちに IL 打ち込む作業に入れるはず。来年こそは、自分を含めた開発者の方の心躍らせられるようなモノ、出せるようにがんばるです。 (`・ω・ )ゝ

さあ、いよいよ明日は大トリ、@mutaguchi さんです。よろしくお願いします!

2012年12月5日水曜日

再考! PowerShell で LINQ - Terrific! LINQ to PowerShell -

PowerShell Advent Calendar 2012、5 日目です!

はじめましての人ははじめまして!PowerShell Advent Calendar 2012 の 5 日目を担当させていただきます、杉浦と申します。
自分は、本業的には、.NET の静的型付けな言語で構築されたシステムに関わることがほとんどで、PowerShell を扱うのは素人なのですが、業務効率改善や不具合調査等、色んなところで助けられたこともあり、今回少しでも PowerShell 遣いな方々と情報共有できればと、こちらに参加させていただいた次第です。よろしくお願いいたします!

今回扱うテーマは、「PowerShell で LINQ」。もうこれまで散々議論されて来た感がありますが、.NET 開発者の 99.9% の人が知らないあの機能と同様、まだ見落とされてきた何かがあるのでは・・・!?と、私の記事では、これまで Web 上で発表された成果を振り返りつつ、別の手法を改めて考えてみたいと思います。では、早速行ってみましょー!

※今回の記事で扱うのは、PowerShell v2 の情報となります。PowerShell v3 での情報は、また別の機会に検証させていただければと思います <(_ _)>


こちらの情報を参考にさせていただきました。この場を借りてお礼申し上げます。この世界、ホント奥が深いとです・・・...( = =)
統合開発環境「PowerShell ISE」を使ってみよう:CodeZine
add-types.ps1 - poor man's using for PowerShell - BUGBUG poor title - Site Home - MSDN Blogs
PowerShell で LINQ - NyaRuRuが地球にいたころ
Powershell 2.0でチャーチ数の夢を見た - めらんこーど地階
Hey! Don’t break my pipe! IT Pro PowerShell experience
Best Practices for Windows PowerShell
LINQ for PowerShell
Tellingmachine PSUnit PowerShell Unit Testing Framework – Getting Started Guide – Installation - Version 2 Beta 1
Further Down the Rabbit Hole PowerShell Modules and Encapsulation
Pester - BDD style testing framework for PowerShell - @fsugiyamaの技術日誌
if($array -eq $null) には要注意! - PowerShell Scripting Weblog
PowerShell 2.0の新機能(2) ――リモート処理編 :CodeZine
HOW change Microsoft Windows PowerShell"! Ui Culture the time of a PSSession





目次

まずは成果を眺めてみる
作成途中ではあるのですが、もしよろしければ、どんな感じか触ってみていただくのが良いかもしれません。Pester みたく、PsGet できるとかっこいいのですが、とりあえずは手動です (^_^;)
  1. urasandesu / PSAnonym - GitHub からリポジトリをダウンロード(もしくは git clone)。
  2. 必要に応じて、管理者権限で PowerShell を起動し、Set-ExecutionPolicy を使って、スクリプト実行可能に。
  3. ダウンロードしたリポジトリの中にある、Urasandesu.PSAnonym ディレクトリを $env:PSModulePath にコピー。
  4. Import-Module 'Urasandesu.PSAnonym' でモジュールをインポート。

早速、単純な例として、MSDN にもある「1 から 10 個の整数シーケンスを生成し、それぞれを 2 乗する」をやってみましょう。
PS C:\> QRange 1 10 | QSelect { $_ * $_ } | QToArray
1
4
9
16
25
36
49
64
81
100
 

select や where は既に予約されていますので、Prefix として "Q" を付けてみました(linQ の Q です!)。また、ラムダ式に当たる部分は、ScriptBlock を使って表現しています。引数は、1 つだけの時は、$_ が、複数ある場合は、$1, $2, ・・・がプレースホルダとして使えます。もちろん ScriptBlock ですので、明示的に param () を使って別の名前で受け取ることもできます。集計操作の時などは、意味のある名前を付けたほうがわかりやすいかもしれませんね。
こちらも MSDN にある例で恐縮ですが、「" "(半角スペース)で区切られた文字列を逆順にする」を書いてみました。
PS C:\> $sentence = 'the quick brown fox jumps over the lazy dog'
PS C:\> $words = $sentence -split ' '
PS C:\> $reversed = { $words } | 
>>                      QAggregate { 
>>                          param ($workingSentence, $next) 
>>                          $next + ' ' + $workingSentence 
>>                      }
>>
PS C:\> $reversed
dog lazy the over jumps fox brown quick the
 

プレースホルダを使った版はこんな感じになります。C++ の Boost とかをよく使われているのであれば、こちらのほうが親しみがあるかもしれません。
PS C:\> $sentence = 'the quick brown fox jumps over the lazy dog'
PS C:\> $words = $sentence -split ' '
PS C:\> $reversed = { $words } | QAggregate { $2 + ' ' + $1 }
PS C:\> $reversed
dog lazy the over jumps fox brown quick the
 

これと同じですね。
#include <boost/algorithm/string.hpp>
#include <boost/lambda/lambda.hpp>
#include <boost/range/numeric.hpp>
#include <iostream>
#include <string>
#include <vector>

int main(int argc, char* argv[])
{
    using namespace boost;
    using namespace boost::algorithm;
    using namespace boost::lambda;
    using namespace std;

    string sentence = "the quick brown fox jumps over the lazy dog";

    vector<string> words;
    split(words, sentence, is_any_of(" "));

    string reversed = accumulate(words, string(), _2 + " " + _1);

    cout << reversed << endl;
    // This code produces the following output:
    // dog lazy the over jumps fox brown quick the

    return 0;
}
 

最後の例は、良く知られた FizzBuzz 問題。「無限リストを生成し、最初の 36 個の数字について Fizz Buzz する」、こんな感じになるでしょうか。
PS C:\> $query = QRange 1 ([int]::MaxValue) |
>>                 QSelect {
>>                     switch ($_) {
>>                         { $_ % 15 -eq 0 } { 'Fizz Buzz'; break }
>>                         { $_ % 3 -eq 0 } { 'Fizz'; break }
>>                         { $_ % 5 -eq 0 } { 'Buzz'; break }
>>                         default { $_ }
>>                     }
>>                 } |
>>                 QTake 36
>>
PS C:\> ($query | QToArray) -join ', '
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, Fizz Buzz, 16, 17, Fizz, 19, Buzz, F
izz, 22, 23, Fizz, Buzz, 26, Fizz, 28, 29, Fizz Buzz, 31, 32, Fizz, 34, Buzz, Fizz
 

最終的に用意するものは、以下の表のようなものを考えています。
PowerShell 関数名 エイリアス 機能 元の LINQ クエリ 実装
状況
Get-Aggregated QAggregate シーケンスにアキュムレータ関数を適用する。 Aggregate 100%
Get-AllSatisfied QAll シーケンスのすべての要素が条件を満たしているかどうかを判断する。 All<TSource> 100%
Get-AnySatisfied QAny シーケンスの要素が存在するか、または条件を満たすかどうかを判断する。 Any 100%
Get-Average QAverage 数値のシーケンスの平均値を計算する。 Average 100%
ConvertTo-Casted
Select-Casted
QCast 列挙子の要素を、指定した型に変換する。 Cast<TResult> 100%
Join-Concatenated QConcat 2 つのシーケンスを連結する。 Concat<TSource> 100%
Get-Contained QContains 指定した要素がシーケンスに格納されているかどうかを判断する。 Contains 100%
Get-Count QCount シーケンス内の要素数を返する。 Count 100%
Get-DefaultIfEmpty
Select-DefaultIfEmpty
QDefaultIfEmpty 列挙子の要素を返する。シーケンスが空の場合は既定値を持つシングルトン コレクションを返する。 DefaultIfEmpty 100%
ConvertTo-Distinct QDistinct シーケンスから一意の要素を返する。 Distinct 100%
Get-ElementAt QElementAt シーケンス内の指定されたインデックス位置にある要素を返する。 ElementAt<TSource> 0%
Get-ElementAtOrDefault QElementAtOrDefault シーケンス内の指定されたインデックス位置にある要素を返する。インデックスが範囲外の場合は既定値を返する。 ElementAtOrDefault<TSource> 0%
New-Empty QEmpty 指定した型引数を持つ空の 列挙子を返する。 Empty<TResult> 0%
Join-Except QExcept 2 つのシーケンスの差集合を生成する。 Except 0%
Get-First QFirst シーケンスの最初の要素を返する。 First 0%
Get-FirstOrDefault QFirstOrDefault シーケンスの最初の要素を返する。要素が見つからない場合は既定値を返する。 FirstOrDefault 0%
Group-SequenceBy QGroupBy シーケンスの要素をグループ化する。 GroupBy 100%
Join-GroupedBy QGroupJoin キーが等しいかどうかに基づいて 2 つのシーケンスの要素を相互に関連付け、その結果をグループ化する。 GroupJoin 0%
Join-Intersect QIntersect 2 つのシーケンスの積集合を生成する。 Intersect 0%
Join-Sequence QJoin 一致するキーに基づいて 2 つのシーケンスの要素を相互に関連付ける。 Join 0%
Get-Last QLast シーケンスの最後の要素を返する。 Last 0%
Get-LastOrDefault QLastOrDefault シーケンスの最後の要素を返する。要素が見つからない場合は既定値を返する。 LastOrDefault 0%
Get-LongCount QLongCount シーケンス内の要素数を表す Int64 を返する。 LongCount 0%
Get-Max QMax 値のシーケンスの最大値を返する。 Max 0%
Get-Min QMin 値のシーケンスの最小値を返する。 Min 0%
ConvertTo-OfType QOfType 指定された型に基づいて 列挙子の要素をフィルタ処理する。 OfType<TResult> 0%
Select-OrderBy QOrderBy シーケンスの要素を昇順に並べ替える。 OrderBy 100%
Select-OrderByDescending QOrderByDescending シーケンスの要素を降順に並べ替える。 OrderByDescending 100%
New-Range QRange 指定した範囲内の整数のシーケンスを生成する。 Range 100%
New-Repeat QRepeat 繰り返される 1 つの値を含むシーケンスを生成する。 Repeat<TResult> 100%
ConvertTo-Reversed QReverse シーケンスの要素の順序を反転させる。 Reverse<TSource> 0%
Select-Sequence QSelect シーケンスの各要素を新しいフォームに射影する。 Select 100%
Select-ManySequence QSelectMany シーケンスの各要素を 列挙子に射影し、結果のシーケンスを 1 つのシーケンスに平坦化する。 SelectMany 100%
Get-SequenceEquality QSequenceEqual 等値比較演算子に従って 2 つのシーケンスが等しいかどうかを判断する。 SequenceEqual 0%
Get-Single QSingle 値のシーケンスの 1 つの特定の要素を返する。 Single 0%
Get-SingleOrDefault QSingleOrDefault 値のシーケンスの 1 つの特定の要素を返する。そのような要素が見つからない場合は既定値を返する。 SingleOrDefault 0%
Skip-Sequence QSkip シーケンス内の指定された数の要素をバイパスし、残りの要素を返する。 Skip<TSource> 0%
Skip-SequenceWhile QSkipWhile 指定された条件が満たされる限り、シーケンスの要素をバイパスした後、残りの要素を返する。 SkipWhile 0%
Get-Sum QSum 数値のシーケンスの合計を計算する。 Sum 0%
Find-CountOf QTake シーケンスの先頭から、指定された数の連続する要素を返する。 Take<TSource> 100%
Find-While QTakeWhile 指定された条件を満たされる限り、シーケンスから要素を返した後、残りの要素をスキップする。 TakeWhile 100%
Select-ThenBy QThenBy シーケンス内の後続の要素を昇順で配置する。 ThenBy 100%
Select-ThenByDescending QThenByDescending シーケンス内の後続の要素を降順で配置する。 ThenByDescending 100%
ConvertTo-Array QToArray 列挙子から配列を作成する。 ToArray<TSource> 100%
ConvertTo-Dictionary QToDictionary 列挙子から Hashtable を作成する。 ToDictionary 0%
ConvertTo-List QToList 列挙子から ArrayList を作成する。 ToList<TSource> 0%
ConvertTo-Lookup QToLookup 列挙子から Lookup を作成する。 ToLookup 0%
Join-Union QUnion 2 つのシーケンスの和集合を生成する。 Union 0%
Find-Sequence QWhere 述語に基づいて値のシーケンスをフィルタ処理する。 Where 100%
Invoke-Linq QRun クエリを単純に起動する。 - 100%
2012/12/05 現在、進捗率は 30%。なるべく今月中に一通り仕上げたいところですが、師走でもあり何かとありまして・・・ぼちぼち進められればと思いますです (^_^;)
2012/12/24 現在、進捗率は 48%。思ったより多い…。途中、今後に向けた仕込みもしていたためか、進捗は芳しくありません。ちょいちょい進めますです (^^ゞ

さてさて、実は最後に挙げた例ですが、PowerShell に LINQ するような小品を作りたいと思った場合、障害となるいくつかの特徴を持っています。ここから先は、ちょっとお堅い話になりますので、興味があればご覧ください~。




先人の成果を検証させていただく
2012/12/05 現在、Bing で、キーワード "PowerShell LINQ" を使った検索をしてみると、先人の方々が試行錯誤された結果がずらずらと見つかります。その結果、44 万件以上・・・。それは、PowerShell が動的型付け言語であるにも関わらず、元の静的型付けで Generics な I/F を使いやすくするような Wrapper であったり、Dynamic Query を使った統一的な表記で DB へのアクセスを可能にした、1 つの言語内 DSL であったり、と本当に多種多様です。

これらの中から、今回は検索結果の 1 ページ目に現れる、ある程度のクエリが定義された以下の 2 つについて検証させていただくことにしました。

LINQ for PowerShell
  • 2010/02 ごろに公開された Josh Einstein 氏作成のライブラリです。
  • *.psm1 形式になっていますので、ダウンロード後、Import-Module … し、インストールを行います。
  • 20 以上の LINQ 処理が定義されており、一通りの処理はこれだけでできるようになっています。
参考サンプルコードを引用します:
Import-Module LINQ

function Assert-AreEqual($Expected, $Actual) {
    if (@(Compare-Object $Expected $Actual -SyncWindow 0).Length) {
        $OFS = ','
        Write-Error "Assert-AreEqual Failed: Expected=($Expected), Actual=($Actual)"
    }
}

# Take
Assert-AreEqual -Expected @(1..3)   -Actual @(1..5 | Linq-Take 3)

# TakeWhile
Assert-AreEqual -Expected @(1..2)   -Actual @(1..5 | Linq-TakeWhile { $_ -lt 3})

# Repeat
Assert-AreEqual -Expected @(1..5)   -Actual @(1..5 | Linq-Repeat 1)
 

PowerShell で LINQ - NyaRuRuが地球にいたころ
  • NyaRuRu 氏解説の日本語による記事です。2008/12 ごろに公開ということは、なんと今から 4 年も前!
  • ダウンロードファイル等はないようですので、適当に *.psm1 や *.ps1 としてコピペして使うことになります。
  • 定義されている LINQ 処理は 5 つ程ですが、元々ある機能も十分強力ですので、組み合わせて使うことを想定されているのでしょう。
参考サンプルコードを引用します:
# win.ini のダンプ
xrepeat {new-object IO.StreamReader 'c:\windows\win.ini'} `
  | xselect { $_.ReadLine() } `
  | xtakewhile { $_ -ne $null } `
  | Out-Default 

#1 桁の乱数を 100 個得る
xrepeat {new-object Random} `
  | xselect { $_.Next(0,10) } `
  | xtake 100 `
  | Out-Default 
 




ん?何が違う?
サンプルが異なると、パッと見同じ雰囲気に見えますね。ですので、先ほどの FizzBuzz 問題をどのライブラリでも扱えるよう変形して、処理を揃えてみましょうか。
PS C:\> function NewCounter {
>>         New-Object psobject |
>>             Add-Member NoteProperty m_counter 0 -PassThru |
>>             Add-Member ScriptMethod Increment `
>>             {
>>                 $this.m_counter++
>>                 $this.m_counter
>>             } -PassThru
>>      }
>>
 
無限リストのためのカウンターを生成する補助関数です。どの例からでも参照できるように最初に定義しておきましょう。
これを使った私のものの例はこうなります。
PS C:\> $query = QRepeat { NewCounter } |
>>                 QSelect { $_.Increment() } |
>>                 QSelect {
>>                     switch ($_) {
>>                         { $_ % 15 -eq 0 } { 'Fizz Buzz'; break }
>>                         { $_ % 3 -eq 0 } { 'Fizz'; break }
>>                         { $_ % 5 -eq 0 } { 'Buzz'; break }
>>                         default { $_ }
>>                     }
>>                 } |
>>                 QTake 36
>>
PS C:\> ($query | QToArray) -join ', '
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, Fizz Buzz, 16, 17, Fizz, 19, Buzz, F
izz, 22, 23, Fizz, Buzz, 26, Fizz, 28, 29, Fizz Buzz, 31, 32, Fizz, 34, Buzz, Fizz
 

Einstein 氏のライブラリを使うと、こんな感じの表現になります。
PS C:\> $query = NewCounter |
>>                 Linq-Repeat (40) |
>>                 Linq-Select { $_.Increment() } |
>>                 Linq-Select {
>>                     switch ($_) {
>>                         { $_ % 15 -eq 0 } { 'Fizz Buzz'; break }
>>                         { $_ % 3 -eq 0 } { 'Fizz'; break }
>>                         { $_ % 5 -eq 0 } { 'Buzz'; break }
>>                         default { $_ }
>>                     }
>>                 } |
>>                 Linq-Take 36
>>
PS C:\> $query -join ', '
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, Fizz Buzz, 16, 17, Fizz, 19, Buzz, F
izz, 22, 23, Fizz, Buzz, 26, Fizz, 28, 29, Fizz Buzz, 31, 32, Fizz, 34, Buzz, Fizz
 
2 行目の Linq-Repeat、上限が 40 に制限されていますが、これを元の問題であったような無限リストに近づけようと、[int]::MaxValue にすることはできません。
スクリプトが終わらなくなってしまうからです。
PowerShell v2 でよく問題として挙げられる挙動に、Select-Object コマンドレットの -First パラメータを指定しても、パイプラインが止まらないというものがあると思いますが、これも同様の問題を抱えているようです。

NyaRuRu 氏のライブラリの場合は、こんな感じでしょうか。
PS C:\> $query = $(do {
>>                 xrepeat { NewCounter } |
>>                 xselect { $args[0].Increment() } |
>>                 xselect {
>>                     switch ($args[0]) {
>>                         { $_ % 15 -eq 0 } { 'Fizz Buzz'; break }
>>                         { $_ % 3 -eq 0 } { 'Fizz'; break }
>>                         { $_ % 5 -eq 0 } { 'Buzz'; break }
>>                         default { $_ }
>>                     }
>>                 } |
>>                 xtake 36
>>      } until ($true))
>>
PS C:\> $query -join ', '
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, Fizz Buzz, 16, 17, Fizz, 19, Buzz, F
izz, 22, 23, Fizz, Buzz, 26, Fizz, 28, 29, Fizz Buzz, 31, 32, Fizz, 34, Buzz, Fizz
 
Einstein 氏のライブラリと比べると、無限リストであることは良いのですが、全体が do { } until (...) で囲まれるようになりました。残念ながら、これを外すことはできません。
スクリプトが途中で終了してしまうからです。
元の xtake の中に break 文を見つけることができますが、本来これは foreach/for/while/do/switch 文をただちに終了するためのものです。break の外側にこれらの囲いが無い場合、容赦なくスクリプトを止めてしまうのですね。

あとは共通した問題として、1 行目の $query が遅延評価されていない、というものがあります(どちらの例でも、Take に与える数を増やした分だけ、最初の式の実行に時間がかかるようになります)。元々の LINQ では、foreach に引き渡したり、スカラー値を戻すメソッド(All や Cout、ToArray など)を呼ばない限りは、実際の計算を先延ばしにすることができました。
しかしながら、PowerShell の動きとして、IEnumerable や IEnumerator を実装したオブジェクトは、ありとあらゆる場所で先行評価が試みられます。そのポイントは、-eq 演算子の左辺への指定や、ValidateNotNull 検証属性が付与された関数への引き渡し、変数への格納のタイミングなど、本当にいたるところでその対象になってしまいます。よほど気を付けていないと回避はできません。LINQ で連鎖させることになっている IEnumerable オブジェクトは、扱うのが困難という結論になってしまうのです。




どうやって解決を?
「間接法をもう一段増やせば解けない問題はない.―Butler Lampson」ですね。先行評価されないものを一枚被せれば良いのです。
構文的・ライブラリのサポートが充実しているということから、私は ScriptBlock を被せてみました。さらに、これなら最後の評価文を do { } until (...) で囲むことで、中で break しても安全に実行できるといううれしいオマケも付いてきます。最初の例で使った 3 つのクエリの中身を覗いてみましょう。
function New-Range {
    [CmdletBinding()]
    [OutputType([scriptblock])]
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [int]
        $Start,

        [Parameter(Position = 1, Mandatory = $true)]
        [int]
        $Count
    )
    
    if (($Count -lt 0) -or ([int]::MaxValue -lt ($Start + $Count - 1))) {
        $exParamName = '$Count'
        $exName = 'ArgumentOutOfRangeException'
        $ex = New-Object $exName -ArgumentList $exParamName
        throw $ex
    }

    {
        [CmdletBinding()]
        param (
            [switch]
            $WithSpecialInvoker = $(throw New-Object Urasandesu.PSAnonym.Linq.InvalidInvocationException)
        )
        
        $Start = $Start
        $Count = $Count
        
        for ($index = $Start; $index -lt $Start + $Count; $index++) {
            ,$index
        }
        
    }.GetNewClosure()
}

New-Alias QRange New-Range
 
function Select-Sequence {
    [CmdletBinding()]
    [OutputType([scriptblock])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [scriptblock]
        $InputObject,

        [Parameter(Position = 0, Mandatory = $true)]
        [scriptblock]
        $Selector
    )

    {
        [CmdletBinding()]
        param (
            [switch]
            $WithSpecialInvoker = $(throw New-Object Urasandesu.PSAnonym.Linq.InvalidInvocationException)
        )
        
        $InputObject = $InputObject
        $Selector = $Selector
        
        & $InputObject -WithSpecialInvoker | 
            ForEach-Object {
                , (& $Selector.GetNewClosure() $_)
            }

    }.GetNewClosure()

}

New-Alias QSelect Select-Sequence
 
function ConvertTo-Array {
    [CmdletBinding()]
    [OutputType([scriptblock])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [scriptblock]
        $InputObject
    )
    
    @(do {
        & $InputObject -WithSpecialInvoker | 
            ForEach-Object {
                $_
            }
    } until ($true))

}

New-Alias QToArray ConvertTo-Array
 
QRange/QSelect が返す ScriptBlock 内では、関数内の変数を参照する必要がありますので、GetNewClosure で環境を保存しておきます。また、決まった実行方法で実行しないとスクリプトが途中で終了してしまうことがありますので、それを防ぐために返した ScriptBlock の呼び出し方法を若干面倒にしてみました。QToArray では、実行する ScriptBlock を do { } until (...) で囲み、配列に変換しているという寸法です。
これで、基本的に同様の idiom で、これまでの問題は解決できることになります。つまり、全ての LINQ クエリを定義することができるようになったのです・・・はい、後は力業ですね (`・ω・ )ゝ




終わりに
PowerShell Advent Calendar 2012 の 5 日目は、PowerShell に LINQ するライブラリを再考してみました。いかがでしたか?たまには振り返るのも良いかな、と。Windows 8 の新基盤 WinRT で、また COM の知識が見直されたりしていますしね。

PowerShell、個人的には、Windows 7 以降、標準で搭載されている強力な統合開発環境(PowerShell ISE)や、.NET Framework との親和性の高さなどもあり、Windows 環境においては、最強クラスのスクリプト言語の 1 つだろうと感じています。
この記事が、少しでも PowerShell 遣いの方々のお役に立てば、また、他の .NET 系言語で LINQ を知った方が、PowerShell をちょっとでも触ってみようかと思っていただければ幸いに思いますです。

さあ、引き続き PowerShell Advent Calendar 2012 楽しみたいと思います。明日の担当は @twit_ahf さんです。
よろしくどうぞ~(´ー`~)