Register-ArgumentCompleter で 入力補完機能を自作しよう!

これは、PowerShell Advent Calendar 2015 9日目の記事です。

atnd.org

目次

はじめに

以前、TabExpansion++ というモジュールを使うと、自前の入力補完機能を簡単に作ることが出来る、という内容の記事を書きました。

pierre3.hatenablog.com

この入力補完のカスタマイズ機能ですが、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 フォルダにショートカットとして保存されます。スタートメニューやタスクバーアイコンを右クリックすると表示される「最近使ったもの」一覧で利用されます。

f:id:pierre3:20151203225018p:plain

実装

実装したコードは、以下の通りです。
細かな部分はコード内のコメントと、後述の補足説明をご確認ください。

「最近使った項目」フォルダからショートカットファイルの一覧を取得する

「最近使った項目」(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](「最近使った項目」の一覧を表示)

f:id:pierre3:20151207220053p:plain

"ab" を入力後 [Ctrl]+ [Space] (「最近使った項目」で名前に "ab" を含むファイルのみ表示)

f:id:pierre3:20151207220232p:plain

"c:\" を入力 (c ドライブのアイテム一覧を表示)

f:id:pierre3:20151207220256p:plain

ちゃんと、期待通りの動きになっているようです。

まとめ

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/EtwStreamPowerShellから使えるようにしてみました。

github.com

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);

PSEventSource

DumpWithColor Switch

DumpWithColor スイッチで、色付きの簡易ビューアになります。

PS C:\> Get-TraceEventStream -NameOrGuid SampleEventSource -DumpWithColor

DumpWithColor

WellKnownEventSource

WellKownEventSource パラメータで、既知のEventSource のGUID を一覧から指定できます。

WellKnownEventSource

View in GridView-Window

Out-GridView に流すと、GUI上でソートやフィルタリングが自在にできるリッチなEventビューワにもなります。

PS C:\> Get-TraceEventStream SampleEventSource | Out-GridView

EtwStreamPS_Out-GridView_filter.png

EtwStreamPS_Out-GridView_filter2.png

実装

コマンドレットの実装は、こちらの記事を参考にさせて頂きました。

tech.tanaka733.net

参考というか、コードのベース部分は、ほぼそのまま流用させて頂きました。

変更したのは、ソースのIObservableをEtwStream.ObservableEventListener.FromTraceEvent() で取得するようにした事と、 StopProcessing() メソッドをオーバーライドして Ctrl+Break による中断に対応した事位です。

まとめ

ブログのネタになればと、試しに作ってみましたが、予想以上に使えそうな予感がします。

LINQPadのライセンスが無かったり、そもそも職場でフリーソフトのインストールが制限されていて使えない!といったような方の代替手段として使えるかも??

PowerShell の入力補完にGoogleサジェストの結果を表示する

↑ みたいな事って出来るのかな、となんとなく調べてみたところ、どうやら TabExpansion++ というモジュールを使うと簡単に出来そう!
という事で、ちょっと試してみました。

f:id:pierre3:20150625215746p:plain

TabExpansion++

github.com

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階層下のフォルダ
  • 任意のフォルダに置いて、そのパスを $env:PSArgumentCompleterPath に設定する

実行結果

"powershell" と入力して[Ctrl]+[Space]

f:id:pierre3:20150627002308p:plain

"powershell" と入力して[Tab]

f:id:pierre3:20150625220854p:plain

まとめ

思った以上に簡単に実現できてしまって驚きです。

他にも工夫次第でいろいろ便利なものが出来そうな予感がします。

TabExpansion++ が提供する入力補完も相当数あるようですので、自作しなくても十分かもしれませんが。

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"

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()の一連の処理を自前で書いた方が良さそうです。

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