私は HOME の下に Projects
というディレクトリを作り、その下に各プロジェクトのディレクトリを置いている。
そこそこプロジェクトの数が多くなってきたので、こんな風に階層化した。
Projects
├── business/
│ ├── 2020/
│ │ ├── project-a/
│ │ └── 2020年度開始のプロジェクト
│ ├── 2021/
│ │ ├── project-b/
│ │ └── 2021年度開始のプロジェクト
│ └── 仕事のプロジェクト
└── personal/
├── private/
│ ├── project-c/
│ └── GitHubで非公開にしてるプロジェクト
├── public/
│ ├── project-d/
│ └── GitHubで公開してるプロジェクト
└── 個人のプロジェクト
綺麗に整頓できて大変満足なのだが、とにかくプロジェクトのディレクトリが深い所にある。(自業自得)
そのせいでプロンプトをいじったりもした。
さらに言えば、cd
コマンドで各プロジェクトのディレクトリに行くだけでも大変。
なんとかこれを簡単にできんものかと。
レベル1:ZSHの補完を使う
パッと思いついたのは、cd
の代わりに cdp
という ZSH の関数を作り、バーンバーンとZSHの補完(completion)を使って移動すれば良いんじゃないかな、と。
~/.zshrc
あたりにこんな関数を作った。
PROJECT_TOP_DIR="${HOME}/Projects"
function cdp() {
pushd "${PROJECT_TOP_DIR}/$1" >/dev/null
}
引数に補完済みの path が入ってくることを期待するので、後はそこに行くだけ。
後は補完を設定するのだが、まずは ~/.zshrc
にこんな感じの設定を追加。
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
というファイルを作り、以下を書き込む。
# 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
と打つ手間が省けた。特に ~
!)
レベル2:FZF を使う
ところがすぐに不満が出てくる。
プロジェクトのディレクトリに行く度に personal
や private
や public
っていう紛らわしい名前を何度も何度も補完しなきゃならない。(ネーミングセンスが無い自業自得)
プロジェクトの名前で直接行きたいんじゃ!何となくそれっぽい名前を打ち込んだら、いい感じに移動して欲しいんじゃ!
何となくそれっぽい補完といえば、fzf (fuzzy-finder) である。
目的のディレクトリは ~/Projects
から数えて3階層下ってのは分かってるので、find
でリストアップして fzf
で良い感じに探せるようにしよう!
~/.zshrc
の関数は以下のようになった。
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
以下だけを取得するために、ひっくり返して、最初の3つだけ取って、もう一回ひっくり返している-
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 の 14章: Expansion とか 15章: Parameters とか読まなきゃならない。でもこんなブ厚い本、とても読めないし、覚えられる自信は全く無い。
しかし有り難いことに Zsh 変数メモ · GitHub に全て日本語で分かりやすくまとめられていた。感謝 x 100。
~/.zcomp/_cdp
は以下のようになる。
# 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
するから、2回 Return
することになって気が狂いそうになる。
cdp
と打ったら Tab
で補完しないと気が済まない。
こうなるともう、ZSH の補完を乗っ取るしか無い。
~/.zshrc
の設定。
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(zsh で自分好みの補完インターフェースを定義する - Qiita)を使っても良かったのだが、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 芸人。