Screwtape's Notepad

How to limit the length of your bash prompt

Under “PROMPTING”, the bash manual describes two codes that can be added to the PS1 variable to insert information about the current working directory into your prompt:

\w
the current working directory, with $HOME abbreviated with a tilde
\W
the basename of the current working directory, with $HOME abbreviated with a tilde

\w is annoying because it shows the complete path from the root of the directory tree to your current location - if you have deeply nested directory structures (and once you start doing actual work on a decent-sized project you probably will) your prompt can get absurdly long.

\W is annoying because it removes all the context from your current location - if you have several projects lying around and they all have a “docs” subdirectory, you’ve got no way of telling them apart without having to run some other command.

Wouldn’t it be good if there was a way to have your prompt show you a level of detail in between “everything” and “nothing”?

The pieces

Getting the current working directory

Because \w and \W are special codes interpreted by bash‘s prompt code, you can’t work on them like ordinary shell variables. One alternative might be to use command expansion with pwd, or (on linux) clever introspection via /proc/self, but it turns out there’s an easier way: bash keeps a record of the current working directory in the magic variable PWD. I call it “magic” because the shell keeps it up-to-date without you having to manually set it every time. See “Shell Variables” in the bash manual for other magic variables that bash maintains.

Trimming a variable

What we really want here is something like Python’s string slicing, where you can say “give me the last n characters of this variable”. Looking at the “Parameter Expansion” section of the bash manual, we discover Substring Expansion:

${parameter:offset}
${parameter:offset:length}
Expands to up to length characters of parameter starting at the character specified by offset. If length is omitted, expands to the substring of parameter starting at the character specified by offset. […] If offset evaluates to a number less than zero, the value is used as an offset from the end of the value of parameter.

Sounds great, let’s try it!

$ echo $PWD
/home/st
$ echo ${PWD: -4}
e/st
$ _

It works! Or does it…

$ echo ${PWD: -10}
$ _

It turns out that if you have a string n characters long, asking for the last (n-1) characters will give you (n-1) characters, asking for the last (n) characters will give you (n) characters, but asking for (n+1) or more characters will give you nothing.

We can get around this by using a temporary variable and the Use Default Values exansion:

${parameter:-word}
If parameter is unset or null, the expansion of word is substituted. Otherwise, the value of parameter is substituted.

(clever readers will note that the Substring Expansion examples above very carefully left a space between the “:” and the negative number, so the shell wouldn’t get them confused with Use Default Values expansion)

Here’s our workaround. First, we grab the last however-many characters (10 in this case) if we can:

$ TEMP_PWD=${PWD: -10}

If PWD was less than 10 characters long, TEMP_PWD will be empty.

If TEMP_PWD is empty, we know PWD was already short enough and we can just set TEMP_PWD to PWD. Otherwise (TMP_PWD is not empty), we must have trimmed something and we can use it as-is:

$ TEMP_PWD=${TEMP_PWD:-$PWD}
$ echo $TEMP_PWD
/home/st
$ _

Hooking into the shell

We have some commands we can run to set up a variable that contains the data we want to display in our prompt, but we need to recalculate it every time the user changes directory. bash doesn’t give us useful event-handlers like that, but under “Shell Variables”, the bash manual describes the PROMPT_COMMAND variable, which can be set to a sequence of commands to be run every time bash is about to display a prompt.

Variables in the prompt

There’s one final trick to setting a custom prompt: if you just set PS1 to include a variable directly, it probably won’t do what you expect:

$ PS1="You are in $PWD: "
You are in /home/st: pwd
/home/st
You are in /home/st: cd /
You are in /home/st: pwd
/
You are in /home/st: _

This is because PWD was expanded at the time you set PS1 - if you want the prompt to include the contents of PWD at the time the prompt is displayed, you have to stop bash from expanding the variables at the time you set PS1. The easiest way is just to use single-quotes instead of double-quotes:

$ PS1='You are in $PWD: '
You are in /Users/st: cd /
You are in /: _

Putting it all together

Here’s what I have in my ~/.bashrc that uses all the things mentioned above, plus a few more that should be adequately described in the comments:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# If PS1 is not set at all, this is not an interactive
# shell and we should not mess with it.
if [ -n "$PS1" ]; then
    # A temporary variable to contain our prompt command
    NEW_PROMPT_COMMAND='
        TRIMMED_PWD=${PWD: -40};
        TRIMMED_PWD=${TRIMMED_PWD:-$PWD}
    '

    # If there's an existing prompt command, let's not 
    # clobber it
    if [ -n "$PROMPT_COMMAND" ]; do
        PROMPT_COMMAND="$PROMPT_COMMAND;$NEW_PROMPT_COMMAND"
    else
        PROMPT_COMMAND="$NEW_PROMPT_COMMAND"
    fi

    # We're done with our temporary variable
    unset NEW_PROMPT_COMMAND

    # Set PS1 with our new variable
    # \h - hostname, \u - username
    PS1='\u@\h:$TRIMMED_PWD\$ '
fi

Note that I’ve boosted the trim length up to 40 characters (half the width of a standard terminal window), and introduced some logic to extend PROMPT_COMMAND if it’s already set. The simple thing to do would have been:

PROMPT_COMMAND="$PROMPT_COMMAND; new prompt command"

…but if PROMPT_COMMAND is not already set, that gives you a PROMPT_COMMAND beginning with a semicolon, and certain versions of bash complain bitterly about such things.