What’s in your Bash Prompt?

I spend a great deal of time on the command line. That’s how I usually manipulate files, do little maintenance tasks, run tests from and etc. Because of this I like to make that environment nice to look at. I typically set up my console terminals and editors with the excellent and rather popular Solarized theme. I like it because it provides an unified look and feel to my command line regardless of the client or the platform. It works in Konsole, iTerm2 and even in PuTTY on windows via a neat registry hack. Not to mention that the Solarized Dark theme is actually one of the very few light text on dark background combinations that doesn’t make my eyes hurt.

Last week we talked about various ways to streamline and customize Bash. That post didn’t include an important area: the command prompt itself. That’s because it is a complex topic in and of itself.

There are prompt purists out there who swear by the standard, minimalistic prompt like:

PS1="\$ "

This is enough to distinguish whether or not you are logged in as root (though I don’t recall the last time I had to log in anywhere as root because of the magical properties of sudo. But, that’s way to simple for me. For example, I tend to forget where I am in the file system quite easily. Sure, I could always just run pwd to see my location, but why do that if I could simply display it in my prompt using the \w tag like this:

PS1="\w \$ "

While you’re adding crap to your prompt, you might as well go all the way and use the standard prompt string you commonly see on Ubuntu boxes:

PS1="\u@\h:\w \$"

This typically evaluates to the boring username@host:~ $ style prompt. Purists and unix Longbeards scoff at this. They are like “does your username and host change so often you need it in the prompt?” The answer to this is of course… Yes. Kinda. On a typical day I have bunch of terminals open, each of them logged into a different machine. Having my username and host on the prompt lets me ascertain at a glance weather I am logged into the production server, the test server, my home server, my other home server, my vagrant instance or if I’m just on the local prompt. So I actually really need these things there. I also need them not to be this ugly.

Bash lets you use ANSI color codes to colorize your output, using the rather ugly \033 sequence followed by a color code. Unfortunately using these color codes directly turns your prompt into a completely unreadable mess. Check this out:

PS1="\[\033[32m\]\u\[\033[0m\]@\[\033[32m\]\h\[\033[0m\]:\[\033[33m\]\w\[\033[0m\] \$ "

I absolutely hate this. I edit my prompt only every once in a blue moon, so I don’t memorize these codes. Every time I have to make a change to the prompt I have to make effort to actually parse this ungodly string. So I like to define my color codes like this:

##-ANSI-COLOR-CODES-##
Color_Off="\[\033[0m\]"
###-Regular-###
Red="\[\033[0;31m\]"
Green="\[\033[0;32m\]"
Purple="\[\033[0;35\]"
####-Bold-####
BRed="\[\033[1;31m\]"
BPurple="\[\033[1;35m\]"

Then I can just write my prompt with readable variable names. Please tell me that this is not much, much better than the thorn-bush-tangle looking line from paragraph above:

PS1+="$BRed\u$Color_Off@$BRed\h$Color_Off:$BPurple\w$Color_Off \$"

This gives you a prompt that looks like the one you might have seen in my PHP or bash tutorials from the past – a red username/host part and purple path. It is simple, aesthetic and useful. Lately though I’ve been kinda jealous of the people with tricked out prompts that return the status of the last command or the status of their git repository. So I decided to be fancy and write my own.

The status of the last command was pretty easy. You just check the $? variable and set the colors appropriately. I wanted to do it nicely, so I wrote it as a function.

# Status of last command (for prompt)
function __stat() { 
    if [ $? -eq 0 ]; then 
        echo -en "$Green[✔]$Color_Off" 
    else 
        echo -en "$Red[✘]$Color_Off" 
    fi 
}

The git part was little bit more difficult because while most systems have a __git_ps1 function that is to be used specifically in the prompts, it only returns the name of the current branch, but not it’s status. What I really wanted was to have prompt that could tell me whether or not my index is “dirty” or not.

Most of the time when I cd into a git repository, I run git status to see if there are any uncommited or untracked files in there. Wouldn’t it be nice to have that information in the prompt? But how?

Well, I tried couple of different things and settled on a solution I mostly stole from here. It turns out that there is no way to avoid running git status, but you can force your prompt to do it automatically and then parse the results like this:

# Display the branch name of git repository
# Green -> clean
# purple -> untracked files
# red -> files to commit
function __git_prompt() {
 
    local git_status="`git status -unormal 2>&1`"
 
    if ! [[ "$git_status" =~ Not\ a\ git\ repo ]]; then
        if [[ "$git_status" =~ nothing\ to\ commit ]]; then
            local Color_On=$Green
        elif [[ "$git_status" =~ nothing\ added\ to\ commit\ but\ untracked\ files\ present ]]; then
            local Color_On=$Purple
        else
            local Color_On=$Red
        fi
 
        if [[ "$git_status" =~ On\ branch\ ([^[:space:]]+) ]]; then
            branch=${BASH_REMATCH[1]}
        else
            # Detached HEAD. (branch=HEAD is a faster alternative.)
            branch="(`git describe --all --contains --abbrev=4 HEAD 2> /dev/null || echo HEAD`)"
        fi
 
        echo -ne "$Color_On[$branch]$Color_Off "
    fi
}

This is not a perfect solution. It parses what is commonly known as the porcelain – the user readable output that is bound to change. So upgrading git to the next release is likely to break if they decide to change the wording of these prompts a bit. Still, this is by far the fastest solution which utilizes only a single external command (other than bash built-ins).

Now you put it all together like this:

PS1=""
# command status (shows check-mark or red x if last command failed)
PS1+='$(__stat) '$Color_Off
 
# debian chroot stuff (take it or leave it)
PS1+="${debian_chroot:+($debian_chroot)}"
 
# basic information (user@host:path)
PS1+="$BRed\u$Color_Off@$BRed\h$Color_Off:$BPurple\w$Color_Off "
 
# add git display to prompt
PS1+='$(__git_prompt)'$Color_Off
 
# prompt $ or # for root
PS1+="\$ "
export PS1

In a perfect world you ought to have a prompt that looks like this:

My Bash Prompt

My Bash Prompt

Unfortunately, it does not work. Or rather it outputs something that looks like this:

\[\][✔]\[\] luke@firmin:~ \[\][master]\[\] $

The stuff in the middle is fine, but the non-printable character escape codes \[ and \] get printed from within the function calls. Why? Well, internally Bash recognizes these as escape codes when it parses the PS1 prompt variable but the echo command does not recognize them as such. So you can’t use these characters when you echo.

You could always set up your color codes without these characters like this:

Color_Off="\033[0m"
Red="\033[0;31m"
Green="\033[0;32m"
Purple="033[0;35"

Now my functions will work correctly but this causes another issue. If you don’t use the \[ and \] escape codes, Bash treats the ASNI codes as printable characters when it calculates the column count. If your column count is off, then your commands won’t wrap to the next line. Instead they will wrap around and overwrite your prompt. This is endlessly frustrating and annoying. Unfortunately it is a catch 22 – for PS1 prompt to work correctly you must escape color codes. But if you use functions you must echo the results and can’t escape properly.

I spent countless hours fighting with this issue, until I realized that using functions was just not a practical idea. In most scripting environments this would be a sound choice – modular code, encapsulation, etc. But Bash just doesn’t work that way.

So I rewrote it using the PROMPT_COMMAND variable. Normally when you set up your PS1 it gets evaluated when a Bash shell is instantiated. The trick I was using was injecting function literals into that variable, to trick Bash into running them each time it evaluates the prompt. This is hackish, but that’s how most people do it.

There is actually a proper way to set up Bash to build your prompt at run time. What you do is you assign a callback function to PROMPT_COMMAND variable. This function then gets called every time Bash is about to print out your PS1. What’s more important is that this function can manipulate and update the PS1 variable before it gets printed. So as long as you do all your magic in that function, you can append variables directly to PS1 without the need to echo them.

Here is my solution:

# set up command prompt
function __prompt_command()
{
    # capture the exit status of the last command
    EXIT="$?"
    PS1=""
 
    if [ $EXIT -eq 0 ]; then PS1+="\[$Green\][\!]\[$Color_Off\] "; else PS1+="\[$Red\][\!]\[$Color_Off\] "; fi
 
    # if logged in via ssh shows the ip of the client
    if [ -n "$SSH_CLIENT" ]; then PS1+="\[$Yellow\]("${$SSH_CLIENT%% *}")\[$Color_Off\]"; fi
 
    # debian chroot stuff (take it or leave it)
    PS1+="${debian_chroot:+($debian_chroot)}"
 
    # basic information (user@host:path)
    PS1+="\[$BRed\]\u\[$Color_Off\]@\[$BRed\]\h\[$Color_Off\]:\[$BPurple\]\w\[$Color_Off\] "
 
    # check if inside git repo
    local git_status="`git status -unormal 2>&1`"    
    if ! [[ "$git_status" =~ Not\ a\ git\ repo ]]; then
        # parse the porcelain output of git status
        if [[ "$git_status" =~ nothing\ to\ commit ]]; then
            local Color_On=$Green
        elif [[ "$git_status" =~ nothing\ added\ to\ commit\ but\ untracked\ files\ present ]]; then
            local Color_On=$Purple
        else
            local Color_On=$Red
        fi
 
        if [[ "$git_status" =~ On\ branch\ ([^[:space:]]+) ]]; then
            branch=${BASH_REMATCH[1]}
        else
            # Detached HEAD. (branch=HEAD is a faster alternative.)
            branch="(`git describe --all --contains --abbrev=4 HEAD 2> /dev/null || echo HEAD`)"
        fi
 
        # add the result to prompt
        PS1+="\[$Color_On\][$branch]\[$Color_Off\] "
    fi
 
    # prompt $ or # for root
    PS1+="\$ "
}
PROMPT_COMMAND=__prompt_command

The end result looks like this:

Improved Prompt

Improved Prompt

As you can see I sort of abandoned the check-mark and x notation in lieu of command history numbers. Why? Mostly because these characters were causing line wrapping issues in some terminals (notably PuTTY). But also because the history numbers are actually somewhat more useful than simple ok/fail which is effectively communicated by color alone.

In addition I also added a little yellow tag that shows your remote IP if you happen to be connected via SSH like this:

Logging into Remote Systems

Logging into Remote Systems

Why do I need that on my prompt? Well, sometimes it is useful to know your IP – for example not to firewall yourself off from a remote system by accident. It also helps when you do port forwarding or have to play SSH-INCEPTION to get somewhere. For example, if I’m at work I can’t really SSH out of the network because of draconian firewall rules. The only outbound ports that are open to end users are 80 and 443. So if I for some reason I need to log in to one of my Linode boxes or to a university server I first have to SSH home. I run a tiny server listening for SSH connections on port 80 at home for the sole purpose of being my “relay” when I’m at work. So seeing the IP on the prompt gives me an idea where the hell am I logged in from when I suddenly find an open terminal with a live ssh session from 4 hours ago.

How about you? What is on your prompt?

This entry was posted in sysadmin notes and tagged . Bookmark the permalink.



16 Responses to What’s in your Bash Prompt?

  1. FX FRANCE Google Chrome Linux says:

    I used to have roughly the same prompt as you in Bash, but I’ve switched over to Zsh and Oh-My-Zsh!, so now mine looks like below. The (red, but you can’t see it here) asterisk indicates that the Git repo has uncommited things, and the number right after it is the exit code from last command.

    fx@computer-name:/full/path/.../.../ (*) [1]
    > ls -lrt                            master [c22c6d5] modified untracked

    I’m also using Dropbox to sync my dotfiles between computers, so they are always relatively painless to modify ;)

    Reply  |  Quote
  2. Naum UNITED STATES Google Chrome Mac OS says:

    Heh, mine is very simple…

    export PS1='[\u@\h:\w]$ '

    Reply  |  Quote
  3. Wow, I really like the idea of changing the color of the prompt based on the exit status of the previous command. Clever idea.

    Since I work in the world of Solaris zones, I got in the habit of also adding the zone name to the prompt. This was especially handy when working with Trusted Extensions. I also change the prompt color to represent the security level. I found the prompt getting a little too long sometimes to I also added a carriage return.

    If you work on multiple platforms like myself, you can also add a case statement to allow you to have multiple environments based on uname.

    Reply  |  Quote
  4. JuEeHa FINLAND Links Linux says:

    Hello from consoleland! Mine is pretty simple:
    PS1=”`whoami`@`hostname`$ ”
    Oh and I’m using mksh instead of that god-awful bash.

    Reply  |  Quote
  5. reacocard UNITED STATES Google Chrome Linux says:

    I’ve played around with having more information in my prompts before, but it always seems to end up making it harder for me to see the things I really use. Hence I opted for a slight modification of the default: http://i.imgur.com/Dcry9.png
    I find this two-line variant quite handy – it makes it easier to read long cwd paths, has lots of space to type in a command, and makes prompts easier to pick out if you’re scrolling back up through a ream of output. The 0 in front of the prompt is the exit code of the last command – turns red if nonzero.

    Source on github: https://github.com/reacocard/dotfiles/blob/master/.zshrc#L116

    Reply  |  Quote
  6. I worked out my bash prompt about 6 months ago when establishing my dotfiles repository. I hadn’t bothered customizing it before because propagating the configuration was too much of a hassle, but the repository solved that.

    Here’s what it’s looked like the last 6 months,

    http://i.imgur.com/7YXSU.png

    It’s fairly minimal: user/host/cwd. With heavy SSH usage, this is everything I need to know on the fly. I mostly drive Git from within Emacs using Magit, so I don’t need any Git prompt fanciness. It’s colorful so that it doesn’t need delimiters between the prompt items (@, :, etc.), and so that it’s easy to spot the command lines among a bunch of output.

    Reply  |  Quote
  7. Paul SWEDEN Google Chrome Windows says:

    I actually went the minimal route for the prompt:

    export PS1=”> ”

    Primarily I did this because I wanted to maximise the horizontal space. However I like having more information available quickly (e.g. time, where am I, host etc.) so I created an alias in my .alias file that includes detailed information:

    alias s=’echo $(date) : $(whoami)@$(hostname) : $(ps axu | grep “paul” | wc -l) ps : $(pwd)’

    This lets me in two key strokes get out the detailed information I want.

    Reply  |  Quote
  8. Scott Hansen UNITED STATES Mozilla Firefox Linux says:

    I found a nifty little trick that will only print the full prompt if you hit enter twice in a row or when opening a new terminal. Gives more room on the line for the rest of your commands.

    .bashrc snippet here: http://sprunge.us/OQjU
    git-completion-plus.sh: http://sprunge.us/SbRZ

    This adds a nice indication as to what git branch you’re on, how many uncommitted changes and committed changes there are and the current path/hostname. The branch name and the number of changes all change color depending on the status. I like your idea of adding the IP address for remote servers. I only access a few different machines, so I just use different colors for my tmux status bar depending on which machine I’m logged in to (95% of the time I have tmux running).

    firecat53@scotty:~/projects/company/inventory/src (master) 1 1 $

    Scott

    Reply  |  Quote
  9. Chris UNITED KINGDOM Google Chrome Mac OS says:

    I’m often confronted with a lack of screen space, so I tend to go with the minimalistic:
    PS1="\$ "
    as it means you don’t have anything taking up valuable console width.

    Reply  |  Quote
  10. larsmans NETHERLANDS Google Chrome Ubuntu Linux says:

    Checking for exit status 0 isn’t enough; when you type Ctrl+Z to push a command into the background (“stop” it, in POSIX terms), $? gets set to a non-zero value. The solution is to check for

    [ $? -eq 0 -o `kill -l $?` = TSTP ]

    Reply  |  Quote
  11. Aske Olsson Google Chrome Linux says:

    Why don’t you use some of the other git ps1 vars like:

    GIT_PS1_SHOWDIRTYSTATE
    GIT_PS1_SHOWUPSTREAM

    Check them out here

    Reply  |  Quote
  12. LIAR FRANCE Mozilla Firefox Ubuntu Linux says:

    Hey!

    I’ve fighted hours with the bad wrapping of lines due to the use of colors in PS1.

    The best solution I found:
    https://bugs.r-project.org/bugzilla3/show_bug.cgi?id=14800

    And yes the solution is as simple as using 01 and 02 instead of \[ and \]!!!!

    Unfortunately, it is very difficult to find, since it is not discussed in bash texts/blogs/articles/…, but in an R lang bug report!!!

    Hope this helps!

    Reply  |  Quote
  13. D SWEDEN Google Chrome Windows says:

    Brilliant look for the PS1, love the idea.
    Just an FYI for people ending up here using Ubuntu.
    When I tried to use the above it didn’t work for me (Ubuntu 13.10). This is because Ubuntu has been using a different shell since 6.10 (https://wiki.ubuntu.com/DashAsBinSh).
    Basically it means that some of the things (like the substitutions) wont work. I haven’t figured out how to get it to adjust the above code to make it work. But if someone does: post it!

    Reply  |  Quote
  14. John UNITED KINGDOM Google Chrome Linux says:

    Color me unimpressed. The lengths some hipsters will go to in order to feel unique. A prompt is supposed to be functional, not distracting.

    Reply  |  Quote
  15. captnjlp UNITED STATES Google Chrome Mac OS says:

    Thanks for posting this; saved me inordinate amounts of time. However, the SSH_CLIENT line creates a “bad substitution” error on every prompt. Removing the extra dollar sign (and the unescaped quotation marks) fixed it for me:


    if [ -n "$SSH_CLIENT" ]; then PS1+="\[$Yellow\](${SSH_CLIENT%% *})\[$Color_Off\]"; fi

    Reply  |  Quote
  16. captnjlp UNITED STATES Google Chrome Mac OS says:

    Sorry, one more… I was getting weird linewrap issues using the above code (illustrative GIF: http://i.stack.imgur.com/fccjc.gif). Because I had my color codes defined as:

    ##-ANSI-COLOR-CODES-##
    Color_Off="\[33[0m\]"
    ###-Regular-###
    Red="\[33[0;31m\]"
    Green="\[33[0;32m\]"
    Purple="\[33[0;35\]"
    Yellow="\[33[1;33m\]"
    ####-Bold-####
    BRed="\[33[1;31m\]"
    BPurple="\[33[1;35m\]"

    lines like
    PS1+="\[$Green\][\!]\[$Color_Off\] "
    were causing the escape sequences to be double quoted. The article’s a little unclear as to the final color definitions you arrived at (it also doesn’t define $Yellow despite its use in the SSH_CLIENT code). If somebody’s reading this and defines their colors as I do, I’ll save you the trouble of removing the extra escape sequences:


    # set up command prompt
    function __prompt_command()
    {
    # capture the exit status of the last command
    EXIT="$?"
    PS1=""
    if [ $EXIT -eq 0 ]; then PS1+="$Green[\!]$Color_Off "; else PS1+="$Red[\!]$Color_Off "; fi
    # if logged in via ssh shows the ip of the client
    if [ -n "$SSH_CLIENT" ]; then PS1+="$Yellow(${SSH_CLIENT%% *})$Color_Off"; fi
    # debian chroot stuff (take it or leave it)
    PS1+="${debian_chroot:+($debian_chroot)}"
    # basic information (user@host:path)
    PS1+="$BRed\u$Color_Off@$BRed\h$Color_Off:$BPurple\w$Color_Off "
    # check if inside git repo
    local git_status="`git status -unormal 2>&1`"
    if ! [[ "$git_status" =~ Not\ a\ git\ repo ]]; then
    # parse the porcelain output of git status
    if [[ "$git_status" =~ nothing\ to\ commit ]]; then
    local Color_On=$Green
    elif [[ "$git_status" =~ nothing\ added\ to\ commit\ but\ untracked\ files\ present ]]; then
    local Color_On=$Purple
    else
    local Color_On=$Red
    fi
    if [[ "$git_status" =~ On\ branch\ ([^[:space:]]+) ]]; then
    branch=${BASH_REMATCH[1]}
    else
    # Detached HEAD. (branch=HEAD is a faster alternative.)
    branch="(`git describe --all --contains --abbrev=4 HEAD 2> /dev/null || echo HEAD`)"
    fi
    # add the result to prompt
    PS1+="$Color_On[$branch]$Color_Off "
    fi
    # prompt $ or # for root
    PS1+="\$ "
    }
    PROMPT_COMMAND=__prompt_command
    @ captnjlp:

    Reply  |  Quote

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>