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


[PowerShell からも peco で日本語を扱えるようになった](https://qiita.com/AWtnb/items/088ca9f10a4e8fabff57) ので、コマンドラインから使える簡単なランチャを作ってみようと思います。

環境は以下の通り。

```
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 となり少し記述が短くなっています。

```PowerShell
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:\Personal` に `launch.txt` を作って改行区切りでパスを書いていきます（文字コードは BOM なし utf-8 ）。  
パスをそのままフィルタリングするとフルパスにもマッチしてしまうので少し工夫しました。

+ テキストファイルの各行からフィルタリング用の「表示名」を生成する
    + 表示名はパスの後ろに `|` で区切って指定
    + 指定しなかった場合はパス最下層を表示名とする
+ 表示名をキーとしたハッシュテーブルを操作するようにする
    + キーは文字数順でソートしておく（マッチを楽にするため）
+ 存在しないパスは除外しておく

```PowerShell
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
```

期待通りに読み込まれました。
![202006172001056.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501098/cd9a8776-3ae5-1ffe-32b4-d0910a8cf2a2.png)


`Name` と表示されているのに `.Keys` でキーを取得できるのはツッコミを受けそうですが仕様です。
![202006172001333.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501098/88eb7276-1b43-b6df-a9c1-de3cc969c8fa.png)

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

```PowerShell
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` など短いエイリアスを当てておくとさらに便利）。

![202006172002237.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501098/cb64a43b-44b5-1153-aca7-311190303144.png)

### 2020-06-25追記
[go で書き直してみました。](https://qiita.com/AWtnb/items/45ec1d19f8fa1e2dfaf3)