My PS1: Confessions of a Bash Illiterate

Full-disclosure: I’ll never win a Bash Script of the Year Award. I occasionally write them, but my skills are pretty basic, so apologies if my attempt to build out a meaningful bash prompt offends your bash sensibilities. Comments are semi-welcomed.

“To truly know a man, first you must know his prompt.”
~Sage.

I’m always intrigued when I see a fellow developer’s terminal prompt, as if I’m peering into his or her nerd soul. If you spend a lot of time in front of a terminal, it’s worthwhile to customize your prompt. Yet I often see people who stick with the system default.

Below is the default on my MacBook, and I honestly can’t imagine using it:

bash
1
2
macbook:~ aloder$ cd ~/checkout/projects/some/project/
macbook:project aloder$

I don’t have many prompt requirements, but the following are non-negotiable must-haves:

  • I can see the full pathname of the current working directory. I have lots of directories named target, bin, webapp, etc., so seeing the full path gives me a better chance of catching myself before doing something stupid due to an incorrect assumption about the current directory. Additionally, when scanning your previous commands, you know the working directory in which each was executed.
  • My cursor is left-justified. Given that I want to see the full-path, if the cursor is on the same line, this shifts the cursor to the right as the path gets longer. Even the default prompt above shifted the cursor to the right because ‘aloder’ is longer than ‘~’. When I look back in my terminal window, I like having each command start in the same place. I also want to give my verbose commands the best chance of fitting on a single line.
  • There’s a blank line before the prompt. This makes it easier to differentiate commands and their output when browsing the scrollback buffer.

Below is a PS1 environment variable that meets the above requirements. This lived in my .bashrc for quite some time.

emacs .bashrc
1
PS1="\n\u@\h: \w\n$ "

This is the prompt it creates:

bash
1
2
aloder@macbook: ~/checkout/projects/some/project
$

This prompt did the trick for a while, but in order to include the git branch as additional context, I later added in a parse_git_branch function and updated my PS1:

emacs .bashrc
1
2
3
4
parse_git_branch() {
    git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/ [\1]/'
}
PS1="\n\u@\h\$(parse_git_branch): \w\n$ "

This is how it looks after cd-ing into a git working directory in which I’ve checked out the ‘master’ branch:

bash
1
2
aloder@macbook [master]: ~/checkout/projects/some/project
$

I was satisfied with this until I started using virtualenv and virtualenvwrapper for my Python development. I wanted to add the current active virtualenv to my prompt context, but I was not a fan of the PS1 update performed by virtualenv’s bin/activate script. I made my own changes but my prompt wouldn’t update the git or virtualenv context after changing directories due to virtualenv’s own PS1 mangling. Rather than figure out what was causing this, I did the following:

  1. Moved all PS1 properties/code into a separate ~/.bashrc_ps1 file.
  2. Updated the virtualenvwrapper $WORKON_HOME/postactivate script to source the new file, which ensures the PS1 is unaffected by virtualenv.
emacs postactivate
1
2
3
4
#!/bin/bash
# This hook is run after every virtualenv is activated.

source ~/.bashrc_ps1

As for the prompt context itself, I wanted it to be terse. I’ve seen some crazy prompts that put way too much info on the screen. Assuming webpy is my active virtualenv and that I’m on the master git branch, I want the context portion of my prompt to look like this:

1
[webpy|master]

If I don’t have a virtualenv activated or am not in a git directory, it should degrade and display only one:

1
2
[webpy]
[master]

For instance, here’s what it looks like when I have a ‘webpy’ virtualenv activated but am not in a git project:

bash
1
2
aloder@macbook [webpy]: ~/checkout/home
$

I also decided that I wanted to display the virtualenv or git branch in a different color if either is non-default; this way, only the non-default values will stand out. For instance, my default virtualenv is named ‘default’, and my typical git branch name is ‘master’.

Below is the full contents of my ~/.bashrc_ps1 file to achieve this:

emacs .bashrc_ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# .bashrc_ps1

export GITBRANCH_DEFAULT=master
export VIRTUALENV_DEFAULT=default

get_gitbranch() {
    git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/'
}

get_virtualenv() {
    if [ -n "$VIRTUAL_ENV" ];
    then
        basename $VIRTUAL_ENV
    fi
}

get_ps1_context() {
    virtualenv=$(get_virtualenv)
    gitbranch=$(get_gitbranch)
    if [ -n "$virtualenv" ] || [ -n "$gitbranch" ];
    then
        ps1="${ps1}["
        if [ -n "$virtualenv" ];
        then
            ps1="${ps1}"
            if [ "$virtualenv" != $VIRTUALENV_DEFAULT ];
            then
                ps1="${ps1}\e[0;32m"
            fi
            ps1="${ps1}${virtualenv}"
            if [ "$virtualenv" != $VIRTUALENV_DEFAULT ];
            then
                ps1="${ps1}\033[0m"
            fi
        fi
        if [ -n "$virtualenv" ] && [ -n "$gitbranch" ];
        then
            ps1="${ps1}|"
        fi
        if [ -n "$gitbranch" ];
        then
            if [ "$gitbranch" != $GITBRANCH_DEFAULT ];
            then
                ps1="${ps1}\e[0;91m"
            fi
            ps1="${ps1}${gitbranch}"
            if [ "$gitbranch" != $GITBRANCH_DEFAULT ];
            then
                ps1="${ps1}\033[0m"
            fi
        fi
        ps1="${ps1}]"
        printf $ps1
    fi
}

PS1="\n\u@\h \$(get_ps1_context): \w\n$ "

Comments