PsEnv への TabExpansion の実装

git-poshのコードを見ていたら、PowerShell 3.0 から導入された TabExtentionを使ってタブ補完を実装していた。[1]仕組みはシンプルで便利な機能だったので、いつも使っているPsEnv[2]に実装してみた。[3]簡単なコードだが知らないテクニックが使われていて面白かったので軽く紹介。

Piccolo

TabExtentionの基本

「コマンドラインで、タブを押したときに Function:TabExtention が呼ばれ、コマンドラインと最後のワードが渡される、TabExtention の関数で文字列の配列を返すとコマンドラインでは補完文字列として扱われる」

基本、これだけなので、非常にシンプルでわかり易い。[4]git-poshのコードを見ながら実装してみたら、こんな感じになった。

# Ideas from the Awesome Posh-Git - https://github.com/dahlbyk/posh-git
# Posh-Git License - https://github.com/dahlbyk/posh-git/blob/1941da2472eb668cde2d6a5fc921d5043a024386/LICENSE.txt
# http://www.jeremyskinner.co.uk/2010/03/07/using-git-with-windows-powershell/
function script:GetEnvironmentToolNames() {
return Get-Member -InputObject $Global:PsEnvConfig -MemberType NoteProperty | ForEach-Object -MemberName 'Name'
}
function script:GetEnvironmentSpecNames($toolName) {
$results = $Global:PsEnvConfig.$toolName | ForEach-Object{$_.name}
if($null -eq $results){return @()} else {return $results}
}
function script:GetAliasPattern($command) {
$aliases = @($command) + @(Get-Alias | Where-Object { $_.Definition -eq $command } | Select-Object -Exp Name)
"($($aliases -join '|'))"
}
function PsEnvTabExpansion($lastBlock) {
$patten = "^(?<toolname>$((GetEnvironmentToolNames) -join '|'))\s+(?<toolspec>\S*)"
switch -regex ($lastBlock -replace "^$(GetAliasPattern use-tool)\s+","") {
# Handles use <toolname> <toolspec>
"^(?<toolname>$((GetEnvironmentToolNames) -join '|'))\s+(?<toolspec>\S*)" {
(GetEnvironmentSpecNames $matches['toolname']) -like ("{0}*" -f $matches['toolspec'])
}
# Handles use <toolname>
"^(?<toolname>\S*)$" {
(GetEnvironmentToolNames) -like ("{0}*" -f $matches['toolname'])
}
}
}
if (Test-Path Function:\TabExpansion) {
Rename-Item Function:\TabExpansion TabExpansionBackup
}
function TabExpansion($line, $lastWord) {
$lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()
switch -regex ($lastBlock) {
# Execute psenv tab completion for all use-tool commands
"^$(GetAliasPattern use-tool) (.*)" {
PsEnvTabExpansion $lastBlock
}
# Fall back on existing tab expansion
default {
if (Test-Path Function:\TabExpansionBackup) {
TabExpansionBackup $line $lastWord
}
}
}
}

コード解説

L33で現在のTabExpansionの関数をリネームして保存しておく、補完処理が自分と関係無い場合は、このコードにフォールバックする。(関数名を変更できるのは知らなかった)コールバックチェーンの実装方法の一つらしい。TabExpansion は、Globalな名前空間にExportされてるが、TabExpansionBackup はExportされておらず、名前が衝突する心配は無い。タブが押されると、L37 TabExpansion関数が呼ばれる。正規表現で引っ掛けて、補完処理対象のコマンドラインならPsEnvTabExpansionへ、その他はTabExpansionBackupへフォールバックする。

L19 PsEnvTabExpansionは、コマンドラインのラストブロックを貰って、正規表現で引っ掛けて対象の補完文字列を返す。switch と正規表現を使うとスッキリ書けて気持ちいい。

DynamicParamとの比較

TabExtention では、仕組みがコマンドライン文字列のパターンマッチングなので PowerShell Cmdlet 以外のWin32 EXE などの補完も実装できる。PsEnvはCmdletなので、DynamicParamでも良かったかなという気もするが、実装を比べると.NETのクラス出まくりのDynamicParamに比べTabExtentionは綺麗に書ける。しかし、TabExpansion はGlobalなキー入力をフックするので下手に作るとキー入力のレスポンスが悪くなるという欠点がある。どっちも、デバックし辛いのは欠点。

PsEvnの紹介

あまり知られていない気がするので、PsEnvの紹介を簡単に書いておく。Windowsでは環境変数の取り合いになって、うまく共存できない環境がママある。linuxや、Macだと、rubyや、pythonの仮想的に切り替える仕組みだけで足りたりするのだが、Windowsだと駄目だったり。cygwin, msys さらにそのバージョン違いでコンパイルされていたりして混ざって動かなくなったりすることがある。そのため、標準の環境変数はなるべくクリーンにしておいて環境によって必要な環境変数を設定するようにしている。PsEnvは環境変数を設定するのに便利なCmdletだ。

使い方は、PsEnvを見てもらうとして、使ってる設定ファイルpsenv.jsonを参考までに上げておく。ゴミが多くて少々長い申し訳無い。

最後に

これを書いている途中で、TabExtention++ が、PSReadlineと同時に WMF5の標準機能として取り込まれている[5]のを知った。共存しても問題無いのだろうか。このあたり[6]には、PowerTab(?)対応のコードが入ってるので余計気になる。