featured

By Cris EwingJanuary 24, 2014

Bash for Fun & Profit

A few enhancements can improve your experience working in a shell.

I like the command line.

I enjoy the control that using a shell gives me over my daily work environment. And I like that I can do most things without needing my mouse. The less often I remove my hands from my keyboard, the faster my work goes.

But there are a few things I miss in a standard command-line environment.

What was that name, again?

For example, bash offers tab completion. But that doesn’t extend to interactions with git. Wouldn’t it be nice if I could tab-complete git commands or the names of my remotes and branches?

Well, I can.

The folks who create such things have been kind enough to provide a shell script that sets this up. And it’s not hard to install.

The script is called git-completion and it’s available in bash, tcsh and zsh flavors.

To use it, download the version of the script that corresponds to your preferred shell from the tag of the git repo that corresponds to the version of git you are using. I’ve got git 1.8.4.2 installed on my machine, so this is the version for me. I put it in my home directory:

$ cd
$ curl https://raw.github.com/git/git/v1.8.4.2/contrib/completion/git-completion.bash -o .git-completion.bash

Then I source it from my .profile file:

source ~/.git-completion.bash

I even found a nifty script online that does this automatically for OS X.

Upon starting a new shell, I have tab completion for git! Nice.

Where am I, what am I doing?

I work on contract. I’ve got a lot of projects ongoing over time. Keeping track of all the various repositories, branches, tags and so on can get a bit confusing. What’s a poor developer to do?

Enter git-prompt. Again, I place this code in my home directory, and then source it from my .profile:

source ~/.git-prompt.sh

Once I do this, I can use the __git_ps1 shell command and a number of shell variables, to configure PS1 and change my shell prompt. I can show the name of the current branch of a repository when I’m working in one. I can get information about the status of HEAD, modified files, stashes, untracked files and more.

There’s two ways to do this. The first is to use __git_ps1 as a command directly in a PS1 expression:

PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ '

The result looks like this:

Simple Bash Prompt

That’s not bad, but a bit of color would be nice, and perhaps breaking things onto more than one line so I can parse what I’m seeing more easily would be helpful.

For that, I need to change strategies. The __git_ps1 command can be used as a single element in the expression for PS!. But it can also be used itself as the PROMPT_COMMAND env variable. This variable names a command that will be used to form PS1 dynamically.

When I use __git_ps1 in this way, a couple of things happen. First, instead of taking only one optional argument (a format string), I can provide two or three arguments. The first will be prepended to the output of the command. The second will be appended after. Both of these arguments are required, but can be empty strings. The optional third argument will be used as a format string for the output of the command itself. If there is no output, it will not be used at all.

When combined these three elements can be very expressive. For example, My normal OS X command prompt can be expressed like so: \h:\W \u\\\$. If I use this as the second argument, leave the first empty and provide a simple format ending in a newline for the __git_ps1 output, my .profile looks like this:

PROMPT_COMMAND='__git_ps1 "" "\h:\W \u\\\$ " "[%s]\n"'

That produces a nice two-line prompt that appears when I’m in a git repo, and disappears when I’m not:

Two-line Prompt

Setting a few env variables in .profile expand this further, colorizing the output and providing information about the state of my repo:

GIT_PS1_SHOWDIRTYSTATE=1
GIT_PS1_SHOWCOLORHINTS=1
GIT_PS1_SHOWSTASHSTATE=1
GIT_PS1_SHOWUPSTREAM="auto"
PROMPT_COMMAND='__git_ps1 "" "\h:\W \u\\\$ " "[%s]\n'

Colorized Prompt

Not half bad at all.

But wait, there’s more.

The problem with this is that it doesn’t play well with another of my favorite Python tools, virtualenv. When I activate a virtualenv, it prepends the name of the environment I am working on to my command prompt. But it uses the standard PS1 shell variable to do this. Since I’ve used the PROMPT_COMMAND to set my prompt, PS1 doesn’t get used, and this nice feature of virtualenv is lost.

Luckily, there is a way out. Bash shell scripting offers parameter expansion and a trick of the syntax for that can help me. Normally, a shell parameter is referenced like so:

$ PARAM='foobar'
$ echo $PARAM
foobar

In complicated situations, I can wrap the name of the paramter in curly braces to avoid confusion with following characters:

$ echo ${PARAM}andthennotparam
foobarandthennotparam

What is not as well known is that this curly-brace syntax has a lot of interesting variations. For example, I can use PARAM as a test and actually print something else entirely:

$ echo ${PARAM:+'foo'}
foo
$ echo ${PARAM:+'bar'}

$

The key here is the :<char> bit immediately after PARAM. If the + char is present, then if PARAM is unset or null, what comes after is not printed, otherwise it is.

When I was looking at the script that activates a virtualenv in bash I noticed that it exports VIRTUAL_ENV. This means that so long as a virtualenv is active, this paramter will be set. And it will be unset when no environment is active.

I can use that!

Armed with this knowledge, I can construct a shell expression that will either print the name of the active virtualenv in square brackets, or print nothing if no virtualenv was active:

$ echo ${VIRTUAL_ENV:+[`basename $VIRTUAL_ENV`]}

$ source /path/to/someenv/bin/activate
$ echo ${VIRTUAL_ENV:+[`basename $VIRTUAL_ENV`]}
someenv

If I roll that into my .profile file, I can get everything I want (with a little more color thrown in for good measure):

… code-block:: bash

source ~/.git-prompt.sh
# PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ '
GIT_PS1_SHOWDIRTYSTATE=1
GIT_PS1_SHOWCOLORHINTS=1
GIT_PS1_SHOWSTASHSTATE=1
GIT_PS1_SHOWUPSTREAM="auto"
Color_Off="\[\033[0m\]"
Yellow="\[\033[0;33m\]"
PROMPT_COMMAND='__git_ps1 "${VIRTUAL_ENV:+[$Yellow`basename $VIRTUAL_ENV`$Color_Off]\n}" "\h:\W \u\\\$ " "[%s]\n"'

And voilà! I’ve got a shell prompt that keeps me informed about all the things I need to know when working on a daily basis:

add the virtualenv prompt

Wrap-up

Given the amount of time I spend at the command line, it only makes sense to work a bit at customizing it. The command prompt is something I see all day every day. Why not use it to my advantage? Combined with other minor modifications like git-completion, my shell becomes a power tool. There’s still a lot to learn, but I’m pretty happy with this so far.