Bash Prompt #5: Singletons Prompt

Bash Prompt Index

I had an idea a while ago that one of the biggest annoyances of the standard prompt is the length of the full name of the working directory. Some people use the Bash \W prompt string escape, which is just the last directory name instead of \w which is the full path, but I wanted something in between the two because the final directory name can be something common. If you work with Ansible, you're familiar with this: every project has a folder called "tasks/" and how do you know which project you're in from that? I thought it would be good if we could have the first letter of every directory except the last one ... although the idea requires some specialty caveats, such as if the first character of a directory name is a "." I want to preserve one more letter, and if $HOME is part of the working directory, I want it replaced with "~".

Let's take the construction in steps.

First Version

PS1="\$ "
PROMPT_COMMAND="echo \"\${PWD}\" | sed -e 's@'${HOME}'@~@' -E -e 's@(\.?[^/])[^/]*(/)@\1\2@g'"

I freely admit I don't have a great grasp on that second sed expression: I got assistance from William and Angelo. The first expression replaces $HOME with "~", the second replaces directory names with the first letter of the name - unless there's a dot, in which case that's saved too.

You can paste these two lines in a terminal and you should see something like this:

~$ cd /usr/bin
/u/bin$ cd ~/.config/autostart
~/.c/autostart$

Second Version

From here on I'd recommend copying the commands into a text file, and then sourcing the file: it becomes trickier to paste some of the more complex commands below directly at the prompt.

The next revision is just to chop off the last directory name:

prompt_command ()
{
    newPWD=$(echo "${PWD/${HOME}/\~}" | sed -E -e 's@(\.?[^/])[^/]*(/)@\1\2@g' -e 's/[^/~]*$//' );
    export newPWD
}
PROMPT_COMMAND=prompt_command
PS1="\${newPWD}\W\$ "

This raises the question: why remove the last directory name when you're only going to replace it with \W which is practically the same? The answer is so we can colourize that last name separately in the next step. This version of the prompt also has a flaw: when you're in your $HOME, it shows "~~" instead of just "~" as it should. We'll fix that.

Third Version

In one big change, we'll add colourization and a couple new features. If the directory you're in isn't writeable, the final directory name will be colourized. And we'll show the return value (colourized, differently) if it's anything other than zero. And just to get fancy and a bit garish, we'll colourize all the slashes in the directory path red.

BG_YELLOW="\[\e[43m\]"
BLACK="\[\e[1;30m\]"
BG_GRAY="\[\e[47m\]"
RED="\[\e[1;31m\]"
NOCOLOUR="\[\e[0;0m\]"

prompt_command ()
{
    # record the return value from the last command before we run other
    # commands (which would modify it):
    _p_retval="$?"
    if (( _p_retval == 0 ))
    then
        # if the return value is zero, discard it:
        _p_retval=""
    fi
    newPWD=$(echo "${PWD/${HOME}/\~}" | sed -E -e 's@(\.?[^/])[^/]*(/)@\1\2@g' -e 's/[^/~]*$//' );
    export newPWD
}
PROMPT_COMMAND=prompt_command
# use Bash parameter expansion to replace each slash with a red slash:
PS1="$( echo -e "\${newPWD//\//$(echo -e ${RED})\/$(echo -e ${NOCOLOUR})}" )"
# check if $PWD is writeable, if not, insert a colour code:
PS1="${PS1}\$( if ! [ -w \"\${PWD}\" ]; then echo -en \"${BG_YELLOW}${BLACK}\"; fi )"
# if we're in $HOME, don't double-up on the tildes:
PS1="${PS1}\$( if [ \"\${PWD}\" != \"${HOME}\" ]; then echo \"\W\"; fi )"
# colourize the retval, which will only show if non-zero:
PS1="${PS1}${BG_GRAY}${RED}\${_p_retval}${NOCOLOUR}\$ "