【PowerShell】peco を使った簡易ランチャ


PowerShell からも peco で日本語を扱えるようになった ので、コマンドラインから使える簡単なランチャを作ってみようと思います。

環境は以下の通り。

Name                           Value
----                           -----
PSVersion                      7.0.2
PSEdition                      Core
GitCommitId                    7.0.2
OS                             Microsoft Windows 10.0.18362
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

このような動作をイメージしています。

(1) よくアクセスするフォルダ/ファイルのパスをテキストファイルに列挙しておく
(2) 読み込んだファイルを peco でフィルタリング
  ・[ファイルを選んだ場合]→そのまま開く
  ・[フォルダを選んだ場合]→サブフォルダを再度 peco でフィルタリングして開く

実際にコードを書いていきます。まずは peco のラッパーから。

上記の記事から PowerShell 7 にバージョンを上げたことで $OutputEncoding のデフォルト値が utf-8 となり少し記述が短くなっています。

function psPeco {
    $origin = [System.Console]::OutputEncoding
    $utf8 = [System.Text.Encoding]::UTF8
    [System.Console]::OutputEncoding = $utf8
    $out = $input | Out-String -Stream | peco.exe "--initial-filter=Fuzzy"
    [System.Console]::OutputEncoding = $origin
    return $out
}

次はテキストファイルの読み込み部分。C:\Personallaunch.txt を作って改行区切りでパスを書いていきます(文字コードは BOM なし utf-8 )。

パスをそのままフィルタリングするとフルパスにもマッチしてしまうので少し工夫しました。

  • テキストファイルの各行からフィルタリング用の「表示名」を生成する
    • 表示名はパスの後ろに | で区切って指定
    • 指定しなかった場合はパス最下層を表示名とする
  • 表示名をキーとしたハッシュテーブルを操作するようにする
    • キーは文字数順でソートしておく(マッチを楽にするため)
  • 存在しないパスは除外しておく
function New-LaunchHashTable {
    $objArray = New-Object System.Collections.ArrayList
    Get-Content "C:\Personal\launch.txt" -Encoding utf8 | ForEach-Object {
        if ($_ -match "\|") {
            $pair = @($_ -split "\|")
            $fullPath = $pair[0]
            $displayName = $pair[1]
        }
        else {
            $fullPath = $_
            $displayName = $fullPath | Split-Path -Leaf
        }
        $objArray.Add([PSCustomObject]@{
            Name = $displayName
            Fullname = $fullPath
        }) > $null
    }
    $hashTable = [ordered]@{}
    $objArray | Where-Object {Test-Path $_.Fullname} | Sort-Object {($_.Name).Length} | ForEach-Object {
        $hashTable[$_.Name] = $_.Fullname
    }
    return $hashTable
}

launch.txt を下記のように指定すると……

launch.txt
C:\Personal\tools
C:\Personal\tools\mytool
C:\Personal\tools\memo.txt
C:\Personal\work\memo.txt|work_memo

期待通りに読み込まれました。

Name と表示されているのに .Keys でキーを取得できるのはツッコミを受けそうですが仕様です。

最後に、実際のフィルタリング処理です。

サブフォルダの検索で * を候補として表示させているのは、テキストファイルの候補から絞り込んだフォルダそのものを開けるようにするためです。サブフォルダが多すぎる場合は Get-ChildItem-Depth で検索の深さを指定してやると速くなります。

function Invoke-FuzzyLauncher {
    $launchHashTable = New-LaunchHashTable
    $selected = $launchHashTable.Keys | psPeco -fuzzy | Select-Object -First 1
    if (-not $selected) {
        return
    }
    $rootDir = $launchHashTable[$selected]

    if (Test-Path $rootDir -PathType Leaf) {
        $targetPath = $rootDir
    }
    else {
        $subDir = @(Get-ChildItem $rootDir -Directory -Name)
        if (-not $subDir) {
            $targetPath = $rootDir
        }
        else {
            $sorted = @("*") + @($subDir | Sort-Object Length)
            $leafPath = $sorted | psPeco -fuzzy
            if (-not $leafPath) {
                return
            }
            elseif ($leafPath -eq "*") {
                $targetPath = $rootDir
            }
            else {
                $targetPath = $rootDir | Join-Path -ChildPath $leafPath
            }
        }
    }
    Invoke-Item $targetPath
}

以上の内容を $profile に書いておくとターミナル起動時に自動で読み込まれて使用できるようなります(z など短いエイリアスを当てておくとさらに便利)。

2020-06-25追記

go で書き直してみました。