特定の階層下にあるディレクトリに最速で辿り着く話

私は HOME の下に `Projects` というディレクトリを作り、その下に各プロジェクトのディレクトリを置いている。
そこそこプロジェクトの数が多くなってきたので、こんな風に階層化した。

```
Projects
├── business/
│   ├── 2020/
│   │   ├── project-a/
│   │   └── 2020年度開始のプロジェクト
│   ├── 2021/
│   │   ├── project-b/
│   │   └── 2021年度開始のプロジェクト
│   └── 仕事のプロジェクト
└── personal/
    ├── private/
    │   ├── project-c/
    │   └── GitHubで非公開にしてるプロジェクト
    ├── public/
    │   ├── project-d/
    │   └── GitHubで公開してるプロジェクト
    └── 個人のプロジェクト
```

綺麗に整頓できて大変満足なのだが、とにかくプロジェクトのディレクトリが深い所にある。（自業自得）
そのせいで[プロンプトをいじった](https://qiita.com/tetutaro/items/c3402ac4359e1a0fdd37)りもした。
さらに言えば、`cd` コマンドで各プロジェクトのディレクトリに行くだけでも大変。

なんとかこれを簡単にできんものかと。

# レベル１：ZSHの補完を使う
パッと思いついたのは、`cd` の代わりに `cdp` という ZSH の関数を作り、バーンバーンとZSHの補完（completion）を使って移動すれば良いんじゃないかな、と。

`~/.zshrc` あたりにこんな関数を作った。

```shell
PROJECT_TOP_DIR="${HOME}/Projects"
function cdp() {
    pushd "${PROJECT_TOP_DIR}/$1" >/dev/null
}
```

引数に補完済みの path が入ってくることを期待するので、後はそこに行くだけ。

後は補完を設定するのだが、まずは `~/.zshrc` にこんな感じの設定を追加。

```shell
fpath=(${HOME}/.zcomp ${fpath} /usr/local/share/zsh-completions /usr/local/share/zsh/site-functions /usr/local/share/zsh/functions)
autoload -Uz compinit
compinit
```

本当はこの後に `zstyle` で色々と設定しているのだが、それは ZSH の解説をしているサイトに任せよう。
ここで言いたいのは、自作コマンド（関数）の補完を定義するファイルの置き場として、`${HOME}/.zcomp` を付け足してるよ、ということだけ。

後は `${HOME}/.zcomp/_cdp` というファイルを作り、以下を書き込む。

```shell
#compdef cdp
function _cdp() {
    _arguments '1:Project Directory:_path_files -W ~/Projects/ -/'
}
compdef _cdp cdp
```

`_arguments` は良いとして、`_path_files` に `-W ~/Projects/` を付けることで（どんなディレクトリにいても）`~/Projects` の下のエントリを探すし、`-/` を付けることでディレクトリしか候補に出さなくしてる。

これで、どこにいても `cdp ` と打って後は `Tab` をバーンバーンと打つことによって、`~/Projects` の下のディレクトリを補完しながら直接移動することが出来るようになった。
（`~/Projects` と打つ手間が省けた。特に `~` ！）

# レベル２：FZF を使う
ところがすぐに不満が出てくる。

プロジェクトのディレクトリに行く度に `personal` や `private` や `public` っていう紛らわしい名前を何度も何度も補完しなきゃならない。（ネーミングセンスが無い自業自得）

プロジェクトの名前で直接行きたいんじゃ！何となくそれっぽい名前を打ち込んだら、いい感じに移動して欲しいんじゃ！

何となくそれっぽい補完といえば、[fzf](https://github.com/junegunn/fzf) (fuzzy-finder) である。

目的のディレクトリは `~/Projects` から数えて３階層下ってのは分かってるので、`find` でリストアップして `fzf` で良い感じに探せるようにしよう！

`~/.zshrc` の関数は以下のようになった。

```shell
PROJECT_TOP_DIR="${HOME}/Projects"
PROJECT_DEPTH_FROM_TOP=3
function __list_projects() {
    local -a projects
    projects=($(find -L ${PROJECT_TOP_DIR} -type d -mindepth ${PROJECT_DEPTH_FROM_TOP} -maxdepth ${PROJECT_DEPTH_FROM_TOP} \( -name '.*' -or -name '_*' \) -prune -or -type d -print 2>/dev/null | rev | cut -d/ -f-${PROJECT_DEPTH_FROM_TOP} | rev | sort))
    print -r -- ${(qq)projects}
}
function cdp() {
    local -a dir
    if [[ -z "$1" ]]; then
        dir=$(echo ${(F)${(@Q)${(z)$(__list_projects)}}} | fzf --no-multi --preview "tree -N ${PROJECT_TOP_DIR}/{} | head -20" --preview-window down:50%:nowrap:hidden --bind "?:toggle-preview")
        if [[ "${dir}" != "" ]]; then
            pushd "${PROJECT_TOP_DIR}/${dir}" >/dev/null
        fi
    else
        pushd "${PROJECT_TOP_DIR}/$1" >/dev/null
    fi
}
```

ZSH の変数展開とか頑張って盛り込んで作ってみた。

* プロジェクトを `find` で探す部分は補完でも使うので、関数（`__list_projects()`）にした
* `__list_projects()` の工夫
	* `mindepth` と `maxdepth` を同じ値にして、狙うディレクトリだけを持ってくる
	* `.*` と`_*` という名前のディレクトリは除外（念の為）
	* `find` の結果は絶対パスなので、`~/Projects` 以下だけを取得するために、ひっくり返して、最初の３つだけ取って、もう一回ひっくり返している
		* `fzf` にダラダラ長い名前が出てこない様に
	* ZSH の関数は 0-255 の値しか返せないので、`${(qq)VAR}` でクォーテーション付けながら結合して `print`
* `cdp()` の工夫
	* `__list_projects` の結果を
		* `${(z)VAR}` で、分解
		* `${(@Q)VAR}` で、配列の各要素のクォーテーション除去
		* `#{(F)VAR}` で、配列の各要素を改行でつなげる
			* `fzf` に食べさせる用
	* `fzf` のオプション
		* `--no-multi` で複数選択を出来ないように
			* したら困る
		* preview を用意したけど、いつも出てるのはウザいので、default で隠す
			* でも `?` を叩くと見える

ZSH の変数展開とか置換とか本当に難しい。本来ならば [The Z Shell Manual](https://zsh.sourceforge.io/Doc/Release/index.html) の [14章: Expansion](https://zsh.sourceforge.io/Doc/Release/Expansion.html) とか [15章: Parameters](https://zsh.sourceforge.io/Doc/Release/Parameters.html) とか読まなきゃならない。でもこんなブ厚い本、とても読めないし、覚えられる自信は全く無い。
しかし有り難いことに [Zsh 変数メモ · GitHub](https://gist.github.com/sho-t/d9cdf8271b3de7c4238739e523490542) に全て日本語で分かりやすくまとめられていた。感謝 x 100。

`~/.zcomp/_cdp` は以下のようになる。

```shell
#compdef cdp
function _cdp() {
    _values 'Project Directory' ${(@Q)${(z)$(__list_projects)}}
}
compdef _cdp cdp
```

頑張って関数を作ったので、こちらは超カンタン。

これで `cdp` と打って `Return` すると FZF で良い感じに探せるし、何なら補完もそのまま使える。
だいぶ便利になった。

# レベルMAX：ZSH の補完を乗っ取る
これでしばらくは満足していたのだが、やがて耐えられなくなった。

`cdp` の後に `Return` するのが耐えられない。
`cdp` の後に `Return` して、その後 FZF の決定で `Return` するから、２回 `Return` することになって気が狂いそうになる。
`cdp` と打ったら `Tab` で補完しないと気が済まない。

こうなるともう、ZSH の補完を乗っ取るしか無い。

`~/.zshrc` の設定。

```shell
PROJECT_TOP_DIR="${HOME}/Projects"
PROJECT_DEPTH_FROM_TOP=3
function __list_projects() {
    local -a projects
    projects=($(find -L ${PROJECT_TOP_DIR} -type d -mindepth ${PROJECT_DEPTH_FROM_TOP} -maxdepth ${PROJECT_DEPTH_FROM_TOP} \( -name '.*' -or -name '_*' \) -prune -or -type d -print 2>/dev/null | rev | cut -d/ -f-${PROJECT_DEPTH_FROM_TOP} | rev | sort))
    print -r -- ${(qq)projects}
}
function cdp() {
    local -a dir
    if [[ -z "$1" ]]; then
        pushd "${PROJECT_TOP_DIR}" >/dev/null
    else
        dir=$(echo ${(F)${(@Q)${(z)$(__list_projects)}}} | fzf --no-multi --no-exit-0 --no-select-1 --preview "tree -N ${PROJECT_TOP_DIR}/{} | head -20" --preview-window down:50%:nowrap:hidden --bind "?:toggle-preview" --query "$1")
        if [[ "${dir}" != "" ]]; then
            pushd "${PROJECT_TOP_DIR}/${dir}" >/dev/null
        fi
    fi
}
function __cdp_widget() {
    local -a buffer cmd arg dir
    buffer="${LBUFFER}${RBUFFER}"
    cmd=${${(ws, ,)buffer}[1]}
    arg=${${(ws, ,)buffer}[2]}
    if [[ ${cmd} == "cdp" ]]; then
        dir=$(echo ${(F)${(@Q)${(z)$(__list_projects)}}} | fzf --no-multi --no-exit-0 --no-select-1 --preview "tree -N ${PROJECT_TOP_DIR}/{} | head -20" --preview-window down:50%:nowrap:hidden --bind "?:toggle-preview" --query "${arg}")
        if [[ "${dir}" != "" ]]; then
            pushd "${PROJECT_TOP_DIR}/${dir}" >/dev/null
            zle accept-line
            LBUFFER=""
            RBUFFER=""
        else
            zle reset-prompt
            LBUFFER=""
            RBUFFER=""
        fi
    else
        zle expand-or-complete
    fi
}
zle -N __cdp_widget
bindkey "^I" __cdp_widget
```

[pmy](https://github.com/relastle/pmy)（[zsh で自分好みの補完インターフェースを定義する - Qiita](https://qiita.com/relastle/items/237ca33ac5a605702e25)）を使っても良かったのだが、ZSH だけで何とかなりそうだったので、自分で作った。汎用性（他のコマンドでも自由に fzf したい）ならば pmy を使うべき。ただ、ソースコードは非常に参考にさせて頂いた。感謝 x 1000。

* `_cdp()` の変更点
	* 一応 `Return` して補完する手段も残しておいた
		* for 自分 only backward compatibility
	* `Return` する時、引数が有る時と無い時で、以前と挙動が逆
		* 引数が無いと、`${PROJECT_TOP_DIR}` に行く
			* そんな事をしたい時もある
		* 引数が有れば、それを `fzf` にクエリとして渡すようにした
* `__cdp_widget()` の工夫
	* `Tab` を乗っ取ってる
		* でも `cdp` の補完じゃなかったら、`expand-or-complete`（通常の ZSH の補完）に明け渡してる
	* カーソルがどこにあっても、全部含めて補完
		* 漢
	* 引数は `fzf` のクエリとして引き渡し
	* `fzf` でパスを確定したら、`reset-prompt` じゃなくて `accept-line`
		* `accept-line` じゃないと `$VIRTUAL_ENV` が即座に反映されない
* `fzf` の工夫
	* `exit-0`, `select-1` の抑止
		* そうじゃないと、`Return` 全く押さずに `Tab` だけで移動しちゃうことがあるので、最速すぎてビックリする
			* 真の最速を求める人には `--exit-0`, `--select-1` 推奨
* 問題点
	* `fzf` のオプションが明らかに被っているので、変数としてまとめたいんだけど、そうすると動かなくなる
		* なんでや

# まとめ
Shell 芸人。