bashプロンプトの表示をNerdfontsで格好良くする


無限人がやってるPowerLineのあのパンくずリスト型の表示が格好いいアレです。

最近は色々なshがあるしプラグイン的な形でサクッと導入できる中、わざわざbashでひいこらやる必要も無いと思うけど、他のshを導入するのは面倒くさいし、それはともかく自分用にカスタマイズしたいなと思ったので頑張りました。

作ってる途中で色々知見を得たので、備忘録的な。

格好いい画像

筆者環境

WindowsTerminal (fontには@tawara_氏のHackGenNerd35 Consoleを利用)
WSL2 (Ubuntu 18.04.5)
bash 4.4.20
これと完全一致じゃなくても別にどうにかなるとは思います

HackGen35の場合全角文字が絡むとカーソル位置がおかしくなるので、そのときはHackGenNerd Consoleの方を選んであげてください。
三角が行の高さと合わないときは文字サイズを調整してください。(12とか?)

とにかくソースコード

nerdps1.sh
#!/bin/bash
set -euC        # bash option

declare -l DSPCOLOR="reset"

COLORCHANGE() {
  echo -e -n "${ESC}"
  if [ "$1" = "back" ]; then
    # background color
    printf "\033[4"
  else
    # charactor color
    printf "\033[3"
  fi
  case "$2" in
    "red" )         printf "8;2;255;0;15m";;
    "green" )       printf "8;2;0;145;64m";;
    "yellow" )      printf "8;2;250;191;20m";;
    "blue"  )       printf "8;2;0;0;255m";;
    "purple" )      printf "8;2;146;7;131m";;
    "cyan" )        printf "8;2;0;160;233m";;
    "gray" )        printf "8;2;229;229;229m";;
    "white" )       printf "8;2;255;255;255m";;
    "black" )       printf "8;2;0;0;0m";;
    * )             printf "9m";;
  esac
}
COLORCHANGEFROMBACK() {
  case "$1" in
    "white" | "gray")
      COLORCHANGE "chara" "black";;
    * )
      COLORCHANGE "chara" "white";;
  esac
}
# make bar like powershell
TOPICCHANGE() {
  # > color
  local isUsed=true
  if [ "$DSPCOLOR" = "reset" ]; then
    echo -n ""
    isUsed=false
  else
    echo -n " "
  fi
  # > background color
  COLORCHANGE "back" "$1"
  # > color

  COLORCHANGE "chara" "$DSPCOLOR"
  # >
  if "${isUsed}"; then
    echo -n ""
    COLORCHANGE "chara" "reset"
  fi
  echo -n " "
  COLORCHANGEFROMBACK "$1"
  DSPCOLOR="$1"
}

export VIRTUAL_ENV_DISABLE_PROMPT=1 # pythonのvenv仮想環境でPS1を書き換えさせない
nerdPS1() {
  local userName="$1"
  # if userName yourname, use short name
  # [TODO] Change YOUR-USER-NAME
  if [[ $userName == "YOUR-USER-NAME" ]]; then
    userName="🥦" # terminalによってはカラーフォント絵文字も使える。自分っぽいものに置き換えよう
  fi
  local hostName="$2"
  # if hostName ..
  # [TODO] Change YOUR-HOST-NAME
  if [[ $hostName == "YOUR-HOST-NAME" ]]; then
    hostName="" # \uf878 nf-mdi-monitor 一番ホストっぽかった
  fi
  local pwdInfo="$3"

  # chroot
  # とりあえず書き足しておいたけどvenvと関係なかった。ここのは動作検証してないので……
  if [[ -v debian_chroot ]]; then
    TOPICCHANGE "purple"
    echo -e -n "\uf306 $debian_chroot" #  nf-linux-debian 
  fi
  # (optional) python venv
  if [[ -v VIRTUAL_ENV ]]; then
    local PYTHON_VER="$(python -V)"
    local PYTHON_ENVNAME="$(basename $VIRTUAL_ENV)"
    TOPICCHANGE "cyan"
    # for remove uniquename (pipenv hoge-{uniquename})
    echo -e -n "\ue235 ${PYTHON_VER#Python } ${PYTHON_ENVNAME%-*}" #  nf-fae-python 一番見やすいPythonロゴ
  fi

   # host
  TOPICCHANGE "blue"
  echo -n "$userName@$hostName"
  # pwd
  TOPICCHANGE "gray"
  echo -e -n "\ue5ff $pwdInfo" #  nf-custom-folder フォルダアイコン

 # (optional) git
  # [TODO] `source git-prompt.sh` (you have to download or find)
  if [[ "$(uname -r)" == *microsoft* && "$pwdInfo" =~ ^/mnt/ ]]; then
   # Git is too slow in WSLdir
   :
  else
    if git status --ignore-submodules &>/dev/null; then
      # You Use Git
      local gitps1="$(__git_ps1)"
      if [[ $gitps1 =~ [*+?%] ]]; then
        TOPICCHANGE "yellow"
      else
      TOPICCHANGE "green"
      fi
      echo -e -n "\ue725 $gitps1" #  nf-dev-git_branch 一番見やすかったGitぽいアイコン
    fi
  fi
  TOPICCHANGE "reset" # 忘れずに
}

# 右端に時刻を表示
# コピペ https://orebibou.com/ja/home/201810/20181002_001/
__command_rprompt() {
  local rprompt=$(date "+%Y/%m/%d %H:%M:%S")

  local num=$(($COLUMNS - ${#rprompt} - 2))
  printf "%${num}s$rprompt\\r" ''
}
set +e # これが無いと、プロンプトで実行したコマンドにエラーが有った時に動かなくなる

PS1='$(nerdPS1 \u \h \w)\n\$ '
PROMPT_COMMAND=__command_rprompt
.bashrc
# ...
if [ -e ~/nerdps1.sh ]; then
  source ~/nerdps1.sh
fi
source ~/.git-prompt.sh
GIT_PS1_SHOWDIRTYSTATE=true
GIT_PS1_SHOWUPSTREAM=true
GIT_PS1_SHOWUNTRACKEDFILES=true
GIT_PS1_SHOWSTASHSTATE=true
GIT_PS1_COMPRESSSPARSESTATE=true # 増えた
GIT_PS1_STATESEPARATOR=' '

# ...

説明

プロンプトの表示の変更

一般に見えているホスト名とか書いてある部分は環境変数PS1で指定されています。この内容は1行実行ごとに解釈して都度都度実行されます。
後述のC関数を1つのパンくずごとに実行させたかったのですが、これをそのままCに埋め込むと思うように動かない(同時に処理される?)ので、今回その内容について全て関数に投げています。

Nerdfonts

powerline用のフォントを始めとする様々なアイコンフォントが含まれたフォントです。外字領域を利用しているいて、Nerdfontsと他の一般的なfontを合成することもできるため、様々な派生があります。

パンくずの仕組み

そもそもあのパンくずは、Nerdfontsに含まれるnf-pl-left_hard_dividerという文字を、文字色をそれ以前の背景色にすることで表現しています。
今回DSPCOLORという変数で以前の色を管理し-C-TOPICCHANGEという変数で、背景色の変更とnf-pl-left_hard_dividerの表示を行っています。
一元管理しているので、好きなようにパンくずを生やしたり消したりできます。
-変数名の汚染は少し心配-
追記(2020/09/14) 変数名変えておきました。ついでに色を処理する関数COLORCHANGEを別に用意しました。

色を変える仕組み

\e[{hoge}mの形で入力する(制御文字を使うためechoには-eオプションが必要)することで、{hoge}に応じた色変更が行えます。具体的な記入例は詳しい説明がいくらでも他の人の記事にあるので任せます。
\e\033は同じ意味ですが、後者のほうが互換性が良いみたいです

ユーザ名とホスト名のアイコン化

長ったるいのは嫌だけど非表示にするのも何かあった時アレだな、ということで、普段のものに一致していたら1字に書き換えるようにしておきます。

python venv実行中の表示

pythonにはバージョン管理を司るvenvという機能がありますが、デフォルトだと環境名をPS1に埋め込みやがります。埋め込むかどうかは環境変数VIRTUAL_ENV_DISABLE_PROMPTで指定できるし、仮想環境名はbasename $VIRTUALENVで取得できます。
pipenvを使う時、環境作成時のユニークな値がこれの後ろに付いてきてしまうので、ハイフン以後を削除する設定しています。

git の表示

色々細々した項目について場合分けするのが大変そうですが、公式のgit-prompt.shというのがいい感じに処理してくれるので、これを見つけてきて読み込みましょう。ブランチ名と状況を出力してくれる関数__git_ps1を含んでいます。
これの出力で変更の有る無しを判断しています。
他の記事ではあまり書いてませんでしたが、最近ではGIT_PS1_COMPRESSSPARSESTATEについても設定する必要があるようです。

ただ、WSLにおいて、通常のWindowsのファイルにこれを実行しようとすると明らかに時間がかかるようになってしまいます。__git_ps1の仕業です。いつか改善されることを期待して。

追記(2020/09/14)

試しに使っててあまりの遅さに耐えられなくなったので、/mnt/以下で動かないようにしました。そもそもmnt以下でgit statusが激重でした。

右端に時刻を表示

bashでPROMPT_COMMANDを利用して右プロンプトを出力させるという記事のものが今回文句なく動いてくれたのでこれをそのまま使用しました。
もうちょっと飾っても良いかもしれない。

やらかしたとき

これで遊んでると.bashrcにバグを埋め込んで起動できなくなることが有るので、気をつけましょう。
WSLの場合は、cmdなりPowerShellなりからWSL内部のbashをrcを読みこまずに実行することができます。
bash
wsl.exe -e bash --norc

.bashrcでsourceを埋め込む前にとりあえずテストして確認しましょう。(先達の知恵)

最後に

パンくず(そういう言い方で良いのか?)再現の記事は多いけど、このスクリプトでは割と楽に好きな色のパンくずを追加できるので、色々各々の状況に応じた形にカスタムしやすいと思います。作ってる内に互換性への意識がおざなりになっちゃったけど……

参考