Customizing Your Bash Prompt with PS1
PS1 is the primary prompt string. Bash expands it every time it displays a prompt.
echo $PS1
# \u@\h:\w\$
That produces colton@thinkpad:~$.
The escapes
| Escape | Shows |
|---|---|
\u | Username |
\h | Hostname up to the first dot |
\H | Full hostname |
\w | Working directory, ~ for home |
\W | Basename of the working directory |
\$ | # for root, $ otherwise |
\t | Time, 24-hour |
\n | Newline |
\! | History number |
PS1='\u@\h \W \$ '
Put it in ~/.bashrc to persist. Our environment variables guide covers where shell configuration is read from.
Colour, and the mistake everyone makes
PS1='\[\e[32m\]\u@\h\[\e[0m\]:\[\e[34m\]\w\[\e[0m\]\$ '
The \[ and \] are not optional. They tell bash that what they enclose produces no visible output.
Without them, bash counts the escape sequence characters as part of the prompt’s width. Its idea of where the cursor is becomes wrong, and the symptom is that recalling a long command from history overwrites the prompt, or the line wraps in the wrong place, or editing a long command corrupts the display.
This is the single most common prompt bug, and it appears intermittently, which is why people live with it for months.
\[\e[0m\] reset
\[\e[1m\] bold
\[\e[31m\] red \[\e[91m\] bright red
\[\e[32m\] green \[\e[92m\] bright green
\[\e[33m\] yellow \[\e[93m\] bright yellow
\[\e[34m\] blue \[\e[94m\] bright blue
\[\e[35m\] magenta
\[\e[36m\] cyan
Always reset at the end. An unreset colour bleeds into the command you type and into program output.
Exit status
Showing whether the last command failed is the single most useful addition:
PS1='$(if [ $? -eq 0 ]; then echo "\[\e[32m\]ok"; else echo "\[\e[31m\]!!"; fi)\[\e[0m\] \w \$ '
Use single quotes. Double quotes expand $? once when PS1 is assigned, so you get the same value forever.
Cleaner, using a function:
prompt_status() {
local ex=$?
if [ $ex -ne 0 ]; then
printf '\[\e[31m\][%d]\[\e[0m\] ' "$ex"
fi
}
PS1='$(prompt_status)\u@\h:\w\$ '
That shows nothing on success and the exit code on failure. Our exit codes guide covers what the numbers mean.
Capture $? first. Any command inside the function overwrites it, including the [ test itself.
Git branch
Do not parse git branch output. Git ships a function for this:
# in ~/.bashrc
source /usr/share/git/completion/git-prompt.sh 2>/dev/null \
|| source /usr/lib/git-core/git-sh-prompt 2>/dev/null
GIT_PS1_SHOWDIRTYSTATE=1 # * unstaged, + staged
GIT_PS1_SHOWSTASHSTATE=1 # $ if stashed
GIT_PS1_SHOWUNTRACKEDFILES=1 # % if untracked
GIT_PS1_SHOWUPSTREAM="auto" # < behind, > ahead, = level
PS1='\[\e[32m\]\u@\h\[\e[0m\]:\[\e[34m\]\w\[\e[33m\]$(__git_ps1 " (%s)")\[\e[0m\]\$ '
The path varies by distribution, hence the fallback with 2>/dev/null.
GIT_PS1_SHOWDIRTYSTATE costs performance in a large repository, because it runs git status before every prompt. In a repository with many files that is a visible pause on every command. Disable it there:
git config bash.showDirtyState false
Our Git basics guide covers what the indicators mean.
Multi-line prompts
Deep paths eat the line. Putting the command on its own line helps:
PS1='\[\e[32m\]\u@\h\[\e[0m\] \[\e[34m\]\w\[\e[0m\]$(__git_ps1 " (%s)")\n\$ '
colton@thinkpad ~/projects/linuxdork/src/content (main *)
$
The full width is available for commands regardless of path depth, which is a genuine improvement when you spend time in nested directories.
PROMPT_COMMAND
Runs before the prompt is displayed, which is where dynamic values get computed:
set_prompt() {
local exit_code=$?
local venv=""
[ -n "$VIRTUAL_ENV" ] && venv="(${VIRTUAL_ENV##*/}) "
local status=""
[ $exit_code -ne 0 ] && status="\[\e[31m\][$exit_code]\[\e[0m\] "
PS1="${venv}${status}\[\e[32m\]\u@\h\[\e[0m\]:\[\e[34m\]\w\[\e[0m\]\$ "
}
PROMPT_COMMAND=set_prompt
More readable than a single long string, and it can branch on conditions properly.
Make root obvious
Genuinely useful, not decoration:
if [ "$EUID" -eq 0 ]; then
PS1='\[\e[41;97m\] ROOT \[\e[0m\] \[\e[31m\]\w\[\e[0m\] # '
else
PS1='\[\e[32m\]\u@\h\[\e[0m\]:\[\e[34m\]\w\[\e[0m\]\$ '
fi
A red block that says ROOT prevents the occasional destructive command run in the wrong terminal. Worth putting in /root/.bashrc on every server you administer.
Similarly, colouring production hosts differently from development ones is a cheap safeguard.
Keep it fast
Every external command in the prompt runs before every prompt.
# slow: two subshells per prompt
PS1='$(date +%H:%M) $(whoami) \$ '
# fast: builtin escapes do the same thing
PS1='\t \u \$ '
Measure:
time (for i in {1..50}; do eval "echo \"$PS1\"" >/dev/null; done)
If your shell feels sluggish, the prompt is a strong suspect, particularly in large Git repositories.
Starship
At some point hand-rolling stops being worth it.
curl -sS https://starship.rs/install.sh | sh
echo 'eval "$(starship init bash)"' >> ~/.bashrc
It shows Git state, language versions, cloud context, and command duration automatically, is written in Rust and is fast, works identically across bash, zsh, and fish, and is configured in TOML:
# ~/.config/starship.toml
add_newline = true
[git_status]
disabled = false
[cmd_duration]
min_time = 2000
The argument for still knowing PS1: it works on every machine with nothing installed. When you SSH into a server you will not be customising, understanding the default prompt and being able to adjust it in one line is the useful skill.
Frequently Asked Questions
Why does my prompt wrap text incorrectly after adding colours?
Non-printing escape sequences must be wrapped in the bracket escapes so bash excludes them from its line-length calculation. Without them bash counts the colour codes as visible characters, miscalculates where the cursor is, and overwrites the prompt when you recall a long command.
What is the difference between PS1 and PROMPT_COMMAND?
PS1 is the prompt string itself, expanded each time the prompt is displayed. PROMPT_COMMAND holds a command that bash runs before displaying the prompt, which is how you compute something dynamic and store it in a variable that PS1 then references.
How do I show the current Git branch in my prompt?
Source the git-prompt.sh script shipped with Git and call the __git_ps1 function inside PS1. It is far more reliable than parsing git branch output yourself, and it can also show dirty state, staged changes, and stash presence through environment variables.
Why does my prompt change break in single-user or rescue mode?
Because it depends on something unavailable, usually a function from a file that was not sourced or an external command not on the path. Guard anything external with a check that it exists, so the prompt degrades to something simple rather than printing errors on every line.
Does a complex prompt slow down my shell?
It can, noticeably. Any external command in the prompt runs before every single prompt, and running git status in a large repository adds visible delay. Keeping the prompt to shell builtins where possible, or using a tool written for speed, avoids this.
Should I use Starship instead of writing PS1 by hand?
If you want language versions, Git state, and cloud context displayed automatically, yes. Starship is fast, works across bash, zsh, and fish, and is configured in a readable TOML file. Hand-written PS1 is worth understanding because it works on any machine with no installation.