0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

私は 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 を使う

ところがすぐに不満が出てくる。

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

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

何となくそれっぽい補完といえば、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() の工夫
    • mindepthmaxdepth を同じ値にして、狙うディレクトリだけを持ってくる
    • .*_* という名前のディレクトリは除外(念の為)
    • 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 Manual14章: 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

pmyzsh で自分好みの補完インターフェースを定義する - 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 芸人。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?