Register-ArgumentCompleter で 入力補完機能を自作しよう!
これは、PowerShell Advent Calendar 2015 9日目の記事です。
目次
はじめに
以前、TabExpansion++ というモジュールを使うと、自前の入力補完機能を簡単に作ることが出来る、という内容の記事を書きました。
この入力補完のカスタマイズ機能ですが、PowerShell v5.0 から標準で使えるようになり、より手軽に使用できるようになっていました。
それに伴い、Register-ArgumentCompleterというコマンドレットが追加されていて、このコマンドレットを実行するだけで、自前の入力補完機能が登録できるようになったようです。
今回は、このRegister-ArgumentCompleterコマンドレットを使って、自前の入力補完機能を実装してみたいと思います。
Register-ArgumentCompleter コマンドレット
まずは、Register-ArgumentCompleter コマンドレットに渡すパラメータを確認してみましょう。
Register-ArgumentCompleter ` -CommandName (ターゲットコマンド名) ` -ParameterName (ターゲットパラメータ名) ` -ScriptBlock (入力候補一覧を作成するスクリプトブロック)
CommandName
パラメータに、補完機能のを登録するコマンドの名前を指定します。ParameterName
パラメータに、補完対象となるパラメータの名前を指定します。ScriptBlock
パラメータに、独自の入力候補一覧を作成する処理を記述したスクリプトブロックを指定します。(詳細は後述します)
入力候補の一覧を生成するスクリプトブロック
ScriptBlock
パラメータには、System.Management.Automation.CompletionResult
オブジェクトをコレクションで返すスクリプトブロックを指定します。
また、スクリプトブロックには、5つのパラメータが渡ってきて、入力途中の文字列などが取得できるようになっています。
スクリプトブロックの概要をざっくりと書くと以下の様になります。
$createCompletionResults = { # 以下の5つのパラメータを受け取る。入力中の文字列は $wordToComplete に入ってくる。 param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $items = (入力候補のソースを取得) # 入力候補の数だけ値を出力する foreach($item in $items) { $completionText = (決定時にパラメータ値として挿入される文字列); $listItemText = (入力候補の一覧に表示される文字列); $resultType = (入力候補のタイプ。候補の左側に表示されるアイコンを指定できる); $toolTip = (ツールチップに表示される文字列); # CompletionResult オブジェクトとして出力する [System.Management.Automation.CompletionResult]::new( $completionText, $listItemText, $resultType, $toolTip); } }
それでは、これを踏まえて実際に入力補完機能を作ってみましょう。
Invoke-Item コマンドレットの入力候補に「最近使った項目」の一覧を出す
Invoke-Item コマンドレットのPath
パラメータ入力時に「最近使った項目」の一覧を出して、最近使ったファイルに素早くアクセスできるようにしたいと思います。
Invoke-Item
コマンドレットは、指定したファイルを実行したり、フォルダを開いたりするのに使用するコマンドレットです。
エクスプローラ上で任意のファイルやフォルダをダブルクリックした時と同様の動作をします。「最近使った項目」は、
%AppData%\Microsoft\Windows\Recent
フォルダにショートカットとして保存されます。スタートメニューやタスクバーアイコンを右クリックすると表示される「最近使ったもの」一覧で利用されます。
実装
実装したコードは、以下の通りです。
細かな部分はコード内のコメントと、後述の補足説明をご確認ください。
「最近使った項目」フォルダからショートカットファイルの一覧を取得する
「最近使った項目」(Recent)フォルダは、[System.Environment]::GetFolderPath()
メソッドで取得できます。
取得したフォルダ以下のファイルは Get-ChildItem コマンドで取得しています。フォルダ名の下に\*.lnk
を付けることで、ショートカットファイル(.lnk)のみを取得するようにしています。
取得したファイルの一覧は、更新日時の新しい順にソート後、先頭から表示したい件数分(ここでは50件)を取得しています。
# Recentフォルダから、最新50件のショートカット(*.lnk)ファイルを取得 $recentFolder = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Recent); $items = Get-ChildItem "$($recentFolder)\*.lnk" | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 50;
Pathを直接入力する場合にも対応する
「最近使った項目」の一覧から選択だけでなく、Pathを直接入力したい場合にも対応するため、スクリプトブロックの冒頭で以下のコードを入れています。
# "\" が含まれる場合は、通常のPath入力とみなし、独自の入力補完は行わない # 例えば"C:\" と入力した場合など if(([string]$wordToComplete).Contains("\") ) { return; }
こうすることで、".\" や、"C:\" などと入力した場合は 通常のPathを補完するダイアログが表示されるようになります。
(スクリプトブロックが、なにも返さなかった場合は、通常の補完機能が動作することを利用しています。)
入力済みの文字列がマッチする項目だけを一覧に含める
パラメータに入力途中の文字がある状態で、補完機能が実行された場合、「補完一覧に表示する文字列」に「入力済みの文字列」がマッチする項目のみを一覧に含めるようにしています。
# 一覧に表示する文字列 $listItemText = Split-Path $shortcut.TargetPath -Leaf; # 入力中の文字列$wordToCompleteがマッチしない項目はスキップ if($listItemText -notmatch $wordToComplete) { continue; }
ショートカットの情報はWSHのCreateShortCut()で取得
どうやらショートカットファイルを操作するPowerShellのコマンドはないようなので ショートカットの参照元を取得するために、WSHの機能を使用しています。
$wsh = new-object -comobject WScript.Shell $shortcut = $Wsh.CreateShortcut("path\to\shortcut"); # 参照元のパス $shortcut.TargetPath;
実行結果
Invoke-Item の第1引数(-Path)で [Ctrl]+[Space](「最近使った項目」の一覧を表示)
"ab" を入力後 [Ctrl]+ [Space] (「最近使った項目」で名前に "ab" を含むファイルのみ表示)
"c:\" を入力 (c ドライブのアイテム一覧を表示)
ちゃんと、期待通りの動きになっているようです。
まとめ
Register-ArgumentCompleterコマンドレットを使う事で、自前の入力補完機能が簡単に実装できることが分かりました。
この機能が必要となるケースはそれほど多くはないかもしれませんが、使い方によっては非常に有用な機能となり得るのではないでしょうか。
おまけ
Invoke-Item でファイルを開いても、「最近使った項目」が更新されない問題が発覚しました。
どうやら、エクスプローラや標準のファイル選択ダイアログを使わないと「最近使った項目」は更新されないようです。
これはあまり嬉しくないので、Invoke-Item コマンドを実行後に、「最近使った項目」を更新する処理を追加したコマンドレットを作って、これを使う様にしました。
function Invoke-ItemEx { [CmdletBinding()] [Alias("iix")] Param ( [Parameter(Mandatory=$true, Position=0)] [string] $Path ) if(-not(Test-Path $Path)) { Write-Error -Exception ([System.ArgumentException]::new( "存在するファイル名またはフォルダ名を指定してください","Path")); return; } Invoke-Item -Path $Path; # ファイルのみ if(Test-Path $Path -PathType Leaf) { $Shell = New-Object -ComObject Shell.Application; $Shell.AddToRecent($Path); } } Register-ArgumentCompleter -CommandName Invoke-ItemEx -ParameterName Path ` -ScriptBlock $recentItemsCompletion
- 「最近使った項目」への登録は Shell.Application の AddToRecent()を使います。
- AddToRecent()に、アクセスしたファイルのPathを渡して実行すると、Recentフォルダに以下のショートカットが登録されます。
- 引数に渡したファイルのショートカット
- そのファイルを格納しているフォルダのショートカット
- AddToRecent()にフォルダのPathを渡すと、その1階層上のフォルダのショートカットが登録されてしまいます。
- これは期待する動作ではないので、AddToRecent()にはファイルのPathのみを渡すようにしました。
PowerShellでEtwStream
EtwStream.PowerShell
neuecc/EtwStream をPowerShellから使えるようにしてみました。
Get-TraceEventStream Cmdlet
現在は、このコマンドのみ実装されています。
Get-TraceEventStream [-NameOrGuid] <string> [-DumpWithColor] [-TraceLevel <TraceEventLevel> { Always | Critical | Error | Warning | Informational | Verbose }] [<CommonParameters>] Get-TraceEventStream [-WellKnownEventSource] <string> { AspNetEventSource | ConcurrentCollectionsEventSource | FrameworkEventSource | PinnableBufferCacheEventSource | PlinqEventSource | SqlEventSource | SynchronizationEventSource | TplEventSource} [-DumpWithColor] [-TraceLevel <TraceEventLevel> { Always | Critical | Error | Warning | Informational | Verbose}] [<CommonParameters>]
Object pipeline
出力は、オブジェクトで流れてきますので、PowerShellのコマンドを使ったフィルタリングや加工も可能です。
出力されるオブジェクトは、PSTraceEvent というMicrosoft.Diagnostics.Tracing.TraceEvent
をラップした独自クラスとしています。
EtwStream で実装されている TraceEventの拡張メソッドが使えるようになっています。
public string Dump(bool includePrettyPrint = false, bool truncateDump = false); public void DumpPayload(); public string DumpPayloadOrMessage(); public ConsoleColor? GetColorMap(bool isBackgroundWhite); public void DumpWithColor(bool withProviderName = false, bool withProcesName = false);
DumpWithColor Switch
DumpWithColor スイッチで、色付きの簡易ビューアになります。
PS C:\> Get-TraceEventStream -NameOrGuid SampleEventSource -DumpWithColor
WellKnownEventSource
WellKownEventSource パラメータで、既知のEventSource のGUID を一覧から指定できます。
View in GridView-Window
Out-GridView に流すと、GUI上でソートやフィルタリングが自在にできるリッチなEventビューワにもなります。
PS C:\> Get-TraceEventStream SampleEventSource | Out-GridView
実装
コマンドレットの実装は、こちらの記事を参考にさせて頂きました。
参考というか、コードのベース部分は、ほぼそのまま流用させて頂きました。
変更したのは、ソースのIObservableをEtwStream.ObservableEventListener.FromTraceEvent()
で取得するようにした事と、
StopProcessing() メソッドをオーバーライドして Ctrl+Break による中断に対応した事位です。
まとめ
ブログのネタになればと、試しに作ってみましたが、予想以上に使えそうな予感がします。
LINQPadのライセンスが無かったり、そもそも職場でフリーソフトのインストールが制限されていて使えない!といったような方の代替手段として使えるかも??
PowerShell の入力補完にGoogleサジェストの結果を表示する
↑ みたいな事って出来るのかな、となんとなく調べてみたところ、どうやら TabExpansion++ というモジュールを使うと簡単に出来そう!
という事で、ちょっと試してみました。
TabExpansion++
PowerShellのTab補完、インテリセンスをより賢く、便利にするモジュールで、
コンテキストに応じた入力候補を動的に生成してくれるようです。
さらには、入力候補の生成処理を自作して組み込むことが出来るとのことで、今回はこの機能を使ってGoogleサジェストの結果をインテリセンスに表示させてみたいと思います。
インストール
iex (new-object System.Net.WebClient).DownloadString('https://raw.github.com/lzybkr/TabExpansionPlusPlus/master/Install.ps1')
でダウンロードして、Install.ps1 を実行するか、PsGet がインストールされていれば以下でインストールできます。
Install-Module -ModuleUrl https://github.com/lzybkr/TabExpansionPlusPlus/zipball/master/ -ModuleName TabExpansion++ -Type ZIP
後は、プロファイルに Import-Module TabExpansion++
を追加しておけば準備完了です。
Search-Google コマンドレット
まずは、Google でWeb検索するコマンドレットを作成します。
パラメータ($SearchWords
)に検索ワードを渡すと、検索クエリ付のURLを生成してブラウザに投げるだけの簡単なものです。
Search-Google -$SearchWords あ
のように入力して、[Tab]または[Ctrl]+[Space]を押すと、
Googleサジェストから取得した入力候補が表示されるようになればOKです。
入力候補を動的に生成する関数の定義
以下の要件を満たした関数を定義するだけで、自作の入力補完処理が使用できるようになります。
(すごい!)
関数に ArgumentCompleter 属性を付ける。
- ここで、入力補完のターゲットとしたいコマンドとそのパラメータ名を指定します。
- コマンドは配列で複数指定することが可能です。
入力パラメータは以下の様に定義する。
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
$wordToComplete
に補完対象となる入力中の文字列が入ってきます。
System.Management.Automation.CompletionResult
クラスのオブジェクトを出力するNew-ComoletionResult
コマンドを使用して生成します。引数には順に以下を渡します。- CompletionText:補完結果の文字列
- ToolTip: ツールチップに表示される文字列(省略可。省略時は第1引数と同じになる)
- ListItemText: 入力候補一覧に表示される文字列(省略可。省略時は第1引数と同じになる)
- CompletionResultType : 入力候補の種別 (一覧の左側に表示するアイコンを指定できる)(省略可。規定値'ParameterValue')
- NoQuotes : 文字列の中の変数を展開するか否かを指定?(違うかも)(省略可。規定値False)
では、実際に関数を作ってみましょう。
SearchWoerdsパラメータの一部として入力された文字列をGoogleサジェストに投げて、帰ってきた結果から CompletionResult を生成して出力します。
(2015/12/5 追記) PowerShell v5.0からは、Register-ArgumentCompleter コマンドレットで処理を登録できるようになりました。こちらを使用したサンプルコードを追記します。(以下2番目のコード)
後は、これを記述したスクリプトファイルを 以下の何れかに配置しておくだけでOKです。
- $env:PSModulePath に定義されているPath の1階層下のフォルダ
- 例: C:\Users\UserName\Documents\WindowsPowerShell\Modules\MyCompleter
- 任意のフォルダに置いて、そのパスを $env:PSArgumentCompleterPath に設定する
実行結果
"powershell" と入力して[Ctrl]+[Space]
"powershell" と入力して[Tab]
まとめ
思った以上に簡単に実現できてしまって驚きです。
他にも工夫次第でいろいろ便利なものが出来そうな予感がします。
TabExpansion++ が提供する入力補完も相当数あるようですので、自作しなくても十分かもしれませんが。
gooラボ 形態素解析APIを試してみる
これはPowerShell Advent Calendar 2014 : ATND 13日目の記事です。
PowerShell で REST API
近年、RESTfullなAPIで提供されるWebサービスが非常に増えているように感じます。
そんな中、ちょっと気になるAPIがあったら、気軽に試したりしたいですよね。
「PowerShellを使えば、お手軽にREST API がお試しできますよ!」
という事で、最近見かけた、gooラボの形態素解析APIを試してみたいと思います。
NTTレゾナント、gooで開発・蓄積した「日本語解析API」を公開、ビッグデータ解析機能なども提供予定:CodeZine
と、4種類のAPIが提供されています。
この中から今回は形態素解析APIを使います。
上の記事の内容を見る限りですと、文を語句に分割するだけ?のように思えますが、「PowerShell, 名詞, パワーシェル」の様に、分割した「語句」の「品詞(形態素)」と「読み」を付けて返してくれます。
形態素解析API:日本語文字列を語句に分割する技術 - gooラボ
Invoke-RestMethod でPOST
- リクエスト先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-RestMethod
のBody
パラメータに指定します。
今回は、とりあえず必須項目のapp_id
とsentence
のみを使用します。
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
形態素リスト。
形態素リストは文ごとに分かれた文単位形態素リストの配列となります。
文単位形態素リストは形態素情報の配列で、形態素情報には表記・形態素・読みのうち形態素情報フィルタで指定された要素が含まれます。
例:
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の指定が無いようなのです。
その場合ってどうなるの?
どうやら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 (期日・イベント事等、マイルストーン)
- 特に重要な項目にフラグ
!
を付けることが出来ます。 - ステータス、フラグの状態で色分け表示
コードは、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を起動するたびに以下のように表示されるようになります。
日付の入力はダイアログで
ToDo追加時のAdd-Todoコマンドで -setDate スイッチを指定すると、次のようなダイアログから日付を指定できるようになります。
このダイアログは、「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"
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{}では、スクリプトブロックを返却します
- スクリプトプロック内では以下の処理を行います
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()の一連の処理を自前で書いた方が良さそうです。
※ ただ、これを裏付けるような他の記事、ドキュメント等を見つけることが出来ず、いまいち確信が持てません。
有識者の方々のご意見を伺いたいところです。