Nathan Stien

Abstractions & Applications

Shell Prompt Spider-Sense

Improved Context Awareness through the Use of Color

I like to pack some extra contextual information into my bash prompt, mostly through the use of color.

Prompt Basics

The creaky old UNIX machines I first learned on offered this as the default shell prompt:

$

Today, the situation is considerably improved; here is the default bash prompt in recent versions of Ubuntu:

user@host:/working/path$

This prompt is more helpful because it reminds you of important facts about your environment.

But there is still a lot of room for improvement. Starting from the modern standard prompt, I’ve gradually embellished it to reflect more contextual information. In most cases, though, I have opted to display this information through color rather than adding text – the prompt is already a bit wide for comfort when I am working with a path like ~/projects/NameOfProject/src/main/java/com/acme/crazy/long/package/name/here.

Components of the prompt

A typical prompt for me looks like this:

nathan@hugin:~/blog/posts(master $%)

To break that down a bit:

user@host:/working/path(git-branch status)

Username

It’s very important to know who you are. While the text of the name is certainly sufficient for figuring out which user you are, it’s easy to accidentally ignore it after a while. To combat that, I use color to indicate special conditions.

Hostname

It’s also important to know where you are, so I conditionally highlight my hostnames. Before I did this, I would embarassingly often fail to notice that a given terminal tab was actually connected to a remote shell, which lead to some unnecessary pain.

I have no principled scheme for assigning colors – I simply have a case block that maps colors to particular hosts I deal with. However, I am more likely to use muted tones on personal machines and more striking colors on important production servers. There is also a default color for servers not explicitly mentioned, which reminds me that I’m connected to a VM or a server I don’t log into very often.

user@personal-laptop:~
user@temporary-vm:~
user@prod-server:~

Working Path

The other half of knowing where you are is your current working path. I cannot fathom any shell prompt that does not include the working directory. Even DOS had this.

My use of color here presently distinguishes several cases for the working path:

I recently altered the code to apply this colorization is independently for each directory along the current working path. It’s alreay come in handy, as I’ve been able to “smell” some permissions issues and symlinks I might not have noticed otherwise.

Git Status

If you use git a lot, this one is extemely useful. I have more than once stumbled into a directory and found it to be a git repository by surprise – I would rarely think to preemptively query to see if a given dir is a git repo.

Beyond that, it’s amazingly easy to forget that you’ve been working in a different git branch, and having it printed between every command pretty much eliminates that problem.

It works by calling a bash function __git_ps1, which prepares a string of the form:

branch-name *+$%=

Where the single-character flags have the following meanings:

Flag Meaning
* the working copy is dirty
+ changes have been staged in the index
$ there are stashed changes
% there are untracked files
> local repo is ahead of origin
= local repo is at par with origin
< local repo is behind origin

The untracked files indicator can come in handy when running a build script – if that % suddenly shows up, you probably have some .gitignore additions to make.

If the current working path is not in a git repository, this section does not appear.

Unfortunately, the last time I tried this on Cygwin, it was too slow to use. Launching a processes on Windows is heavier operation than on Linux, but I’m not sure if that could account for the dramatic speed difference. If my situation calls for using Windows, I just have to live without the git stuff.

Prompt Decoration

It’s common to terminate the prompt with a symbol of some kind. Traditionally, the dollar sign ($) is used for regular users, and a octothorpe (#) is used for root. I deviate from tradition slightly by using , a unicode glyph representing Leibniz’ integration symbol.
Like $, it is also a form of the letter ‘S’, but it’s a bit less overloaded with shell-related meanings. It’s also a fairly tall glyph, so it helps separate the prompt from shell commands.

The unicode math symbols, technical symbols, and miscellaneous symbols are pretty good places to start looking for interesting decorations.

Another prompt decoration deviant, Jorge Israel Peña, uses a lambda glyph and an arrow to make his prompt resemble a Haskell lambda expression, which is a pretty cool idea.

λ ~/code/haskell ➜

Debian chroot

While I don’t find myself using chroots very often these days, I still leave in a bit of chroot-indicating code I inherited from some ancient version of Debian. This prefix is only shown when I’m actually in a chroot.

Other Ideas

There are some other possibilities I’ve considered including.

I have experimented with including the current time in the prompt, which seems like it would be handy for logging purposes. But it adds a lot of width, and my prompt is already pretty verbose.

A more concise alternative might simply be to add a command sequence number that increments with each command. There are escape sequences for that (among other things) in the Bash Prompt HOWTO.
Another interesting idea would be a (conditionally colorized) exit status from the previous command.

Some people use a multi-line prompt, with the long part on a separate line from the terminator. This was the default prompt setup in Cygwin when I started (and may still be, for all I know). After trying both ways, I prefer the single-line setup.

If I used a lot of other VCS systems, I would include status code for those. But these days I really only use git and Subversion, and all of my svn use is via git-svn, so I haven’t felt the need.


Appendix: Implementation Details

I use bash, so the specific implementation instructions below are for bash.

Defining the Prompt

The Primary Prompt Variable

The primary bash prompt is controlled by a shell variable called $PS1. You can fill it with both literal text and special sequences that get replaced by contextual information, such as the name of the current user and current working directory. The $PS1 string for the default user@host:~/path$ prompt I mentioned above is simply '\u@\h:\w\$ '. There are many more special sequences listed in the bash man page.

Dynamic Prompts

Setting $PS1 to a static string is sufficient for many prompts. Bash can supply a lot of context-sensitivty through substitution of escapes like \w. But to achieve my fancy per-path-segment highlighting described above, I resorted to rebuilding $PS1 dynamically between each command.

Before drawing the prompt, bash will evaluate any code stored in the $PROMPT_COMMAND variable. Mine looks like this:

export PROMPT_COMMAND='PS1="$(build_prompt)"'

Where build_prompt is a bash function that does all the work of calculating the $PS1 string.

Colorizing Text

Terminal Sequences

Color is achieved by emitting special ANSI terminal sequences that alter the foreground and background color of text. My prompt script simply defines some useful colors as environment variables, which I can use simply by printing their value. I obtain the sequences from a tool called tput, which knows the right sequences for various terminal types.

RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
# etc.

Zero-Width Markers

Bash is not itself smart enough to know about these ANSI sequences, so we have to use special prompt-specific escape sequences, \[ and \], to mark the boundaries of non-printing strings. If you leave these out, bash will miscalculate the width of the prompt, and this can cause nasty drawing problems.

Conditional Color

This requires some bash scripting, usually in the form of case blocks. A typical one for me looks like this:

case "$USER" in
    nathan|nstien|npstien|Nathan|"Nathan Stien") 
        usercolor=$GREEN
        terminator=;;
    root|Administrator) 
        usercolor=$BOLD$RED;
        terminator='#'
        ;;
    *)
        usercolor=$BOLD$YELLOW
        terminator='$'
        ;;
esac

The general pattern is that I inspect various environment variables (including $HOSTNAME and $PWD) and set some new variables to contain the desired colors. Those variables are then used in the prompt definition.

Putting It All Together

I’ve tried to outline some of the general techniques behind my specific prompt, but I think the best way to get the full picture is to go look at the code. My prompt definition is in a separate file .bash_prompt that I source from my main .bashrc.

comments powered by Disqus