Customizing Shell Prompts
While working on one of our servers at work, I noticed the shell was using a custom prompt. Looking in the .bashrc
I was surprised to see only the following few lines:
parse_git_branch() {
git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/(\1)/'
}
export PS1='\e[0;32m[\u@\h \e[1;33m\w\e[0;32m \e[0;91m$(parse_git_branch)\e[0;32m]\e[0m\n$'
So, I went down the rabbit hole that is custom shell prompts. I’ve done this dance before, trying Pure Prompt, Powerlevel10k, and Starship. However, due to some issues with these prompts displaying in certain terminal emulators, I moved away from using an installed shell prompt and returned to the a custom zsh prompt. Here are the requirements I have for a prompt:
- It must be fast
- It must be minimal
- It must show the Git branch and status
The first step in configuring my own prompt was understanding that line I found. The bash docs explain the special characters it used on the last line.
# ~/.bashrc
PROMPT_COMMAND="\u@\h \W \$"
In bash, that line will produce the following prompt:
sam@macbook ~ $
However, I use zsh, so I referred to the zsh docs on prompt expansion to create something similar. In zsh, the following line will produce the same prompt as above:
# ~/.zshrc
precmd() { "n@%m %1~ \$" }
NOTE: precmd
is a zsh hook that executes before the prompt is displayed.
Apart from aesthetics, one of the biggest benefits of a custom prompt is the ability to display the git branch and status. Even the prompt from the server at work had this ability. I found solutions using custom functions as well as solutions using the vcs_info function in zsh, the latter of which I’m currently using.
The vcs_info function can display branch names as well as track changes. There is a way to display untracked files, but it makes the prompt visually inconsistent in my opinion. Using a conditional in the precmd function, you can truncate the directory component to display only the current working directory name only when inside a git repo and the full path anywhere else:
# ~/.zshrc
autoload -Uz vcs_info
zstyle ':vcs_info:*' enable git
zstyle ':vcs_info:*' check-for-changes true
zstyle ':vcs_info:git:*' stagedstr '+'
zstyle ':vcs_info:git:*' unstagedstr '*'
zstyle ':vcs_info:git:*' formats ' %F{green}(%b%u%c)%f'
precmd() {
vcs_info
if [[ -n ${vcs_info_msg_0_} ]]; then
setopt prompt_subst; PROMPT=$'\n''%n: %F{blue}%1~%f ${vcs_info_msg_0_} %(?..%F{red})%#%f '
else
PROMPT=$'\n''%n: %F{blue}%~%f %(?..%F{red})%#%f '
fi
}
However, I decided to display just the name of the current working directory in my configuration:
...
precmd() { vcs_info }
setopt prompt_subst; PROMPT=$'\n''%n: %F{blue}%1~%f${vcs_info_msg_0_} %(?..%F{red})%#%f '
Alternatively, if you are using bash, or just prefer this method, Git offers an official prompt script as an alternative to the vsc_info function. This could maximize portability of your configuration if that is something you need. I go this route when working with repositories on servers that use bash.
# ~/.bashrc or ~/.zshrc
if [[ ! -f $HOME/.git-prompt.sh ]]; then
curl https://raw.githubusercontent.com/git/git/refs/heads/master/contrib/completion/git-prompt.sh > $HOME/.git-prompt.sh
fi
source $HOME/.git-prompt.sh
export GIT_PS1_SHOWCOLORHINTS=true
export GIT_PS1_SHOWDIRTYSTATE=true
export GIT_PS1_UNTRACKEDFILES=true
export GIT_PS1_STATESEPARATOR=''
# for bash (use \w to show the entire path)
PS1=$'\n''\u: \e[34m\W$(__git_ps1 " \e[32m(%s\e[32m)") \e[39m\$ '
# for zsh (use %(4~|%-1~/../%2~|%3~) to show an abbreviated working directory or remove the 1 from %1~ to show the entire path)
setopt prompt_subst; PROMPT=$'\n''%n: %F{blue}%1~%f$(__git_ps1 " \e[32m(%s\e[32m)\e[39m") %(?..%F{red})%#%f '
Let’s break down this snippet. First, it checks for the existence of the .git-prompt.sh script and downloads it if it does not exist. Then, it sources the script and sets the environment variables for the prompt. Both PS1 and PROMPT begin with $'\n'
which inserts a newline before the prompt. Then, the user’s name is displayed, the \u
variable for bash and %n
for zsh. If you want to show the hostname, you can add the @ symbol followed by the \h
variable for bash and %m
for zsh before the colon. Then, the current working directory is displayed and colored blue using \e[34m\W
for bash and %F{blue}%1~%f
for zsh. Finally, the function __git_ps1
is called where the format of the git info is configured, " \e[32m(%s\e[32m)"
for both bash and zsh adds parentheses around the git branch and status and colors it all green. The zsh implementation adds \e[39m
here to ensure the green color is removed before the prompt symbol. Then, the prompt symbol is displayed, \e[39m\$
for bash; however, the zsh implementation uses a conditional substring that changes the color of the following characters to red if the exit status is not 0: %(?..%F{red})%#%f
.
All of these configurations will produce the following prompt:
sam: dotfiles (master*) %
Resources
I hope this article can act as a map for all your prompt research, so here are some other resources I found useful when going down this rabbit hole: