Skip to content
January 19, 2020 / gus3

A more expressive Bash prompt

Introduction

Bash provides some interesting built-in specifiers for the prompt strings PS1. Some of them are static, like ‘\a’ for ‘alert’ (Ctrl-G, the bell, beep, or visible flash), or ‘\n’ or \r’ for newline or carriage return. Some specifiers are set during Bash’s startup, like ‘\h’ for the hostname, or ‘\u’ for the username; these don’t change during any particular shell session.

Some are more dynamic, like ‘\t’ for the current time (in 24-hour format) or ‘\w’ for the current working directory. From one command to the next, these are allowed to change. In fact, in the case of ‘\t’ or ‘\T’, they definitely will change.

Motivation

19 years ago, I was distro-hopping, and discovered Gentoo. Its focus, then and now, has always been “make it shine!”. The visible stuff strives to be clean; the running code is intended to run fast (but still correctly). And it ran well enough for me, albeit with an occasional high CPU load to rebuild stuff, until it finally became too fragile and cracked during a failed install of the latest libffi build.

That aside, the first thing I noticed, at my first login, was Gentoo’s multicolored shell prompt. It gave a visible indicator of being a regular user (green) or having EUID 0, the root user (red). It was another example of color being used to indicate context, kind of like syntax highlighting in a text editor, but showing privileges to the user. A red prompt meant basically, “Caution! You can wreck this system, and I’ll let you,” while a green prompt says, “go ahead, you can only screw up your own stuff.” It’s kind of a broad interpretation, but that’s how I understood it at the time. If you’d like to explore the possibilities of a programmable Bash prompt, look here for some great examples.

So there are two things to understand about this post:

  1. For the last 8 years, my parents and I have used only Slackware Linux. Not to slight other Linux distros, but Slackware is the system I truly understand. And Slackware keeps things as “plain vanilla” as possible, so naturally the Bash command prompts in Slackware are just nice, but basic:

    user@example:~/letters$

    or

    root@example:~#

    No colors, nothing beyond the basic Bash prompt capabilities in any regular CLI terminal.
  1. In the past few years, my programming habits have distilled into “the language required by the assignment, then Bash if possible, finally C.” As a scripting language, Bash is enormously flexible, to the point that it can re-define its own behavior interactively. This is the same spirit of programming as GNU Emacs and the Xerox Alto.

What do I want in my prompt? Well, I’d like to see the result of the last thing I did, or tried to do. It’s no problem to show the exit status in PS1; you can embed ‘\?’ in the PS1 shell variable. But I want more power over it. MORE POWER! (evil laugh) Specifically:

I want the exit status in color. If it’s 0, that means the thing I typed finished successfully, so I want it in green (think “The Fifth Element”). If it didn’t work out, for any reason, I want to show it in red.

And another thing: if I can make the exit status colorful, I can make the user’s current info (name@host:pwd$) bold in case the EUID is root. Just as a nice touch.

The basic format is now something like this:

\? \u@\h:\w\$

but I want to colorize it, every time it appears. This means using Bash’s PROMPT_COMMAND variable to build PS1.

Some notes

It took a couple weeks of occasional research, experiments, and failures, until I settled on a simple approach:

PROMPT_COMMAND="PS1=\"\$(build_prompt)\""

where build_prompt is a shell function that builds the contents of PS1, every time. This did simplify the mental model somewhat, but you’ll see that it caused other complications.

As a convenience, I decided to save the original prompt into PS1ORIG, to restore it later if the user requests (or if the build_prompt function goes off the rails and corrupts the screen). It’s a simple matter to restore PS1 if necessary:

PS1="$PS1ORIG" ; unset PROMPT_COMMAND

And just like that, no more calling build_prompt().

For the script to work properly, it has to be invoked properly, in the current shell. It makes no sense to invoke it as a command, so the sha-bang header is

#! /usr/bin/env false

in order for nothing to happen, while returning an unsuccessful status (to indicate improper invocation, if needed). This step clearly shows how I’m over-thinking this exercise.

Finally, I’m using ANSI color escape sequences, but a better approach might be to use “$(tput $color)”.

Making it happen

One lesson from this exercise is how Bash manages printing vs. non-printing characters in PS1. Bash uses the GNU readline library to manage both the command line history, and editing the current command input. Since terminal escape sequences don’t advance the cursor, using them in PS1 requires that they be enclosed in ‘\[‘ and ‘\]’. A basic example, to clear all text attributes, is “ESC-[-0-m”, which would look like this in the context of PS1:

‘\[\e[0m\]Hi! \$’

Looking at each character:

  • \[ alerts Bash that the following stuff doesn’t actually print anything.
  • \e is the Escape character.
  • The following ‘[‘ starts an ANSI character-control sequence.
  • 0 is a modifier for whatever follows.
  • m concludes the sequence.
  • \] tells Bash that the non-printing stuff is done.
  • Finally, the ‘Hi!’, space and ‘\$’ do actually print 5 characters.

The \[ and \] pair shows Bash what doesn’t print anything on the terminal. In this case:

  • Four characters don’t print, the “ESC-[-0-m” sequence, so we enclose them in \[ and \] for Bash.
  • The following five characters do print: ‘Hi! $’

The “simple approach” I decided to use brought on a serious case of “leaning toothpick syndrome.” I’ve put some ANSI color sequences into Bash variables, for use later in an “echo -e …” command. Take the above “Hi!” example, and re-work it a bit:

‘\\[\\e[0m;\\]Hi! \\$’

That’s what you have to pass to “echo -e” in order to get it to print properly.

Why all this concern about printing? Because that’s how you return arbitrary information from a Bash function. The “return” keyword can pass any numeric value from 0 to 127 back to the caller; anything else has to be printed by the function, then captured by the caller. (Okay, there are more sophisticated ways, using “read”, or storing lots of data in a file… Those aren’t pertinent to this exercise.) Basically, a line of Bash that looks like:

FINAL=”$(get_something)”

and get_something () has a line like:

echo -e “\\e[31mHa!\\e[0m”

the double-backslash turns into a single backslash:

FINAL=”\e[31mHa!\e[0m”

which can then be re-used in another “echo”:

echo “$FINAL”

which will print an actual “Ha!” in red. Leaning toothpicks, indeed. (Side note: this also leaves color codes in the Bash environment. You can type “set” as a command, then scroll back to spot the color.)

Some further things

Initially, I wanted to make it easy to recover from a mis-formatted PS1, so I included a function, default_prompt (), which could restore the original PS1. It didn’t work so well at first, but it matured along with the general script. IOW, once I figured out why my PS1 wasn’t working, I fixed the default_prompt () function as well.

The first time the script is executed in the shell, it saves the (default) PS1 into PS1ORIG. Any later time, if the user types

default_prompt

then PS1 is reset from PS1ORIG and PROMPT_COMMAND gets cleared out. At least, that’s how I wanted it to work.

The big trouble came in trying to inherit the custom environment, PS1ORIG, PROMPT_COMMAND, and the functions. Despite my attempt to keep the code reasonably modular, the effects were turning it into pasta primavera, where every byte was a bit different. Just like the escape sequences mentioned earlier, inheritance became an issue when I ran “su” to become root.

The code

Here’s the actual code I’m using:

#! /usr/bin/env false
# Note, this script should be source'd, not invoked as a command.
# Therefore, invocation as a command returns an error code 1.

if [ -z "$PS1ORIG" ] ; then
  export PS1ORIG="$PS1"
fi

__build_prompt() {
  STATUS=$?

  # double back-slashes b/c we're using "echo -e"
  ERRRES='\\e[0;37;41m' # white on red
  OKRES='\\e[0;32m' # green
  NUSER='' # non-root user, default
  RUSER='\\e[1m' # root user, bold
  P='\\e[0m' # reset to default (plain) text

  # here goes
  if [ $STATUS -eq 0 ] ; then
	RESCOLOR="$OKRES"
  else
	RESCOLOR="$ERRRES"
  fi

  if [ $EUID -eq 0 ] ; then
	UCOLOR="$RUSER"
  else
	UCOLOR="$NUSER"
  fi
  # dump the prompt string for later capture
  # (a serious case of leaning toothpicks)
  echo -en "\\[${RESCOLOR}\\]${STATUS}\\[${P}\\] \\[${UCOLOR}\\]\\u@\\h:\\w\\$ \\[${P}\\]"
}

default_prompt () {
  export PS1="$PS1ORIG"
  unset PROMPT_COMMAND PS1ORIG
}

export PROMPT_COMMAND="PS1=\"\$(__build_prompt)\""
export -f __build_prompt default_prompt

It’s a result of several hours of trial and (mostly) error, due to quoting rules and escaped backslashes. But this version is working very well for me, so far. The two “export” commands at the end, plus the one near the top, carry through any invocations of plain “su”. In fact, a plain “su”, which invokes a root shell (UID 0), shows immediately the bold prompt to indicate elevated privileges.

Becoming root through “su -” or through a root login, means the prompt command environment has to be re-sourced manually:

. ~gus3/prompt_command.sh

or some such. But considerable testing of logins, “su”, and “su -” has shown that, for my purposes, it’s working the way I want.

Installation

Now that I have the prompt I want, and I’ve gotten it to work with both non-root and root users, how can I make it a standard part of the shell profile on my desktop? Well, I could incorporate it into my ~/.bash_profile for both gus3 and root, but that seems a bit redundant. I have a working script, ready to source into any running Bash shell.

If I owned a van, I’d be allowed to add a hitch, so I could tow a trailer behind it. Well, I own this computer, so I can add things to the shell profile management in /etc/profile.d. I put this in an executable file called /etc/profile.d/bash_prompt.sh:

# If the user has an executable ~/.bash_prompt script:
if [ -x ~/.bash_prompt ] ; then
. ~/.bash_prompt
fi

And then I put my custom script into ~/.bash_prompt and /root/.bash_prompt, making both of them executable as well. I can disable any of them whenever I want/need, with a simple “chmod -x”. This gives me a custom prompt for both my regular account and the root account, plus whatever other users I add (not likely). It uses a standard Bash profile management paradigm, and it works superbly on my Slackware desktop. Of course, your global profile configuration directory might be different.

I’ve tested this setup using the Linux text console, xterm, uxterm, konsole, koi8rxterm, and xfce4-terminal. So far, It Works For Me.

Conclusion

Immediate, colorful notification of unsuccessful commands is something I’ve wanted for a while. I’d searched the web for an example of dynamic prompt colors in Bash, but the only one (that I could find) that demonstrates dynamic prompt coloring is this one. A better, more general approach might be to use $(tput setf green) instead of direct ANSI color sequences. I’ll leave that as “an exercise for the reader.”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: