ZSHのPROMPTを短くする（PWD, VIRTUAL_ENV）

真面目に仕事しようとしたら、私のプロンプトがエライことになってた。

![スクリーンショット 2021-10-11 1.46.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84885/d5d4c663-d708-8393-4c9b-6f51ed32ebf1.png)

長いわ！流石に長いわ！こんなんじゃ仕事できん！

* `${PWD}` が長い
    * プロジェクトを整理しようと階層的にしたら、長くなった
* Python の仮想環境の名前が長い
    * poetry の設定をいじって、pyenv の仮想環境と同じように扱えるようにした
    * プロジェクトのディレクトリに行くだけで仮想環境に入れるので、poetry の仮想環境を pyenv と同様に扱うのは（私にとって）必要不可欠
    * 仮想環境の名前を出すのも、どの仮想環境にいるか一目で分かるので、必要不可欠
    * poetry が仮想環境名として unique ID 的なものや Python のバージョンを勝手に付けるので、poetry が悪い
        * でも poetry の仮想環境の名前の付け方を変更する方法は分からない

# これまでの PROMPT の設定

こんな感じの設定にしてた。

```zsh
autoload colors
colors
local p_cdir="%B%F{blue}[%~]%f%b"$'\n'
local p_info="%n@%m${WINDOW:+[$WINDOW]}"
local p_mark="%B%(?,%F{green},%F{red})%(!,#,>)%f%b"
PROMPT=" $p_cdir$p_info $p_mark "
```

一応解説する。

* `%B...%b`
    * 囲まれている中身の文字を Bold にする
* `%F{color}...%f`
    * 囲まれている中身の文字の色を color にする
    * color として使える名前は、`black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`
        * 頑張れば256色とかを指定できるけど、ここで頑張らないで color schema の設定を頑張る方針
* `%~`
    * `${HOME}` の下にいるなら `${HOME}` を `~` に置き換えて、それ以外なら root （`/`）からの絶対パスで、`${PWD}` を表示する
* `$'\n'`
    * 改行
        * ダブルクォーテーション（`"`）だとダメ
* `%n`
    * username
* `%m`
    * hostname
* `${WINDOW}`
    * screen に入っていると、そのウィンドウ番号が入る
    * screen に入っていない場合は、定義されれない
* `${HOGE:+moge}`
    * `${HOGE}` という変数に何か値が入っている場合、`moge` という文字列になる
    * `${HOGE}` という変数に値が入っていない場合、そのまま空になる
* （ちなみに）`${HOGE:-moge}`
    * `${HOGE}` という変数が無いか空の場合に、`moge` という文字列になる
    * `${HOGE}` という変数に値が入っている場合は、その値
* `%(?,hoge,moge)`
    * 直前のコマンドの返り値（`$?`）が 0 （正常終了）ならば、`hoge`
    * 直前のコマンドの返り値が 0 でない（異常終了）ならば、`moge`
* `%(!,hoge,moge)`
    * ユーザーが root（id==0）ならば、`hoge`
    * ユーザーが root でないならば、`moge`

# ${PWD} が長い問題

これは `%~` 自体に if else みたいな機能があるので、それを使えば簡単に設定できる。

* `%(5~|hoge|moge)`
    * `%~` で、表示されるディレクトリの数が５個以上だったら `hoge`、それ以外なら`moge` になる
* `%(5~|.../%2~|%~)`
    * `%~` で、表示されるディレクトリの数が５個以上だったら、`.../` という文字に続きディレクトリの最後の２つを表示、それ以外なら `%~` を全部表示
* `%(5~|%-2~/.../%1~|%~)`
    * `%~` で、表示されるディレクトリの数が５個以上だったら、ディレクトリの最初の２つと、`/.../` という文字、そしてディレクトリの最後の１つを表示

これで解決。あとは自分の好みに設定すれば良い。

# Python の仮想環境名が長い問題

まず、PROMPT に何も設定していないのに、プロンプトに仮想環境名がついてしまっているのだが、これは pyenv の機能。いつもお世話になってます。助かってます。

今回はそれじゃダメなので、無効化する。

`export VIRTUAL_ENV_DISABLE_PROMPT=1`

これを設定しておけば良い。後は自分で設定する。

pyenv の仮想環境に入ると、`${VIRTUAL_ENV}` という変数に仮想環境名、正確には、仮想環境が置かれているディレクトリの絶対パスが入る。
例えば `hogehoge` という名前の仮想環境ならば、`${HOME}/.pyenv/versions/hogehoge`。
（pyenv を `${HOME}/.pyenv` にインストールした場合）

欲しいのは最後のディレクトリ名だけ。これを変数内の文字列置換でさくっと取り出そう。

## 変数内の文字列置換

置換したいなって時に毎回ググるので、ここでまとめてしまう。

* 文字列先頭からの最短マッチで該当部分を削除
    * `${HOGE#pattern}`
        * 使わない
* 文字列先頭からの最長マッチで該当部分を削除
    * `${HOGE##pattern}`
        * basename, 拡張子の抽出
* 文字列最後からの最短マッチで該当部分を削除
    * `${HOGE%pattern}`
        * dirname, 拡張子を取り除く
* 文字列最後からの最長マッチで該当部分を削除
    * `${HOGE%%pattern}`
        * 最も使わない
* 例）`HOGE="/hoge/moge/fuga.fuga.ext"`
    * `${HOGE##*/}` → `fuga.fuga.ext`：basename
    * `${HOGE##*.}` → `ext`：拡張子の抽出
    * `${HOGE%/*}` → `/hoge/moge`：dirname
    * `${HOGE%.*}` → `/hoge/moge/fuga.fuga`：拡張子を取り除く
    * `${${HOGE##*/}%.*}` → `fuga.fuga`

## cut と rev の活用

余談が長かったが、要するに `${VIRTUAL_ENV##*/}` で仮想環境名が取れる。

しかしこの値は `textcnn-gradcam-WPG28PJj-py3.9` のように poetry が勝手に付けた ID や Python バージョンが入っている。
これを取り除きたい。

poetry が仮想環境名として付ける形は `（プロジェクトのディレクトリ名の _ を - に置き換えたもの）-（IDっぽいもの）-（Python バージョン）` ということは分かっているので、「文字列を `-` で分割した最後の２つを取り除いたもの」を作れば良い。

分割すると言えば `cut` である。
例えば以下のようにすると、一見取り出せたかのように見える。

`echo ${VIRTUAL_ENV##*/} | cut -d- -f-2` → `textcnn-gradcam`

`cut` の `-d-` オプションで `-` を区切り文字とすると指定して、`-f-2` オプションで区切られた文字列の最初から２つ目までを取得する（`-f1-2` と同じ）。

しかしこれではダメだ。プロジェクトのディレクトリ名に `_` もしくは `-` が何個入っているかは決まっていない。「最後から２つ」を取り除きたいのだが、`cut` には「最後から数える」という機能は備わっていない。

そこで `rev` である。`rev` は文字をひっくり返すコマンド。
だから、ひっくり返す → 最初から３つ目以降を取得 → またひっくり返す、とやれば良い。

`echo ${VIRTUAL_ENV##*/} | rev | cut -d- -f3- | rev` → `textcnn-gradcam`

これで完璧。

## 更新するタイミング

しかしまだ終わりではない。

`${WINDOW}` ならば、screen で新しい WINDOW に入る時に `.zshrc` を読み込むので、ただ `.zshrc` に `${WINDOW}` と書いておけば意図したとおりに設定される。

しかし今回はディレクトリを移動する毎に値を変更して欲しい。
`.zshrc` にただ書いただけでは、シェル（ターミナル）を起動した瞬間にしか読み込まれず、ディレクトリを移動してもプロンプトは変わらない。

そこで、ZSH の `add-zsh-hook` というモジュールをロードして、プロンプトが呼び出される前のタイミングの hook である `precmd` に関数をぶら下がらせて、その関数の中で `PROMPT` を更新させれば良い。

# 結果

最終的には、`.zshrc` のプロンプトの設定部分は以下のようになった。

```zsh
export VIRTUAL_ENV_DISABLE_PROMPT=1
autoload -Uz colors
colors
autoload -Uz add-zsh-hook
local p_cdir="%B%F{blue}[%(5~|.../%2~|%~)]%f%b"$'\n'
local p_info="%n@%m${WINDOW:+[${WINDOW}]}"
local p_mark="%B%(?,%F{green},%F{red})%(!,#,>)%f%b"
local p_base=" $p_cdir$p_rhost$p_info $p_mark "
function __precmd_virtual_env() {
    local p_penv="%B%F{cyan}${VIRTUAL_ENV:+(`echo ${VIRTUAL_ENV##*/}|rev|cut -d- -f3-|rev`)}%f%b"
    PROMPT="$p_penv$p_base"
}
add-zsh-hook precmd __precmd_virtual_env
```

自分で設定したので、仮想環境名に色が付けられるようになった。

で、こうなる。

![スクリーンショット 2021-10-11 1.48.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84885/d8cfbf8e-f0ba-ab78-1057-1908ff31696a.png)

とりあえず満足である。

# まとめ

「俺はなんで未だに screen を使おうとしてるんだろう」と疑問に思ってきたので、tmux の設定をしたい気分になっている。（仕事はまだしてない）