PowerShellで高速cdを可能にするZLocationのすゝめ

本記事は PowerShell Advent Calendar 2019 の 14 日目です。

ディレクトリ移動する際に便利な ZLocation という PowerShell モジュールを紹介します。

# ZLocation とは

ZLocation とは `cd` コマンドに取って代わり、一度訪問したディレクトリを記憶して、
再度訪問する際に手早く移動するための `z` コマンドを提供してくれる PowerShell モジュールです。

元々、Linux 界で（そこそこ）人気の [z コマンド](https://github.com/rupa/z) を PowerShell に移植したものとなります。

このツールを導入することで、今まで Explorer を使って移動してしまいがちだったディレクトリへ
瞬時に移動することができるようになります。

以下、簡単な概要です。

| 項目     | 説明                                            |
| :------- | ----------------------------------------------- |
| 開発者   | Sergei Vorobev 氏（元 PowerShell のメンテナー） |
| 登場時期 | 2015/05/06 (Initial release)                    |
| 最新版   | 1.3.0 - 2019/09/05[^3]                          |
[^3]: 配布先：[PowerShell Gallery](https://www.powershellgallery.com/packages/ZLocation/)
| リポジトリ | https://github.com/vors/ZLocation |
| 実装言語 | PowerShell |
| 対応 OS | Cross-platform (Windows, Linux or Mac) |
| ライセンス | MIT License |
| 公式ドキュメント | GitHub の REAME のみ |

# 動作環境

```powershell
$PSVersionTable

#=>Name                           Value
#=>----                           -----
#=>PSVersion                      6.2.3
#=>PSEdition                      Core
#=>GitCommitId                    6.2.3
#=>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
```

# インストール方法

公式ページにある通り、[PowerShellGet Gallery](https://www.powershellgallery.com/packages/ZLocation/) からインストールします。
記事を書いている時の最新バージョンは 1.3.0 でした。

```powershell
Install-Module ZLocation -Scope CurrentUser

(Get-InstalledModule ZLocation).Version.ToString()
#=>1.3.0
```

## 自動インポートさせる方法

次回以降 ZLocation を自動的に有効化させるために、
`$profile` ファイル内に `Import-Module` で読み込む処理を追加しておきます。
公式サイトは以下のワンラインで追加できると書いていますが、
手動で `Import-Module ZLocation` を追加するのでも全然丈夫です。

```powershell
Add-Content -Value "`r`n`r`nImport-Module ZLocation`r`n" -Encoding utf8 -Path $profile.CurrentUserAllHosts
```

追記する profile ファイルですが、PowerShell コンソールや Visual Studio Code の両方から
設定を読み込みたい場合は、`$profile.CurrentUserAllHosts` のファイルに、
PowerShell コンソールだけでよい場合は、`$profile.CurrentUserCurrentHost` に書き込むようになります。

また、`prompt` 関数をカスタマイズしている場合は、その関数を宣言している箇所より後に記載してください。
自身でカスタマイズしていなくても、例えば、[oh-my-posh](https://github.com/JanDeDobbeleer/oh-my-posh) などの prompt をカスタマイズしてくれる
モジュールをインポートしている場合、そのインポート箇所より後に記載する必要があります。

追記が終われば、PowerShell を再起動します。

# 基本的な使い方

まず、今回動作確認用に以下のディレクトリ構造を作成しました。

```powershell
@(
    "z_play_ground\sample\sub",
    "z_play_ground\special\sub"
) |
% { New-Item -ItemType Directory -Path $_ }
```

では、`cd` で作成した `z_play_ground` ディレクトリへ移動してください。

```powershell
PS ~ > cd z_play_ground
PS ~\z_play_ground >
```

次に `z` コマンドを入力します。
すると、先ほど移動した `z_play_ground` ディレクトリが ZLocation に記録されていることがわかります。

```powershell
PS ~\z_play_ground > z
#=>
#=>Weight Path
#=>------ ----
#=>     2 C:\Users\nimzo6689
#=>     1 C:\Users\nimzo6689\z_play_ground
```

これで、今後 z_play_ground に移動したいときに、そのディレクトリのフルパスに含まれるキーワードを指定しただけで、
瞬時に移動できるようになりました。
試しに `C:\` 直下に移動したあと、再度 `~\z_play_ground` ディレクトリに移動してみます。

```powershell
PS ~\z_play_ground > cd c:\
PS \ > z z_play
PS ~\z_play_ground > z
#=>
#=>Weight Path
#=>------ ----
#=>     1 C:\
#=>     2 C:\Users\nimzo6689
#=>     3 C:\Users\nimzo6689\z_play_ground
```

基本的な使い方はこれだけです。
最初だけ、登録されているディレクトリ数が少ないためすぐに便利さを感じられる訳ではないかもですが、
しばらく使ってさえいれば段々その便利さを体感していただけると思います。

# 上手く使いこなすためのコツ

ここからは使っていくのに必須という訳ではないですが、
知っておいた方がより上手く使いこなせるというナレッジをまとめていきます。

## Tips 1: コマンドの実行回数に応じて Weight が上がる

ZLocation に記録される情報として、ドライブも含めたフルパスと Weight という訪問数があります。
この Weight ですが、例えば `z` の引数に指定したキーワードに一致するディレクトリが複数存在した場合、
最も Weight が高いディレクトリに移動するような仕様となっています。

そのため、訪問数が多いディレクトリへは比較的少ないキーワードを指定しただけで移動できるようになり、
逆に訪問数が少ないディレクトリへは他の登録済みのディレクトリ名と被らないように指定する必要があります。

```powershell
PS ~\z_play_ground > cd .\sample\sub\
PS ~\z_play_ground\sample\sub > echo "一度コマンドを実行"
PS \ > cd ..\..\special\sub\
PS ~\z_play_ground\special\sub > cd ..\..

PS ~\z_play_ground > z
#=>
#=>Weight Path
#=>------ ----
#=>     1 C:\
#=>     2 C:\Users\nimzo6689
#=>     5 C:\Users\nimzo6689\z_play_ground
#=>     2 C:\Users\nimzo6689\z_play_ground\sample\sub
#=>     1 C:\Users\nimzo6689\z_play_ground\special\sub

PS ~\z_play_ground > z sub
PS ~\z_play_ground\sample\sub >
```

このように、`~\z_play_ground\sample\sub` の方が `~\z_play_ground\special\sub` よりも
Weight が高いため、優先的に移動されるようになります。

※ ちなみに ZLocation に記録されるタイミングはコマンドを実行した後になるため、`cd` した場合、`cd` 後のディレクトリが ZLocation に新たに記録される対象となるようです。

## Tips 2: 複数の条件を指定できる

前の Tips で説明した通り、同じキーワードに一致するディレクトリが複数存在した場合、
Weight が高いディレクトリが選択されるわけですが、
Weight が低いディレクトリへ移動することも可能です。

方法は 2 つありまして、まず１つ目は「Tab」キー、または、「Ctrl + Space」キーによる補完入力を活用することです。
ZLocation では条件に一致するディレクトリ全てを補完入力させることができます。

そのため、候補としてのディレクトリが複数あった場合でも「Tab」キー、または、
「Ctrl + Space」キーで該当のディレクトリを選択さえすれば、あとは「Enter」キーで移動できます。

方法２つ目としては、キーワードを複数指定することです。

例えば、Tips1 の続きとして、`special\sub` に移動したい場合、以下のようにすると補完入力に頼らなくても一発で移動可能です。

```powershell
PS ~\z_play_ground > z sp sub
PS ~\z_play_ground\special\sub > z z
PS ~\z_play_ground >　
```

ただ、キーワードを複数指定するときは同じようなディレクトリが多く存在してしまっているときぐらいで、
ほとんどのケースは補完入力で選択した方が楽だと思います。

## Tips 3: 以前いたディレクトリに戻る場合は `z -` で可能

よく `pushd` で移動すれば、以前いたディレクトリに戻りたい場合、`popd` で戻れるから便利ということが言われていますが、
`z` で移動した場合、実は内部で訪問したディレクトリの順番を保持しており、
`popd` のように以前いた場所に戻りたい場合は、`z -` で可能となります。

ただ、注意点として、`z` で移動したとき限定で `cd` で移動してしまうとその移動元のディレクトリには
`z -` では戻れなくなります。

```powershell
PS ~\z_play_ground > z sub
PS ~\z_play_ground\sample\sub > z sp sub
PS ~\z_play_ground\special\sub > z -
PS ~\z_play_ground\sample\sub > z -
PS ~\z_play_ground >
```

## Tips 4: 直下のサブディレクトリに移動したい場合は `cd` の方が便利

これは使っていて気付いたかなり細かい観点になりますが、
カレントディレクトリの配下にまだ ZLocation に登録されていないディレクトリがあり、
そこへ `z` で移動しようとした場合、補完入力しようとすると既に ZLocation に登録されている
他のディレクトリの候補しか出ないようになります。

文字で書くと分かりやすく書けないので、下のコマンドを実際打ってもらうのが早いです。

```powershell
PS ~\z_play_ground > mkdir supercalifragilisticexpialidocious
PS ~\z_play_ground > z #ここで「Tab」や「Ctrl + Space」をしてもサブディレクトリは表示されない。。
```

この場合、`cd` で移動するか、または、`.\` を最初に入力することで補完入力に出すことができます。

```powershell
PS ~\z_play_ground > z .\s
# ここで「Tab」や「Ctrl + Space」を打つと候補としてサブディレクトリが表示される！
# ただ、候補として表示されるだけで `z .\s` の段階で Enter キーを押しても移動できないので注意。
PS ~\z_play_ground > cd s
# cd だと普通に補完入力に表示される。
```

ここら辺はクセの強い挙動になっていますね。
登録されていないディレクトリでもカレントディレクトリのサブディレクトリであれば
候補としても出してもいいのかなと思ったりもしますが、どうなんでしょう。

## Tips 5: z-location.db でポータブルに使用可能

これは使う機会がどれほどあるのか分かりませんが、
ZLocation で記録されたディレクトリ情報は LiteDB の単一ファイルに保存されているので
別の PC でも記録した情報を使うことができるようです。

ファイルの場所は `$env:USERPROFILE` 配下にあります。

ただ、両方の PC でほぼ同じようなディレクトリ構成にしておく必要があるので、
どっちかというと、PC 買い替え時に良いのかもしれません。（未検証）

## Tips 6: 登録済みのディレクトリが削除されてしまった場合

ZLocation に登録されているディレクトリが削除された場合、
`z` でそのディレクトリに移動を試みることで ZLocation から訪問履歴を削除することができます。

```powershell
PS ~\z_play_ground > mkdir will_be_removed
PS ~\z_play_ground > cd will_be_removed
PS ~\z_play_ground > cd ..

# `-l` オプションで `will_be_removed` に一致する訪問履歴を検索する
PS ~\z_play_ground > z -l will_be_removed
#=>Weight Path
#=>------ ----
#=>     1 C:\Users\nimzo6689\will_be_removed

# ディレクトリを削除する
PS ~\z_play_ground > rm will_be_removed
# 削除されても ZLocation 内に訪問履歴は残っている
PS ~\z_play_ground > z -l will_be_removed
#=>Weight Path
#=>------ ----
#=>     1 C:\Users\nimzo6689\will_be_removed

# 削除されたディレクトリに移動を試みる
PS ~\z_play_ground > z will_be_removed
#=>WARNING: There is no path C:\Users\nimzo6689\z_play_ground\will_be_removed on the file system. Removing obsolete data from database.
#=>WARNING: Cannot find matching location

# 訪問履歴は削除されている
PS ~\z_play_ground > z -l will_be_removed
```

逆に言うと、`z` で移動しない限り削除されてしまったディレクトリは残り続けてしまうため、
万が一ディレクトリ構成が大幅に変更になった際には
LiteDB の [Shell コンソールアプリ](https://github.com/mbdavid/LiteDB/wiki/Shell)などで一括削除してしまうか、
[LiteDBViewer](https://github.com/falahati/LiteDBViewer) というツールでもレコード単位で削除することができるようなので、
使ってみると便利かもしれません。

## Tips 7: 本家の `z` コマンドとの実装差異

本家の `z` コマンドと比べると ZLocation は必要最小限の機能しかまだ実装できていないようです。
（もしくは、敢えて必要最小限としているか。）

メインとなる目的や挙動は一緒ですが、大きい差分としては Weight での優先順位の付け方かなと思います。

ZLocation の場合は、単純に訪問数が多ければ多いほど優先順位が上がる仕組みですが、
`z` コマンドの場合、何もオプションを付けづに移動するときは、
最近訪問したディレクトリと今までの訪問数を考慮して優先順位を決めているため、
ZLocation より若干使い勝手が良さそうです。

# おわりに

勤務中に「探し物」をしている時間は年間 150 時間って言われているけど、
ディレクトリ移動しようとしている時間も割と洒落にならないよね。