zshのmanを見ながらプロンプトを自作してみる

はじめに

こんにちは。42tokyo Advent Calendar 2022の6日目を担当する在校生のrsudoです。

先日、こちらの記事を参考にしてローカルのzshプロンプトを作成しました。

dev.macha795.com

できあがったものは上の記事とほぼ同じになりますが、実装している中でmanを調べてみたのでまとめてみます。
zshのmanはこちらから参照しました。
zsh: The Z Shell Manual

注意点

今回の記事と同じプロンプトを作成する場合、特殊な文字に対応したフォントを導入する必要があります。
私はNerd Fontを使用しています。*1

基礎編

プロンプトを変更するには、zshに使用される変数をいじっていくことになるので、まずはParameters Used By The Shellの章を見てみましょう。

The following parameters are used by the shell. Again, <S> indicates that the parameter is special and <Z> indicates that the parameter does not exist when the shell initializes in sh or ksh emulation mode. *2

次のパラメータは、シェルによって使用されます。 ここでも、<S> はパラメータが特殊であることを示し、<Z> はシェルが sh または ksh エミュレーション モードで初期化されるときにパラメータが存在しないことを示します。

Parameters Used By The Shellの一つであるPROMPTという変数を見てみます。

PROMPT <S> <Z>
PROMPT2 <S> <Z>
PROMPT3 <S> <Z>
PROMPT4 <S> <Z>

Same as PS1, PS2, PS3 and PS4, respectively.

manによるとPROMPTPS1と同じようなので次はPS1を見てみましょう。

PS1 <S>
The primary prompt string, printed before a command is read. It undergoes a special form of expansion before being displayed; see EXPANSION OF PROMPT SEQUENCES in zshmisc(1). The default is '%m%# '.

コマンドが読み取られる前に出力されるプライマリ プロンプト文字列。 表示される前に、特別な形式の拡張が行われます。 zshmisc(1) のプロンプト シーケンスの展開を参照してください。 デフォルトは「%m%#」です。

manの言う通りにプロンプトシーケンスの展開を見てみます。

Prompt sequences undergo a special form of expansion. This type of expansion is also available using the -P option to the print builtin.*3

プロンプト シーケンスは、特別な形で展開されます。 このタイプの拡張は、print ビルトインの -P オプションを使用して利用することもできます。

ビルトインコマンドの print -P で展開結果を見ることができるようなので、実行してみます。

print -P "%~ %D %T"

出力結果

~/dotfiles/zsh 22-11-23 12:58

これだけでも十分カスタマイズできそうですね。
他にも多くの項目があります。
zsh: 13 Prompt Expansion

今回は

  • エスケープシーケンス
  • %~

の2つを扱います。

エスケープシーケンス

%{...%}

Include a string as a literal escape sequence. The string within the braces should not change the cursor position. Brace pairs can nest.

文字列をリテラル エスケープ シーケンスとして含めます。 中括弧内の文字列はカーソル位置を変更してはなりません。 ブレース ペアは入れ子にすることができます。

%~

%~

As %d and %/, but if the current working directory starts with $HOME, that part is replaced by a ‘~’. Furthermore, if it has a named directory as its prefix, that part is replaced by a ‘~’ followed by the name of the directory, but only if the result is shorter than the full path;

%d および %/ と同様ですが、現在の作業ディレクトリが $HOME で始まる場合、その部分は「~」に置き換えられます。 さらに、プレフィックスとして名前付きディレクトリがある場合、その部分は '~' に置き換えられ、その後にディレクトリ名が続きますが、結果がフル パスより短い場合に限ります。

作成したのがこちら

function prompt {
  brace_start='%{'
  brace_end='%}'

  back_light_blue='\e[30;48;5;031m'

  text_white='\e[38;5;255m'
  text_light_blue='\e[38;5;031m'
  text_cyan='\e[38;5;087m'

  reset='%{\e[0m%}'

  triangle='\uE0B0'
  allow='\uf101'

  dir_back_color="${brace_start}${back_light_blue}${brace_end}"
  dir_text_color="${brace_start}${text_white}${brace_end}"
  triangle_color="${brace_start}${text_light_blue}${brace_end}"
  dir="${dir_back_color}${triangle}${dir_text_color} %~${reset}${triangle_color}${triangle}${reset}"

  allow_color="${brace_start}${text_cyan}${brace_end}"
  allow_with_color="${allow_color}${allow}${reset}"

  echo "${dir}\n${allow_with_color} "
}

PROMPT=`prompt`

出力

かっこよくなったのではないでしょうか?

git編

プロンプトにはgitのブランチ名が表示されていると便利ですよね。

今回はプロンプトの右側にこれを表示させることにします。

プロンプトの右側に表示させたい場合はRPROMPTという変数を使用します。

RPROMPT <S>
RPS1 <S>

This prompt is displayed on the right-hand side of the screen when the primary prompt is being displayed on the left. This does not work if the SINGLE_LINE_ZLE option is set. It is expanded in the same way as PS1.

このプロンプトは、プライマリ プロンプトが左側に表示されている場合、画面の右側に表示されます。SINGLE_LINE_ZLE オプションが設定されている場合、これは機能しません。 PS1と同じように拡張されます。

まず、gitブランチの表示/非表示を管理するためにカレントディレクトリがgit管理されているかどうかを判定する必要があります。
これは、副作用のないgitコマンドを実行して、エラーが起きるかどうかで判定することができます。

 # git管理されていないディレクトリの場合
if ! git rev-parse 2> /dev/null; then
  return
fi

あとはgit statusの結果に応じて出力を変えていきます。 出来上がったのがこちら

function git-current-branch {
  branch='\ue0a0'
  green='%{\e[38;5;114m%}'
  red='%{\e[38;5;001m%}'
  yellow='%{\e[38;5;227m%}'
  blue='%{\e[38;5;033m%}'
  reset='%{\e[0m%}'

  if ! git rev-parse 2> /dev/null; then
    # git 管理されていないディレクトリは何も返さない
    return
  fi

  branch_name=`git rev-parse --abbrev-ref HEAD 2> /dev/null`
  st=`git status 2> /dev/null`

  if [[ -n `echo "$st" | grep "^nothing to"` ]]; then
    # クリーンな状態
    branch_status="${green}${branch}"
  elif [[ -n `echo "$st" | grep "^Untracked files"` ]]; then
    # git管理されていないファイルがある
    branch_status="${red}${branch}?"
  elif [[ -n `echo "$st" | grep "^Changes not staged for commit"` ]]; then
    # git add されていないファイルがある
    branch_status="${red}${branch}+"
  elif [[ -n `echo "$st" | grep "^Changes to be committed"` ]]; then
    # git commit されていないファイルがある
    branch_status="${yellow}${branch}!"
  elif [[ -n `echo "$st" | grep "^rebase in progress"` ]]; then
    # コンフリクト
    echo "${red}${branch}!(no branch)${reset}"
    return
  else
    # 上記以外の状態の場合
    branch_status="${blue}${branch}"
  fi

  # ブランチ名を色付きで表示する
  echo "${branch_status}${branch_name}${reset}"
}

setopt prompt_subst
RPROMPT='`git-current-branch`'

注意点

git-current-branchはシングルクォートで囲う必要があります。 シングルクォートで囲われた変数は展開されないという特徴があります。(42でbashを再実装する課題を終えた42生なら既に知っているかもしれませんね)

ビルトインのsetコマンドでパラメータを出力できるので実際に見てみます。

set
(省略)
If no arguments and no ‘--’ are given, then the names and values of all parameters are printed on the standard output. If the only argument is ‘+’, the names of all parameters are printed. *4

引数も '--' も指定されていない場合、すべてのパラメーターの名前と値が標準出力に出力されます。 唯一の引数が '+' の場合、すべてのパラメーターの名前が出力されます。

シングルクォートで囲わない場合


展開後の文字列がRPROMPTに入っているので、ブランチを変更しても同じブランチ名が表示され続ける。

シングルクォートで囲った場合


展開前の関数名がRPROMPTに入っているので毎回関数が実行される。

(追記)
setopt prompt_substこちらも必要でした。

PROMPT_SUBST <K> <S>

If set, parameter expansion, command substitution and arithmetic expansion are performed in prompts. Substitutions within prompts do not affect the command status.

設定されている場合、パラメーター展開、コマンド置換、および算術展開がプロンプトで実行されます。 プロンプト内の置換は、コマンド ステータスには影響しません。

おまけ

コマンド実行ごとに改行を出力してほしい場合はprecmd関数を使用します。

precmd
Executed before each prompt. Note that precommand functions are not re-executed simply because the command line is redrawn, as happens, for example, when a notification about an exiting job is displayed.*5

各プロンプトの前に実行されます。 ジョブの終了に関する通知が表示される場合など、コマンド ラインが再描画されるという理由だけでプリコマンド関数が再実行されるわけではないことに注意してください。

function precmd() {
  # プロセスの最初のプロンプトでなかったら改行を出力
  if [ -z "$NEW_LINE_BEFORE_PROMPT" ]; then
    NEW_LINE_BEFORE_PROMPT=1
  elif [ "$NEW_LINE_BEFORE_PROMPT" -eq 1 ]; then
    echo ""
  fi
}

まとめ

以前はOh My Zsh*6 を使用してプロンプトを変更していたのですが、自分でカスタマイズすることができて楽しかったです。

最後にまとめたものを残しておきます。

function prompt {
  brace_start='%{'
  brace_end='%}'

  back_light_blue='\e[30;48;5;031m'

  text_white='\e[38;5;255m'
  text_light_blue='\e[38;5;031m'
  text_cyan='\e[38;5;087m'

  reset='%{\e[0m%}'

  triangle='\uE0B0'
  allow='\uf101'

  dir_back_color="${brace_start}${back_light_blue}${brace_end}"
  dir_text_color="${brace_start}${text_white}${brace_end}"
  triangle_color="${brace_start}${text_light_blue}${brace_end}"
  dir="${dir_back_color}${triangle}${dir_text_color} %~${reset}${triangle_color}${triangle}${reset}"

  allow_color="${brace_start}${text_cyan}${brace_end}"
  allow_with_color="${allow_color}${allow}${reset}"

  echo "${dir} \n${allow_with_color} "
}

# git ブランチ名を色付きで表示させる
function git-current-branch {
  branch='\ue0a0'
  green='%{\e[38;5;114m%}'
  red='%{\e[38;5;001m%}'
  yellow='%{\e[38;5;227m%}'
  blue='%{\e[38;5;033m%}'
  reset='%{\e[0m%}'

  # ブランチマーク
  if ! git rev-parse 2> /dev/null; then
    # git 管理されていないディレクトリは何も返さない
    return
  fi

  branch_name=`git rev-parse --abbrev-ref HEAD 2> /dev/null`
  st=`git status 2> /dev/null`

  if [[ -n `echo "$st" | grep "^nothing to"` ]]; then
    # 全て commit されてクリーンな状態
    branch_status="${green}${branch}"
  elif [[ -n `echo "$st" | grep "^Untracked files"` ]]; then
    # git 管理されていないファイルがある状態
    branch_status="${red}${branch}?"
  elif [[ -n `echo "$st" | grep "^Changes not staged for commit"` ]]; then
    # git add されていないファイルがある状態
    branch_status="${red}${branch}+"
  elif [[ -n `echo "$st" | grep "^Changes to be committed"` ]]; then
    # git commit されていないファイルがある状態
    branch_status="${yellow}${branch}!"
  elif [[ -n `echo "$st" | grep "^rebase in progress"` ]]; then
    # コンフリクトが起こった状態
    echo "${red}${branch}!(no branch)${reset}"
    return
  else
    # 上記以外の状態の場合
    branch_status="${blue}${branch}"
  fi

  # ブランチ名を色付きで表示する
  echo "${branch_status}$branch_name${reset}"
}

PROMPT=`prompt`
RPROMPT='`git-current-branch`'

# コマンドの実行ごとに改行
function precmd() {
  if [ -z "$NEW_LINE_BEFORE_PROMPT" ]; then
    NEW_LINE_BEFORE_PROMPT=1
  elif [ "$NEW_LINE_BEFORE_PROMPT" -eq 1 ]; then
    echo ""
  fi
}

明日は、@LabPixelさんが「高校生が沖縄でのセキュリティ大会Hardeningに行ってきた!」という記事を書いてくれるようなので、そちらの記事もお楽しみに!