fzfでモノレポの移動をしやすくする

モノレポで別のワークスペースに移動するのが面倒でした。cd ../fooとかcd packages/fooとか、けっこうタイプ数が多いです。

この問題をfzfを使って解決しました。この記事では、そのスクリプトを解説します。

スクリプトの概要

筆者が使っているモノレポは「npmを使ってpackages/*でワークスペースを区切っているもの」がほとんどです。今回はその形式で対応しました。

たとえばこんなリポジトリがあったとして。

.
`-- packages
|-- sample
|-- server
`-- client

カレントディレクトリがpackages/sampleのときはこんなfzfになります。

> server | /home/eetann/piyo/packages/server/
client | /home/eetann/piyo/packages/client/
<root> | /home/eetann/piyo

「カレントディレクトリ以外のpackages/*」と「リポジトリのルート」が候補です。選択したらそのディレクトリに移動します。

カレントディレクトリがリポジトリのルートの場合は単にpackages/*が候補になります。

> sample | /home/eetann/piyo/packages/sample/
server | /home/eetann/piyo/packages/server/
client | /home/eetann/piyo/packages/client/

スクリプトの解説

先に実装の全体を載せます。

function monorepo_cd() {
local packages_dir candidates=()
local cwd="$PWD"
# 現在のディレクトリ名に'packages'が含まれているか判定
if [[ "$cwd" == *"/packages"* ]]; then
# 'packages' ディレクトリの直下を探す
packages_dir="${cwd%%/packages*}/packages"
if [[ ! -d "$packages_dir" ]]; then
return
fi
if [[ -d "$packages_dir" ]]; then
# candidates: packages直下のディレクトリ(ただし現在のディレクトリは除外)
for d in "${packages_dir}"/*(/); do
[[ "$d" != "$cwd/" ]] && candidates+=("$d")
done
# packagesより上のディレクトリも候補に追加
local upper_dir="${packages_dir%/packages}"
[[ "$upper_dir" != "$cwd" ]] && candidates+=("$upper_dir")
else
return
fi
elif [[ -d "$cwd/packages" ]]; then
packages_dir="$cwd/packages"
candidates=("${packages_dir}"/*(/))
else
return
fi
# ディレクトリがなければ終了
if [[ ${#candidates[@]} -eq 0 ]]; then
return
fi
# fzfで表示名付きで選択
local display_list=()
for d in "${candidates[@]}"; do
if [[ "$d" == "${packages_dir%/packages}" ]]; then
display_list+=("<root> | $d")
else
display_list+=("$(basename "$d") | $d")
fi
done
local selected=$(printf '%s\n' "${display_list[@]}" | fzf)
if [[ -n "$selected" ]]; then
local dest="${selected##*$' | '}"
BUFFER="cd $dest"
zle accept-line
fi
}
zle -N monorepo_cd
bindkey '^xc' monorepo_cd

スクリプトの解説

*"/packages"*: まるで*で囲っているように見えますが、単にグロブです。

変数から末尾削除

${cwd%%/packages*}: 変数cwd/packages*の部分を削除してます。
${packages_dir%/packages}: 変数packages_dir/packagesの部分を削除してます。

${変数名%%パターン}%%)だとマッチの部分が最長、${変数名%パターン}%)だと最短になります。

参考:zsh のあの記号 (チートシート) - blog.livewing.net


以上、fzfを使ったスクリプトの紹介でした。

今回のスクリプトはほとんどGTP-4.1に書かせてみました。「自分のdotfilesから作った雛形」と「やってほしいことをTODOコメントで書いた状態」で渡したら業務中にサクッとできて良かったです。