ブラウザ上で描いた絵をTwitterで共有するWebアプリを作りました

ピクエスト(α)

MSCC 提出作品です。
現在α版としてテスト公開中です。

ピクエスト(α)

  • リクエストされたテーマに沿って絵を描きます。
  • 描いた絵を画像付きツイートとしてTwitterへ投稿します。
  • リクエストは、ユーザーが自由に作成して追加することが出来ます。

f:id:pierre3:20150115220322p:plain

描いた絵はTwitterで共有

画像付きツイートとして投稿しますので、フォロワーさんのタイムライン上に描いた絵を表示させることが出来ます。

f:id:pierre3:20150116220450p:plain

Twitterとの連携について

リクエスト一覧内で[リクエストに応える]ボタンをクリックするとTwitterの認証画面に移動します。ここで、

  • Twitterの使用を許可した場合は、許可したユーザーのアカウントで投稿します。
  • 許可せず「キャンセル」した場合は、Piquestの公式アカウント @piquest で投稿します。
    • 但し、Twitterにログインしていない状態で「キャンセル」しても「ユーザー名、パスワードに誤りが..」と表示され、再度ログインを求められてしまいます。
      (Piquest側に戻ってくれません。したがって、連携を許可しない場合でも、絵の投稿にはTwitterのアカウントが必須となります。この辺どうにかしたい)

ASP.NET MVC

このWebアプリケーションは、ASP.NET MVC5で実装し、Azure Webサイト上で動いています。

Webアプリとしての基本的な動作は、ほとんどASP.NETが面倒を見てくれるため、この部分のコーディング量は驚くほど少なくて済みました。

CoreTweet

また、Twitterとの連携機能にはCoreTweetを使用させていただきました。こちらも数行書き加えるだけの簡単なお仕事でした。ありがとう CoreTweet。

HTML Canvas + TypeScript

お絵かきツール部分は、HTML Canvasの操作をTypeScriptで記述しました。
機能的には、最低限の物しか実装できていませんが、コーディング量としてはここが一番大きく、時間もかかりました。

TypeScriptは初めて触りましたが、思った以上に違和感なく使えていい感触でした。

最後に

機能的にはまだまだ貧弱で、間に合わせた感は否めませんが、コンテスト後も少しずつ手を加えて改良して行きたいと思います。

どなたか、使ってみてくだされ~

gooラボ 形態素解析APIを試してみる

これはPowerShell Advent Calendar 2014 : ATND 13日目の記事です。

PowerShellREST API

近年、RESTfullなAPIで提供されるWebサービスが非常に増えているように感じます。

そんな中、ちょっと気になるAPIがあったら、気軽に試したりしたいですよね。

PowerShellを使えば、お手軽にREST API がお試しできますよ!」

という事で、最近見かけた、gooラボ形態素解析APIを試してみたいと思います。

NTTレゾナント、gooで開発・蓄積した「日本語解析API」を公開、ビッグデータ解析機能なども提供予定:CodeZine

と、4種類のAPIが提供されています。
この中から今回は形態素解析APIを使います。
上の記事の内容を見る限りですと、文を語句に分割するだけ?のように思えますが、「PowerShell, 名詞, パワーシェル」の様に、分割した「語句」の「品詞(形態素)」と「読み」を付けて返してくれます。

形態素解析API:日本語文字列を語句に分割する技術 - gooラボ

Invoke-RestMethod でPOST

ではAPIを早速試してみます。APIの説明には

  • リクエスト先URL
    https://labs.goo.ne.jp/api/morph
  • リクエストパラメータ
    application/x-www-form-urlencoded、application/json形式でのPOSTを受け付けます。

とあります。
Invoke-RestMethodコマンドレットで試してみます。今回はapplication/jsonでPOSTします。

リクエストパラメータ

  • app_id アプリケーションID。
    必須項目。 こちら(https://labs.goo.ne.jp/apiusage/)で取得した、アプリケーションIDを設定します。(IDの取得にはGitHubアカウントが必要になります。)
  • request_id リクエストID。
    省略時は”labs.goo.ne.jp[タブ文字]リクエスト受付時刻[タブ文字]連番”となります。
  • sentence 解析対象テキスト。
    必須項目。
  • info_filter 形態素情報フィルタ。
    form(表記)、pos(形態素)、read(読み)のうち、出力する情報を文字列で指定します。
  • pos_filter 形態素品詞フィルタ。
    出力対象とする品詞を”|”で区切って指定します。

リクエストパラメータは、HashTebleで作成し、Invoke-RestMethodBodyパラメータに指定します。
今回は、とりあえず必須項目のapp_idsentence のみを使用します。

PS C:\> $text = "Windows PowerShell は、マイクロソフトが開発した拡張可能なコマンドラインインターフェース (CLI) シェルおよびスクリプト言語である。" + `
"オブジェクト指向に基づいて設計されており、.NET Framework 2.0 を基盤としている。"

PS C:\> $res = Invoke-RestMethod -Method Post `
  -Uri https://labs.goo.ne.jp/api/morph `
  -Body @{ 
    app_id = "your application id";
    sentence = $text; 
  }

レスポンスの取得

レスポンスパラメータは、以下の通りです。

  • request_id リクエストID。
    リクエストと同じ値となります。
  • info_filter 形態素情報フィルタ。
    入力と同じ値となります。
  • pos_filter 形態素品詞フィルタ。
    入力と同じ値となります。
  • word_list 形態素リスト。
    形態素リストは文ごとに分かれた文単位形態素リストの配列となります。
    文単位形態素リストは形態素情報の配列で、形態素情報には表記・形態素・読みのうち形態素情報フィルタで指定された要素が含まれます。
    例:
    • 態素リスト: [[[“文”],[“1つ”],[“。”]],[[“文”],[“2つ”],[“。”]]]
    • 文単位形態素リスト: [[“文”],[“1つ”],[“。”]]
    • 形態素情報: [“文”]

Invoke-RestMethod では、JSONで帰ってきたレスポンスパラメータをPSObjectにデシリアライズして返してくれます。大変便利ですね。

PS:\> $res

request_id                              word_list                              
----------                              ---------                              
labs.goo.ne.jp    1418127883    0       {System.Object[] System.Object[] Sys...

解析結果の本体は、word_listにあります。展開してみましょう。

PS C:\> $res.word_list |%{$_}

Windows
åè©
ã¦ã£ã³ãã¼ãº
 
空ç½
ï¼
Power
åè©
ãã¯ã¼
Shell
åè©
ã·ã§ã«

...

ん!? 文字化けしちゃってますね

レスポンスのエンコーディングについて

Invoke-RestMethod コマンドレットは、レスポンスヘッダ(Content-Type ;charset?) を見てエンコーディングしてくれるようなのですが、 このAPIのレスポンスヘッダにはcharsetの指定が無いようなのです。
その場合ってどうなるの?

以下にその答えがありました。
https://social.technet.microsoft.com/Forums/windowsserver/en-US/d795e7d2-dcf1-4323-8e06-8f06ce31a897/bug-invokerestmethod-and-utf8-data?forum=winserverpowershell

どうやらcahrsetの指定がない場合は ISO-8859-1エンコーディングされるらしい。

確認してみましょう。生のレスポンスが必要なのでInvoke-RestMethodではなく、Invoke-WebRequest コマンドの方を使います。
レスポンスの Contentに格納されている文字列を一旦Byte列に戻してからUtf-8エンコーディングし直してみます。

PS C:\> $res = Invoke-WebRequest -Method Post `
  -Uri https://labs.goo.ne.jp/api/morph `
  -Body @{ 
    app_id = "your application id";
    sentence = $text; 
  }

PS C:\> $res.Content

{"request_id":"labs.goo.ne.jp\t1418129359\t0","word_list":[[["Windows","åè©"
,"ã¦ã£ã³ãã¼ãº"],[" ","空ç½","ï¼"],["Power","åè©","ãã¯ã¼"],["Sh
ell","åè©","ã·ã§ã«"],[" ","空ç½","ï¼"],["ã¯","é£ç¨å©è©","ã"],["
...

PS C:\> [System.Text.Encoding]::Utf8.GetString( `
  [System.Text.Encoding]::GetEncoding("ISO-8859-1").GetBytes($res.Content))

{"request_id":"labs.goo.ne.jp\t1418129359\t0","word_list":[[["Windows","名詞","ウィ
ンドーズ"],[" ","空白","$"],["Power","名詞","パワー"],["Shell","名詞","シェル"],[" ","空白","$"],
["は","連用助詞","ハ"],["、","読点","$"],["マイクロソフト","名詞","マイクロソフト"],["が","格助詞","ガ"],["開発
...

おおー、ちゃんと読めるようになりました!

なお、レスポンスのクラスにはRawContentStream というプロパティがあり、ここからエンコード前のByte配列が取れるので、 こちらを使った方が1手間減って良いかもしれません。

PS C:\>$data = [System.Text.Encoding]::Utf8.GetString($res.RawContentStream.GetBuffer()))

データをもう少し扱いやすくする

解析結果の本体が格納された word_list は配列が入れ子になっていて扱いづらいので、少し手を加えます。

# 以下のように入れ子になっているのを
単語(n) = [form(表記), pos(形態素), read(読み)]
文(n) = [単語(1), 単語(2), 単語(3),...]
word_list = [文(1), 文(2), 文(3),...]

# 以下のように平坦化します
word_list = [@{SentenceNum(文番号), Form(表記), Pos(形態素), Read(読み)},
 @{SentenceNum(文番号), Form(表記), Pos(形態素), Read(読み)},
 @{SentenceNum(文番号), Form(表記), Pos(形態素), Read(読み)},
 @{SentenceNum(文番号), Form(表記), Pos(形態素), Read(読み)},...]
PS C:\> $obj = $data | ConvertFrom-Json | %{$_.word_list} | % -Begin{ $n=0 } {  
  $_ | % { [PSCustomObject]@{ SentenceNum=$n; Form=$_[0]; Pos=$_[1]; Read=$_[2] } }; $n++;
}

PS C:\> $obj

 SentenceNum Form                Pos                 Read               
 ----------- ----                ---                 ----               
           0 Windows             名詞                  ウィンドーズ             
           0                     空白                  $                  
           0 Power               名詞                  パワー                
           0 Shell               名詞                  シェル                
           0                     空白                  $                  
           0 は                   連用助詞                ハ                  
           0 、                   読点                  $                  
           0 マイクロソフト             名詞                  マイクロソフト            
           0 が                   格助詞                 ガ                  
           0 開発                  名詞                  カイハツ               
           0 し                   動詞活用語尾              シ                  
           0 た                   動詞接尾辞               タ                  
           0 拡張                  名詞                  カクチョウ              
           0 可能                  名詞                  カノウ                
           0 な                   判定詞                 ナ                  
           0 コマンド                名詞                  コマンド               
           0 ライン                 名詞                  ライン                
           0 インターフェース            名詞                  インターフェース           
           0                     空白                  $                  
           1 (                   括弧                  $                  
           1 CLI                 Alphabet            シーエルアイ             
           1 )                   括弧                  $                  
           1                     空白                  $                  
           2 シェル                 名詞                  シェル                
           2 および                 連体詞                 オヨビ                
           2 スクリプト               名詞                  スクリプト              
           2 言語                  名詞                  ゲンゴ                
           2 で                   判定詞                 デ                  
           2 あ                   動詞語幹                ア                  
           2 る                   動詞接尾辞               ル                  
           2 。                   句点                  $                  

これで大分扱いやすくなったと思います。
後の集計などは、PowerShellならお手の物ですよね。

名詞のみを抽出する

PS C:\> $obj | where {$_.Pos -eq "名詞"} | %{$_.Form}
Windows
Power
Shell
マイクロソフト
開発
拡張
可能
コマンド
ライン
インターフェース
シェル
スクリプト
言語
オブジェクト
指向
設計
NET
Framework
基盤

形態素(品詞)のランキング

$obj | Group-Object Pos | Sort-Object count -Descending

Count Name        Group                                                       
----- ----        -----
   19 名詞        {@{SentenceNum=0; Form=Windows; Pos=名詞; Read=ウィンドーズ}, @{SentenceNum=0; Form=Power; Pos=名詞; Read=パワー}, @{SentenceNum=0; Form=Shell; Pos=名詞; Read=シェル}, @{SentenceNum=0; Form=マイク...
    7 空白        {@{SentenceNum=0; Form= ; Pos=空白; Read=$}, @{SentenceNum=0; Form= ; Pos=空白; Read=$}, @{SentenceNum=0; Form= ; Pos=空白; Read=$}, @{SentenceNum=1; Form= ; Pos=空白; Read=$}...}       
    7 動詞接尾辞   {@{SentenceNum=0; Form=た; Pos=動詞接尾辞; Read=タ}, @{SentenceNum=2; Form=る; Pos=動詞接尾辞; Read=ル}, @{SentenceNum=3; Form=て; Pos=動詞接尾辞; Read=テ}, @{SentenceNum=3; Form=れ; Pos=動詞接尾辞; Rea...
    4 格助詞      {@{SentenceNum=0; Form=が; Pos=格助詞; Read=ガ}, @{SentenceNum=3; Form=に; Pos=格助詞; Read=ニ}, @{SentenceNum=3; Form=を; Pos=格助詞; Read=ヲ}, @{SentenceNum=3; Form=として; Pos=格助詞; Read=トシテ}}  
    4 動詞語幹    {@{SentenceNum=2; Form=あ; Pos=動詞語幹; Read=ア}, @{SentenceNum=3; Form=基づ; Pos=動詞語幹; Read=モトヅ}, @{SentenceNum=3; Form=お; Pos=動詞語幹; Read=オ}, @{SentenceNum=3; Form=い; Pos=動詞語幹; Read...
    3 動詞活用語尾 {@{SentenceNum=0; Form=し; Pos=動詞活用語尾; Read=シ}, @{SentenceNum=3; Form=い; Pos=動詞活用語尾; Read=イ}, @{SentenceNum=3; Form=さ; Pos=動詞活用語尾; Read=サ}}                                        
    3 句点        {@{SentenceNum=2; Form=。; Pos=句点; Read=$}, @{SentenceNum=3; Form=.; Pos=句点; Read=$}, @{SentenceNum=3; Form=。; Pos=句点; Read=$}}                                                    
    2 読点        {@{SentenceNum=0; Form=、; Pos=読点; Read=$}, @{SentenceNum=3; Form=、; Pos=読点; Read=$}}                                                                                              
    2 判定詞      {@{SentenceNum=0; Form=な; Pos=判定詞; Read=ナ}, @{SentenceNum=2; Form=で; Pos=判定詞; Read=デ}}                                                                                           
    2 括弧        {@{SentenceNum=1; Form=(; Pos=括弧; Read=$}, @{SentenceNum=1; Form=); Pos=括弧; Read=$}}                                                                                              
    1 連用助詞     {@{SentenceNum=0; Form=は; Pos=連用助詞; Read=ハ}}                                                                                                                                      
    1 Alphabet   {@{SentenceNum=1; Form=CLI; Pos=Alphabet; Read=シーエルアイ}}                                                                                                                           
    1 連体詞      {@{SentenceNum=2; Form=および; Pos=連体詞; Read=オヨビ}}                                                                                                                                   
    1 Number     {@{SentenceNum=3; Form=2.0; Pos=Number; Read=ニテンゼロ}}       

まとめ

(レスポンスのエンコーディングのところは予定外でしたが、) Invoke-RestMethodあるいはInvoke-WebRequest コマンドレット1つでWeb APIを試すことが出来ました。

返却されるJSON(やXML)のデータも直ぐにオブジェクトとして扱うことが出来るのも良いですね。

皆さんも、気になるWeb API がありましたら、PowerShellでお試ししてみましょう!

PowerShell で ToDo 管理

これはPowerShell Advent Calendar 2014 : ATND 12日目の記事です。

PowerShellでToDo管理するスクリプトモジュールを作りました。

元々、PowerShellスクリプトを書く練習用に作り始めたのですが、ちょっとしたメモなどを手軽に書き留めておけるので、割と重宝しています。

  • コマンド1つでToDoの追加、削除、一覧できます。
  • ToDoの項目毎にステータスを付けられます。
    • Todo,Doing(着手・進行中),Done(完了)
    • Milestone (期日・イベント事等、マイルストーン)
  • 特に重要な項目にフラグ!を付けることが出来ます。
  • ステータス、フラグの状態で色分け表示

f:id:pierre3:20141126143138p:plain

コードは、github に置いてあります。 posh-todo.psm1 だけ落とせば使用できます。 https://github.com/pierre3/posh-todo

使い方

使い方は、以下の実行例を参照ください。
ちなみに、PowerShell_ISE 上での使用を推奨しています。(PowerShell.exe の方でも使用は可能ですが、色分けがちゃんとできないです。)

完全に自分用に作ったものなので、他の人にとって使いやすいかどうかは微妙なところですが、気になった方(がもしいれば)是非試してみて下さい。

TIPS

以下、小ネタです。

PowerShell起動時にposh-todoを開始してTODO一覧を表示

PowerShell(ISE)のプロファイルに以下の2行を追加しておくと、起動時にposh-todoが開始されて登録済みのToDo一覧が表示されるようになります。

Import-Module path\to\posh-todo.psm1
Start-PoshTodo path\to\todoFile.json

単にToDo一覧を表示するだけでは味気ないので、ようこそ!的な挨拶文も表示したいですね。
以下をStart-PoshTodo 内で呼ぶようにします。

function Show-StartingMessage
{
    $today = (Get-Date).ToString("yyyy/MM/dd")
    Write-Host "こんにちは $env:USERNAME さん! 今日は $today です。"
    Write-Host ('{0}も{1}{2}{3}{4}ぞい!' -f ('今日', '1', '日', 'がん', 'ばる' | % { ($_,'ぞい')[(random 2)] }))
}

これでPowerShellを起動するたびに以下のように表示されるようになります。

f:id:pierre3:20141202215520p:plain

日付の入力はダイアログで

ToDo追加時のAdd-Todoコマンドで -setDate スイッチを指定すると、次のようなダイアログから日付を指定できるようになります。

f:id:pierre3:20141126150054p:plain

このダイアログは、「Hey, Scripting Guy Windows PowerShell を使用して日付を入力する手間を省く方法はありますか 」から頂きました。

一部手を加えて、ダイアログのタイトルを指定したり、Enterキーでダイアログを閉じれるようにしたりしています。
コントロールにイベントハンドラを追加するにはAdd_KeyPressのようなAdd_ + イベント名メソッドを使用するのですね。

ToDoデータは、.Netオブジェクトで保持してJSON 形式で保存

ToDoデータの扱い

ToDoデータは、後で扱うのに型があった方が何かと便利であろうと考え、.Netオブジェクトで保持するようにしました。

Add-Type -TypeDefinition @'
using System;
public enum TodoStatus
{
    Todo, Milestone, Doing, Done
}
public class TodoItem
{
    public int index { get; set; }
    public string text { get; set; }
    public DateTime date { get; set; }
    public bool flag { get; set; }
    public TodoStatus status { get; set; }
}
'@

保存はJSON

データは、ConvertTo-Json コマンドとConvertFrom-Jsonコマンドを使ってJSON形式で保存、復元します。
書き出し時は、TodoItemの配列をそのままConvertTo-Jsonに渡すだけで楽ちんなのですが、
読み取った結果はPSCustomObjectで帰ってくるため、TodoItemオブジェクトに格納するのにひと手間かかります。

# 書き出しはTodoItem の配列をそのまま渡すだけでOK
PS C:\> ConvertTo-Json $todo.items | Out-File $path -Encoding utf8

# 読み取った結果はPSCustomObjectの配列で帰ってくる。TodoItemに変換するのがメンドイ
PS C:\> obj = Get-Content $todo.filePath -Raw -Encoding UTF8 | ConvertFrom-Json
PS C:\> $todo.items = $obj | % { 
  $todoitem = New-Object 'TodoItem' 
  $todoitem.date = $_.date
  $todoitem.flag = $_.flag
  $todoitem.index = $_.index
  $todoitem.status = $_.status
  $todoitem.text = $_.text
  $todoitem
}

変換部分の記述がちょっと面倒です。.Net(TodoItem)側で何とかしたいですね。

明示的型変換を実装

という事で、明示的型変換を実装してキャスト一発で変換可能にしてみます。

Add-Type -TypeDefinition @'
using System;
using System.Management.Automation;
public enum TodoStatus
{
    Todo, Milestone, Doing, Done
}
public class TodoItem
{
    public int index { get; set; }
    public string text { get; set; }
    public DateTime date { get; set; }
    public bool flag { get; set; }
    public TodoStatus status { get; set; }
    public static explicit operator TodoItem(PSObject source)
    {
        return new TodoItem()
        {
            index = (int)source.index,
            text = (string)source.text,
            date = (DateTime)source.date,
            flag = (bool)source.flag,
            status = (TodoStatus)source.status
        }
    }
}
'@

これでOK!と思いきや、これではコンパイルが通りません。
変換元のPSObjectは、PowerShellの世界では動的に型を解決してくれますが、.Net側では「そんなメンバ定義されていませんよ」と怒られてしまいます。

インデクサでできない? index =(int)source["index"] ...これもダメでした。

.Net側でPSObjectのプロパティにアクセスするには?

.Net側ではProperties というメンバからプロパティの値を取得する必要があるようです。
source.Properties["propertyName"].Value のように記述します。
(このPropertiesですが、PowerShell側からは見えない(参照できない)ため、その存在に気付けず苦労しました。)

Add-Type -TypeDefinition @'
using System;
using System.Management.Automation;
public enum TodoStatus
{
    Todo, Milestone, Doing, Done
}
public class TodoItem
{
    public int index { get; set; }
    public string text { get; set; }
    public DateTime date { get; set; }
    public bool flag { get; set; }
    public TodoStatus status { get; set; }
    public static explicit operator TodoItem(PSObject source)
    {
        return new TodoItem()
        {
            index = (int)source.Properties["index"].Value,
            text = (string)source.Properties["text"].Value,
            date = (DateTime)source.Properties["date"].Value,
            flag = (bool)source.Properties["flag"].Value,
            status = (TodoStatus)source.Properties["status"].Value
        }
    }
}
'@

これで、無事コンパイルできるようになりました。キャストもちゃんと動きます。

# 読み取り処理もスッキリ!
$obj = Get-Content $todo.filePath -Raw -Encoding UTF8 | ConvertFrom-Json
$todo.items = $obj | % { [TodoItem]$_ }

# Todoの追加時も、HashTableのキャストでOK!
$todo.items += [TodoItem]@{
    index = 5;
    text = "todo";
    date = "2014/12/1";
    flag = false;
    status = [TodoStatus]::Todo;
}

PSObjectのプロパティにアクセスする方法の別解

Dynamicを使えばPSObjectのプロパティに.でアクセス可能になります。
但し、Dynamicを使用するには"Microsoft.CSharp"を参照アセンブリに指定してあげる必要があります。

public class TodoItem
{
    public int index { get; set; }
    public string text { get; set; }
    public DateTime date { get; set; }
    public bool flag { get; set; }
    public TodoStatus status { get; set; }
    public static explicit operator TodoItem(PSObject source){
        dynamic d = source;
        return new TodoItem(){
            index = (int)d.index,
            text = (string)d.text,
            date = (DateTime)d.date,
            flag = (bool)d.flag,
            status = (TodoStatus)d.status
        };
    }
}
'@ -ReferencedAssemblies "Microsoft.CSharp"

MSCCに参加登録しました

Microsoft Community Champion (MSCC) に勇者として登録しました。

Microsoft Community Champion -

残念ながら戦士と魔法使いが決まらぬままスタートしてしまいそうですが、一人でもがんばるぞい!

今回はWebアプリケーションに挑戦しようかと思っています。
これまで、Webアプリケーションにはあまり縁が無く、一からのチャレンジになりますが、リタイアだけはしないように頑張りたいと思います。

という事で現在、ASP.NET MVC 5 のチュートリアルを写経したり、サンプルを眺めたりしてべんきょー中

Getting Started with ASP.NET MVC 5 | The ASP.NET Site

こちらで紹介されていた本も気になります。

ASP,NET MVC5実践プログラミングを読みました - がりらぼ

PowerShellのパイプラインを遅延評価にしてみる

パイプラインからの入力をスクリプトブロックに包んで返せば、遅延評価にできるのでは? という思い付きのネタです。

例題

1から10までの整数列から偶数を抽出してその値を2乗するだけの簡単な処理を例に見てみます。

C#で書くとこんな感じ

var query = Enumerable.Range(1,10)
    .Where(n=> (n % 2) == 0)
    .Select(n=> n*n);
foreach(var n in query)
{
    Console.WriteLine(n);
}

/** output **
4
16
36
64
100
*/

PowerShell では以下のように書けます。

C#Select()に相当する処理はSelect-Object ではなくForEach-Objectを使いました。
また、このForEach-Objectは出力時のforeach ステートメントも兼ねています。

この一連の処理は遅延評価はされず、各コマンドレットへ値が渡った時点で即時実行されます。

PS C:\> 1..10 |
  Where-Object {($_ % 2) -eq 0 } |
  Foreach-Object { $_*$_ }

4
16
36
64
100

遅延評価版 Where、Select、Foreach

では、遅延評価のためのコマンドレットを作ってみます。
各コマンドレットの名前は Where-Block 、Select-Block、Foreach-Block とします。

Where-Block 、Select-Block はほぼ同じコードで、以下のような処理を行っています。

  • Where-Block では $Predicate パラメータに bool値を返すスクリプトブロックを指定します。
  • Select-Block では $Selector パラメータに 任意のオブジェクトを返すスクリプトブロックを指定します。
  • Process{}では、スクリプトブロックを返却します
  • スクリプトプロック内では以下の処理を行います
    • 入力がスクリプトブロックであれば、それを実行後、その戻り値を引数に$Predicate または $Selector を実行します。
    • 入力がスクリプトブロック以外であれば、その値を引数にPredicate または $Selector を実行します。
    • Where-Block では、$Predicate の戻り値が$true の場合のみ値を出力します。
    • Select-Block では、$Selector の戻り値をそのまま出力します。

Where-Block、Select-Block とコマンドをつなげて実行すると、つなげたコマンドの数だけネストしたスクリプトブロック(の配列)が得られます。 これを Foreach-Block で 一気に実行する、という仕組みです。

実行してみる

$Predicate,$Selector に渡ってくる引数はParam($x)でアクセスします。

PS C:\> $query = 1..10 | 
  Where-Block -Predicate {Param($x) ($x % 2) -eq 0} |
  Select-Block -Selector {Param($x) $x*$x}


PS C:\> $query | Foreach-Block
4
16
36
64
100

期待通りの結果にはなりました。が、本当に遅延評価になっているのかよくわかりません。
各コマンドレットに Write-Verbose を仕込んで確認してみましょう。

(Write-Verbose版のスクリプトはこちら https://gist.github.com/pierre3/c62db52d977bc2841ff4)

PS C:\> $query = 1..10 | 
  Where-Block -Predicate {Param($x) ($x % 2) -eq 0} -Verbose |
  Select-Block -Selector {Param($x) $x*$x} -Verbose


PS C:\> $query | Foreach-Block -Verbose
詳細: [Where] Input Data = 1
詳細: [Where] Predicate(1)
詳細: [Where] Input Data = 2
詳細: [Where] Predicate(2)
詳細: [Where] return 2
詳細: [Select] Predicate(2)
詳細: [Select] return 4
詳細: [Foreach] return 4
4
詳細: [Where] Input Data = 3
詳細: [Where] Predicate(3)
詳細: [Where] Input Data = 4
詳細: [Where] Predicate(4)
詳細: [Where] return 4
詳細: [Select] Predicate(4)
詳細: [Select] return 16
詳細: [Foreach] return 16
16
詳細: [Where] Input Data = 5
詳細: [Where] Predicate(5)
詳細: [Where] Input Data = 6
詳細: [Where] Predicate(6)
詳細: [Where] return 6
詳細: [Select] Predicate(6)
詳細: [Select] return 36
詳細: [Foreach] return 36
36
詳細: [Where] Input Data = 7
詳細: [Where] Predicate(7)
詳細: [Where] Input Data = 8
詳細: [Where] Predicate(8)
詳細: [Where] return 8
詳細: [Select] Predicate(8)
詳細: [Select] return 64
詳細: [Foreach] return 64
64
詳細: [Where] Input Data = 9
詳細: [Where] Predicate(9)
詳細: [Where] Input Data = 10
詳細: [Where] Predicate(10)
詳細: [Where] return 10
詳細: [Select] Predicate(10)
詳細: [Select] return 100
詳細: [Foreach] return 100
100

1行目の実行時には何も表示されず、Foreach-Block を実行した時点で一度に表示されました。
スクリプトブロックが順番に実行される様子も良く分かります。

スクリプトブロックにまとめる段階で一度 配列(コレクション)が展開されてしまうのは、ショウガナイ?

PowerShell で ジェネリック版コレクション IEnumerable<T>を列挙する場合の注意

パイプ経由でのコレクションの受け渡し方などを調べようと思い、以下のようなイテレータクラスを用意しました。

IEnumerable<T> および IEnumerator<T> インターフェースを実装して、インターフェースの各メソッドが実行されるとそのメソッドの名前を出力するようにしています。

まずは、C# で実行してみます。

[c#]
static void Main(string[] args)
{
    var ite = IteratorHelper.CreateIterator(100, 200, 300);
    Console.WriteLine("[foreach]");
    foreach (var s in ite)
    {
        Console.WriteLine("> " + s);
    }
    Console.WriteLine("[count]");
    var count = ite.Count();
    Console.WriteLine("count =" + count);
    Console.Read();
}

/*** Output ***
Constructor()
[foreach]
GetEnumerator()
MoveNext() == True
> 100
MoveNext() == True
> 200
MoveNext() == True
> 300
MoveNext() == False
Dispose()
[count]
GetEnumerator()
Constructor()
MoveNext() == True
MoveNext() == True
MoveNext() == True
MoveNext() == False
Dispose()
count =3
*/

foreach や、Count(),ToArray()などの列挙が走るLinqの拡張メソッドを実行すると、上記の通り
GetEnumerator()、MoveNext()、Dispose() が順に呼ばれるのが確認できます。

では、早速PowerShellで使ってみましょう。
手始めにforeachから

PS C:\> $ite = [IteratorTest.IteratorHelper]::CreateIterator(100,200,300)
Constructor()

PS C:\> foreach($item in $ite){$item}
GetEnumerator()
MoveNext() == True
100
MoveNext() == True
200
MoveNext() == True
300
MoveNext() == False

ん?なにか違和感が... というかC#でforeachした場合と明らかに違う箇所が!

IEnumerator<T>.Dispose() が呼ばれていない!

どうやらPowerShell のforeachは IEnumerator<T>.Dispose() を呼んでくれないようです。

PowerShell 的には、非ジェネリック版のIEnumerable,IEnumerator のみを想定しているという事でしょうか?

ForEach-Object は?

PS C:\> $ite | ForEach-Object {$_}
GetEnumerator()
Constructor()
MoveNext() == True
100
MoveNext() == True
200
MoveNext() == True
300
MoveNext() == False

呼ばれませんね。

.ForEach() メソッド?

PS C:\> $ite.ForEach({$_})
GetEnumerator()
Constructor()
GetEnumerator()
MoveNext() == True
MoveNext() == True
MoveNext() == True
MoveNext() == False
100
200
300

こちらもDispose()されません。
(Dispose()とは関係ありませんが、.ForEach()では列挙が先に走ってからスクリプトブロックに値が渡っているように見えます。 本来の目的としては、こういった挙動の違いを見たかったのです)

Dispose()が呼ばれないと問題になりそうなケース

PowerShellでは、通常であれば非ジェネリック版のコレクションを前提として問題なさそうなのですが、
.Net Framework のクラス等を使ってジェネリックなコレクションを扱う場合は注意しないとハマってしまいそうです。

たとえば、IEnumerable<string> を返す System.IO.File.ReadLines() メソッドでは
以下のように foreachがDispose()呼んでくれるおかげで明示的にファイルを閉じる必要がありません。

[C#]
IEnumerable<string> lines = System.IO.File.ReadLines();
//この時点では読み取り実行されていない。ファイルは開いたまま
foreach(var line in lines)
{
    Console.WriteLine(line);
}
//foreachを抜けたらDispose()が呼ばれてファイルが閉じられる
//途中でBreak;してもDispose()されます

PowerShellで試してみましょう。

$lines = [System.IO.File]::ReadLines("d:\test\test.txt")
foreach($line in $lines){$line}
line1
line2
line3
line4

ここで、Dispose()が呼ばれず、ファイルが開きっぱなしのはずなので、Remove-Item でエラーとなるはず。

PS C:\> Remove-Item D:\test\test.txt -Verbose
詳細: 対象 "D:\test\test.txt" に対して操作 "ファイルの削除" を実行しています。

あれ??普通に出来ちゃいました。どうなっているのでしょう??
ちょっと予想が外れてしまいましたが、気を取り直してMicrosoft Reference Sourceでソースコードを見てみます。

[C#]
internal class ReadLinesIterator : Iterator<string>
{
    //....

    public override bool MoveNext()
    {
        if (this._reader != null)
        {
            this.current = _reader.ReadLine();
            if (this.current != null)
                return true;
            // To maintain 4.0 behavior we Dispose 
            // after reading to the end of the reader.
            Dispose();
        }
        return false;
    }
}

File.ReadLines() が実際に返しているオブジェクトはReadLinesIterator というクラスなのですが、 そのMoveNext()メソッドを見てみると、自らDispose()を呼んでいるじゃないですか。
じゃあ、問題ない?

でも待ってください。これだけでは途中でBreakした場合にDispose()されないのでは?

先ほどのコードにBreakを入れて実行してみましょう。

PS C:\> $lines = [System.IO.File]::ReadLines("d:\test\test.txt")
PS C:\> foreach($line in $lines){$line; break}
line1

PS C:\> Remove-Item D:\test\test.txt -Verbose
詳細: 対象 "D:\test\test.txt" に対して操作 "ファイルの削除" を実行しています。
Remove-Item : 項目 D:\test\test.txt を削除できません: 別のプロセスで使用されているため、プロセスはファイル 'D:\test\test.txt' にアクセスできません。
発生場所 行:1 文字:1
+ Remove-Item D:\test\test.txt -Verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : WriteError: (D:\test\test.txt:FileInfo) [Remove-Item], IOException
    + FullyQualifiedErrorId : RemoveFileSystemItemIOError,Microsoft.PowerShell.Commands.RemoveItemCommand

今度は予想通りの結果になりました。ファイルが開いたままです。

まとめ

ジェネリック版のIEnumerable<T>を列挙する場合は、foreach に任せずに、GetEnumerator()- MoveNext()- Dispose()の一連の処理を自前で書いた方が良さそうです。

※ ただ、これを裏付けるような他の記事、ドキュメント等を見つけることが出来ず、いまいち確信が持てません。
有識者の方々のご意見を伺いたいところです。

hashtable をKey-Valueのコレクションとして使いたい

のですが、普通にforeach, ForeEach-Object しただけでは期待した動作になりませんでした。

  • 期待する結果
PS C:\> $items = @{aaa=111;bbb=222;ccc=333}
PS C:\> $items | ForEach-Object {"{0}={1}" -f $_.Key, $_.Value}
aaa=111
bbb=222
ccc=333
  • 実際は
PS C:\> $items = @{aaa=111;bbb=222;ccc=333}
PS C:\> $items | ForEach-Object {"{0}={1}" -f $_.Key, $_.Value}
=
  • 要素が列挙されず、Hashtableがそのまま渡されてる!
PS C:\> $items | ForEach-Object {$_.GetType().FullName}
System.Collections.Hashtable
  • System.Collrctions.IEnumerableが実装されているはずですよね?
PS C:\> $items.GetEnumerator().GetType().FullName
System.Collections.Hashtable+HashtableEnumerator

IEnumerable.GetEnumerator はちゃんとあります。

  • Enumeratorでforeachしてみる?
PS C:\> $items.GetEnumerator() | ForEach-Object {$_.GetType().FullName}
System.Collections.DictionaryEntry
System.Collections.DictionaryEntry
System.Collections.DictionaryEntry

あ!できた?

PS C:\> $items.GetEnumerator() | ForEach-Object {"{0}={1}" -f $_.Key, $_.Value}
aaa=111
bbb=222
ccc=333

おおお!できました。

  • でもGetEnumerator()つけるのなんかダサい気がする

調べてみると、以下のような書き方が一般的のようですね。

Hashtableをforeachしても・・・ - PowerShell Scripting Weblog

PS C:\> $items.Keys | ForEach-Object {"{0}={1}" -f $_, $items[$_]}
bbb=222
ccc=333
aaa=111

コマンドレットにパイプで渡すと?

  • テスト用のfunctionで見てみましょう
function Test-Parameter
{
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeLine=$true, Mandatory=$true)]
        [PSObject]
        $InputObject
    )
    Begin
    {
        "*** Begin ***"
    }
    Process
    {
        "*** Process ***"
        " - InputObject: {0}" -f $InputObject.GetType()
        if($InputObject -is [System.Collections.IEnumerable])
        {
            " - foreach {"
            foreach($o in $InputObject){"    {0}={1}" -f $o.GetType(),$o}
            " }"
        }
        else
        {
            " - InputObject: {0}={1}" -f $InputObject.GetType(),$InputObject
        }
    }
    End
    {
        "*** End ***"
    }
}
PS C:\> $items = @{aaa=111;bbb=222;ccc=333}
PS C:\> $items | Test-Parameter

*** Begin ***
*** Process ***
 - InputObject: System.Collections.Hashtable
 - foreach {
    System.Collections.Hashtable=System.Collections.Hashtable
 }
*** End ***

想像通りですが、パイプで渡しても列挙されずに、ProcessにはHashtableが1回だけ入ってきました。

  • GetEnumerator()してから渡してみる
PS C:\> $items.GetEnumerator() | Test-Parameter
*** Begin ***
*** Process ***
 - InputObject: System.Collections.DictionaryEntry
 - InputObject: System.Collections.DictionaryEntry=System.Collections.DictionaryEntry
*** Process ***
 - InputObject: System.Collections.DictionaryEntry
 - InputObject: System.Collections.DictionaryEntry=System.Collections.DictionaryEntry
*** Process ***
 - InputObject: System.Collections.DictionaryEntry
 - InputObject: System.Collections.DictionaryEntry=System.Collections.DictionaryEntry
*** End ***

ちゃんと要素数だけ DictionaryEntry が渡されてきました。

コレクションを処理するコマンドレットに、Hashtableをコレクションとして渡したいケースがあれば、 Hashtable.GetEnumerator() で渡すのもアリかも。

他にも

string 型もIEnumerable<char> を実装していますが、そのままでは列挙しません。

PS C:\> "abcdefg" | ForEach-Object {"{0}:{1}" -f $_.GetType().FullName,$_}
System.String:abcdefg

PS C:\> "abcdefg".GetEnumerator() | ForEach-Object {"{0}:{1}" -f $_.GetType().FullName,$_}
System.Char:a
System.Char:b
System.Char:c
System.Char:d
System.Char:e
System.Char:f
System.Char:g

まあ、string をchar配列として使いたいケースは稀ですし、使いたい場合はToCharArray()で変換可能ですので、あまり気にする必要はないかも。

PS C:\> "abcdefg".ToCharArray() | ForEach-Object {"{0}:{1}" -f $_.GetType().FullName,$_}
System.Char:a
System.Char:b
System.Char:c
System.Char:d
System.Char:e
System.Char:f
System.Char:g