Testing exit values in Bash

In Bash scripting (and shell scripting in general), we often want to check the exit value of a command to decide an action to take after it completes, likely for the purpose of error handling. For example, to determine whether a particular regular expression regex was present somewhere in a file options, we might apply grep(1) with its POSIX -q option to suppress output and just use the exit value:

grep -q regex options

An approach sometimes taken is then to test the exit value with the $? parameter, using if to check if it’s non-zero, which is not very elegant and a bit hard to read:

# Bad practice
grep -q regex options
if (($? > 0)); then
    printf '%s\n' 'myscript: Pattern not found!' >&2
    exit 1
fi

Because the if construct by design tests the exit value of commands, it’s better to test the command directly, making the expansion of $? unnecessary:

# Better
if grep -q regex options; then
    # Do nothing
    :
else
    printf '%s\n' 'myscript: Pattern not found!\n' >&2
    exit 1
fi

We can precede the command to be tested with ! to negate the test as well, to prevent us having to use else as well:

# Best
if ! grep -q regex options; then
    printf '%s\n' 'myscript: Pattern not found!' >&2
    exit 1
fi

An alternative syntax is to use && and || to perform if and else tests with grouped commands between braces, but these tend to be harder to read:

# Alternative
grep -q regex options || {
    printf '%s\n' 'myscript: Pattern not found!' >&2
    exit 1
}

With this syntax, the two commands in the block are only executed if the grep(1) call exits with a non-zero status. We can apply && instead to execute commands if it does exit with zero.

That syntax can be convenient for quickly short-circuiting failures in scripts, for example due to nonexistent commands, particularly if the command being tested already outputs its own error message. This therefore cuts the script off if the given command fails, likely due to ffmpeg(1) being unavailable on the system:

hash ffmpeg || exit 1

Note that the braces for a grouped command are not needed here, as there’s only one command to be run in case of failure, the exit call.

Calls to cd are another good use case here, as running a script in the wrong directory if a call to cd fails could have really nasty effects:

cd wherever || exit 1

In general, you’ll probably only want to test $? when you have specific non-zero error conditions to catch. For example, if we were using the --max-delete option for rsync(1), we could check a call’s return value to see whether rsync(1) hit the threshold for deleted file count and write a message to a logfile appropriately:

rsync --archive --delete --max-delete=5 source destination
if (($? == 25)); then
    printf '%s\n' 'Deletion limit was reached' >"$logfile"
fi

It may be tempting to use the errexit feature in the hopes of stopping a script as soon as it encounters any error, but there are some problems with its usage that make it a bit error-prone. It’s generally more straightforward to simply write your own error handling using the methods above.

For a really thorough breakdown of dealing with conditionals in Bash, take a look at the relevant chapter of the Bash Guide.

Bash history expansion

Setting the Bash option histexpand allows some convenient typing shortcuts using Bash history expansion. The option can be set with either of these:

$ set -H
$ set -o histexpand

It’s likely that this option is already set for all interactive shells, as it’s on by default. The manual, man bash, describes these features as follows:

-H  Enable ! style history substitution. This option is on
    by default when the shell is interactive.

You may have come across this before, perhaps to your annoyance, in the following error message that comes up whenever ! is used in a double-quoted string, or without being escaped with a backslash:

$ echo "Hi, this is Tom!"
bash: !": event not found

If you don’t want the feature and thereby make ! into a normal character, it can be disabled with either of these:

$ set +H
$ set +o histexpand

History expansion is actually a very old feature of shells, having been available in csh before Bash usage became common.

This article is a good followup to Better Bash history, which among other things explains how to include dates and times in history output, as these examples do.

Basic history expansion

Perhaps the best known and most useful of these expansions is using !! to refer to the previous command. This allows repeating commands quickly, perhaps to monitor the progress of a long process, such as disk space being freed while deleting a large file:

$ rm big_file &
[1] 23608
$ du -sh .
3.9G    .
$ !!
du -sh .
3.3G    .

It can also be useful to specify the full filesystem path to programs that aren’t in your $PATH:

$ hdparm
-bash: hdparm: command not found
$ /sbin/!!
/sbin/hdparm

In each case, note that the command itself is printed as expanded, and then run to print the output on the following line.

History by absolute index

However, !! is actually a specific example of a more general form of history expansion. For example, you can supply the history item number of a specific command to repeat it, after looking it up with history:

$ history | grep expand
 3951  2012-08-16 15:58:53  set -o histexpand
$ !3951
set -o histexpand

You needn’t enter the !3951 on a line by itself; it can be included as any part of the command, for example to add a prefix like sudo:

$ sudo !3850

If you include the escape string \! as part of your Bash prompt, you can include the current command number in the prompt before the command, making repeating commands by index a lot easier as long as they’re still visible on the screen.

History by relative index

It’s also possible to refer to commands relative to the current command. To subtitute the second-to-last command, we can type !-2. For example, to check whether truncating a file with sed worked correctly:

$ wc -l bigfile.txt
267 bigfile.txt
$ printf '%s\n' '11,$d' w | ed -s bigfile.txt
$ !-2
wc -l bigfile.txt
10 bigfile.txt

This works further back into history, with !-3, !-4, and so on.

Expanding for historical arguments

In each of the above cases, we’re substituting for the whole command line. There are also ways to get specific tokens, or words, from the command if we want that. To get the first argument of a particular command in the history, use the !^ token:

$ touch a.txt b.txt c.txt
$ ls !^
ls a.txt
a.txt

To get the last argument, add !$:

$ touch a.txt b.txt c.txt
$ ls !$
ls c.txt
c.txt

To get all arguments (but not the command itself), use !*:

$ touch a.txt b.txt c.txt
$ ls !*
ls a.txt b.txt c.txt
a.txt  b.txt  c.txt

This last one is particularly handy when performing several operations on a group of files; we could run du and wc over them to get their size and character count, and then perhaps decide to delete them based on the output:

$ du a.txt b.txt c.txt
4164    a.txt
5184    b.txt
8356    c.txt
$ wc !*
wc a.txt b.txt c.txt
16689    94038  4250112 a.txt
20749   117100  5294592 b.txt
33190   188557  8539136 c.txt
70628   399695 18083840 total
$ rm !*
rm a.txt b.txt c.txt

These work not just for the preceding command in history, but also absolute and relative command numbers:

$ history 3
 3989  2012-08-16 16:30:59  wc -l b.txt
 3990  2012-08-16 16:31:05  du -sh c.txt
 3991  2012-08-16 16:31:12  history 3
$ echo !3989^
echo -l
-l
$ echo !3990$
echo c.txt
c.txt
$ echo !-1*
echo c.txt
c.txt

More generally, you can use the syntax !n:w to refer to any specific argument in a history item by number. In this case, the first word, usually a command or builtin, is word 0:

$ history | grep bash
 4073  2012-08-16 20:24:53  man bash
$ !4073:0
man
What manual page do you want?
$ !4073:1
bash

You can even select ranges of words by separating their indices with a hyphen:

$ history | grep apt-get
 3663  2012-08-15 17:01:30  sudo apt-get install gnome
$ !3663:0-1 purge !3663:3
sudo apt-get purge gnome

You can include ^ and $ as start and endpoints for these ranges, too. 3* is a shorthand for 3-$, meaning “all arguments from the third to the last.”

Expanding history by string

You can also refer to a previous command in the history that starts with a specific string with the syntax !string:

$ !echo
echo c.txt
c.txt
$ !history
history 3
 4011  2012-08-16 16:38:28  rm a.txt b.txt c.txt
 4012  2012-08-16 16:42:48  echo c.txt
 4013  2012-08-16 16:42:51  history 3

If you want to match any part of the command line, not just the start, you can use !?string?:

$ !?bash?
man bash

Be careful when using these, if you use them at all. By default it will run the most recent command matching the string immediately, with no prompting, so it might be a problem if it doesn’t match the command you expect.

Checking history expansions before running

If you’re paranoid about this, Bash allows you to audit the command as expanded before you enter it, with the histverify option:

$ shopt -s histverify
$ !rm
$ rm a.txt b.txt c.txt

This option works for any history expansion, and may be a good choice for more cautious administrators. It’s a good thing to add to one’s .bashrc if so.

If you don’t need this set all the time, but you do have reservations at some point about running a history command, you can arrange to print the command without running it by adding a :p suffix:

$ !rm:p
rm important-file

In this instance, the command was expanded, but thankfully not actually run.

Substituting strings in history expansions

To get really in-depth, you can also perform substitutions on arbitrary commands from the history with !!:gs/pattern/replacement/. This is getting pretty baroque even for Bash, but it’s possible you may find it useful at some point:

$ !!:gs/txt/mp3/
rm a.mp3 b.mp3 c.mp3

If you only want to replace the first occurrence, you can omit the g:

$ !!:s/txt/mp3/
rm a.mp3 b.txt c.txt

Stripping leading directories or trailing files

If you want to chop a filename off a long argument to work with the directory, you can do this by adding an :h suffix, kind of like a dirname call in Perl:

$ du -sh /home/tom/work/doc.txt
$ cd !$:h
cd /home/tom/work

To do the opposite, like a basename call in Perl, use :t:

$ ls /home/tom/work/doc.txt
$ document=!$:t
document=doc.txt

Stripping extensions or base names

A bit more esoteric, but still possibly useful; to strip a file’s extension, use :r:

$ vi /home/tom/work/doc.txt
$ stripext=!$:r
stripext=/home/tom/work/doc

To do the opposite, to get only the extension, use :e:

$ vi /home/tom/work/doc.txt
$ extonly=!$:e
extonly=.txt

Quoting history

If you’re performing substitution not to execute a command or fragment but to use it as a string, it’s likely you’ll want to quote it. For example, if you’ve just found through experiment and trial and error an ideal ffmpeg command line to accomplish some task, you might want to save it for later use by writing it to a script:

$ ffmpeg -f alsa -ac 2 -i hw:0,0 -f x11grab -r 30 -s 1600x900 \
> -i :0.0+1600,0 -acodec pcm_s16le -vcodec libx264 -preset ultrafast \
> -crf 0 -threads 0 "$(date +%Y%m%d%H%M%S)".mkv 

To make sure all the escaping is done correctly, you can write the command into the file with the :q modifier:

$ echo '#!/usr/bin/env bash' >ffmpeg.sh
$ echo !ffmpeg:q >>ffmpeg.sh

In this case, this will prevent Bash from executing the command expansion "$(date ... )", instead writing it literally to the file as desired. If you build a lot of complex commands interactively that you later write to scripts once completed, this feature is really helpful and saves a lot of cutting and pasting.

Thanks to commenter Mihai Maruseac for pointing out a bug in the examples.

Avoiding the arrow keys

For shell users, moving to the arrow keys on the keyboard is something of an antipattern, moving away from the home row so central to touch typists. It therefore helps to find ways to avoid using the arrow keys in order to maintain your flow.

Bash

The arrow keys in Bash are used to move back and forth on the current command line (left and right), and up and down through the command history (up and down). This leads to the old shell user’s maxim:

“Those that don’t remember history are doomed to press Up repeatedly.”

There are alternatives to both functions. To move left or right by one character on the command line without deleting characters already placed, we can use Ctrl-B and Ctrl-F.

However, to make things a bit faster, there are four other key combinations to move back and forth on a line that are worth learning too:

  • Alt-B — Move back a word
  • Alt-F — Move forward a word
  • Ctrl-A — Move to the start of the line
  • Ctrl-E — Move to the end of the line

Similarly, to move up and down through history, we can use Ctrl-P and Ctrl-N respectively. Don’t forget that rather than keep tapping one of these, you can search backward or forward through history with Ctrl-R and Ctrl-S.

Whoops, I think I just taught you some Emacs.

Vim

To avoid the arrow keys in normal mode in Vim, use h, j, k, and l instead. This can take a little getting used to, but the benefits in terms of comfort for your hands and speed is easily worth it; using the arrow keys is one of the Vim anti-patterns.

If you’re asking “How do I avoid the arrow keys to move in insert mode?”, the answer is that you shouldn’t be moving in insert mode at all; it’s inefficient. When in insert mode you should be inserting text, and maybe using backspace or delete every now and then. To move through text, switching back to normal mode is vastly more efficient.

Moving in command mode is similar. If you need to move around on the command line, in most cases you should pull up the command window so that you can edit the command in normal mode, and then just tap Enter to run it.

In general, when you start thinking about moving through any kind of text, you should reflexively hit Esc or Ctrl-[ to go into normal mode, in order to take advantage of a whole keyboard’s worth of navigation shortcuts.

Bash prompts

You can tell a lot about a shell user by looking at their prompt. Most shell users will use whatever the system’s default prompt is for their entire career. Under many Linux distributions, this prompt includes the username, the hostname, and the current working directory, along with a $ sigil for regular users, and a # for root.

tom@sanctum:~$

This format works well for most users, and it’s so common that it’s often used in tutorials to represent the user’s prompt in lieu of the single-character standard $ and #. It shows up as the basis for many of the custom prompt setups you’ll find online.

Some users may have made the modest steps of perhaps adding some brackets around the prompt, or adding the time or date:

[10:11][tom@sanctum:~]$

Still others make the prompt into a grand multi-line report of their shell’s state, or indeed their entire system, full of sound and fury and signifying way too much:

-(23:38:57)-(4 users)-(0.00, 0.02, 0.05)-
-(Linux sanctum 3.2.0-3-amd64 x86_64 GNU/Linux)-
[tom@sanctum:/dev/pts/2:/home/tom]{2}!?[MAIL]$

Then there are the BSD users and other minimalist contrarians who can’t abide anything more than a single character cluttering their screens:

$

Getting your prompt to work right isn’t simply a cosmetic thing, however. If done right and in a style consistent with how you work, it can provide valuable feedback on how the system is reacting to what you’re doing with it, and also provide you with visual cues that could save you a lot of confusion — or even prevent your making costly mistakes.

I’ll be working in Bash. Many of these principles work well for Zsh as well, but Zsh users might want to read Steve Losh’s article on his prompt setup first to get an idea of the syntactical differences.

Why is this important?

One of the primary differences between terminals and windowed applications is that there tends to be much less emphasis on showing metadata about the running program and its environment. For the most part, to get information about what’s going on, you need to request it with calls like pwd, whoami, readlink, env, jobs, and echo $?.

The habit of keeping metadata out of the terminal like this unless requested may appeal to a sense of minimalism, but like a lot of things in the command line world, it’s partly the olden days holding us back. When mainframes and other dinosaurs roamed the earth, terminal screens were small, and a long or complex prompt amounted to a waste of space and cycles (or even paper). In our futuristic 64-bit world, where tiny CRTs are a thing of the past and we think nothing of throwing gigabytes of RAM at a single process, this is much less of a problem.

Customising the prompt doesn’t have many “best practices” to it like a lot of things in the shell; there isn’t really a right way to do it. Some users will insist it’s valuable to have the path to the terminal device before every prompt, which I think is crazy as I almost never need that information, and when I do, I can get it with a call to tty. Others will suggest that including the working directory makes the length of the prompt too variable and distracting, whereas I would struggle to work without it. It’s worth the effort to design a prompt that reflects your working style, and includes the information that’s consistently important to you on the system.

So, don’t feel like you’re wasting time setting up your prompt to get it just right, and don’t be so readily impressed by people with gargantuan prompts in illegible rainbow colors that they copied off some no-name wiki. If you use Bash a lot, you’ll be staring at your prompt for several hours a day. You may as well spend an hour or two to make it useful, or at least easier on the eyes.

The basics

The primary Bash prompt is stored in the variable PS1. Its main function is to signal to the user that the shell is done with its latest foreground task, and is ready to accept input. PS1 takes the form of a string that can contain escape sequences for certain common prompt elements like usernames and hostnames, and which is evaluated at printing time for escape sequences, variables, and command or function calls within it.

You can inspect your current prompt definition with echo. If you’re using Linux, it will probably look similar to this:

tom@sanctum:~$ echo $PS1
\u@\h:\w\$

PS1 works like any other Bash variable; you can assign it a new value, append to it, and apply substitutions to it. To add the text bash to the start of my prompt, I could do this:

tom@sanctum:~$ PS1=bash-"$PS1"
bash-tom@sanctum:~$

There are other prompts — PS2 is used for “continued line” prompts, such as when you start writing a for loop and continue another part of it on a new line, or terminate your previous line with a backslash to denote the next line continues on from it. By default this prompt is often a right angle bracket >:

$ for f in a b c; do
>   do echo "$f"
> done

There are a couple more prompt strings which are much more rarely seen. PS3 is used by the select structure in shell scripts; PS4 is used while tracing output with set -x. Ramesh Natarajan breaks this down very well in his article on the Bash prompt. Personally, I only ever modify PS1, because I see the other prompts so rarely that the defaults work fine for me.

As PS1 is a property of interactive shells, it makes the most sense to put its definitions in $HOME/.bashrc on Linux or BSD systems. It’s likely there’s already code in there doing just that. For Mac OS X, you’ll likely need to put it into $HOME/.bash_profile instead.

Escapes

Bash will perform some substitutions for you in the first pass of its evaluation of your prompt string, replacing escape sequences with appropriate elements of information that are often useful in prompts. Below are the escape sequences as taken from the Bash man page:

\a     an ASCII bell character (07)
\d     the date in "Weekday Month Date" format (e.g., "Tue May 26")
\D{format}
       the format is passed to strftime(3) and the result is inserted into
       the prompt string; an empty format results in a locale-specific time
       representation. The braces are required
\e     an ASCII escape character (033)
\h     the hostname up to the first `.'
\H     the hostname
\j     the number of jobs currently managed by the shell
\l     the basename of the shell's terminal device name
\n     newline
\r     carriage return
\s     the name of the shell, the basename of $0 (the portion following
       the final slash)
\t     the current time in 24-hour HH:MM:SS format
\T     the current time in 12-hour HH:MM:SS format
\@     the current time in 12-hour am/pm format
\A     the current time in 24-hour HH:MM format
\u     the username of the current user
\v     the version of bash (e.g., 2.00)
\V     the release of bash, version + patch level (e.g., 2.00.0)
\w     the current working directory, with $HOME abbreviated with a tilde
       (uses the value of the PROMPT_DIRTRIM variable)
\W     the basename of the current working directory, with $HOME
       abbreviated with a tilde
\!     the history number of this command
\#     the command number of this command
\$     if the effective UID is 0, a #, otherwise a $
\nnn   the character corresponding to the octal number nnn
\\     a backslash
\[     begin a sequence of non-printing characters, which could be used to
       embed a terminal control sequence into the prompt
\]     end a sequence of non-printing characters

As an example, the default PS1 definition on many Linux systems uses four of these codes:

\u@\h:\w\$

The \! escape sequence, which expands to the history item number of the current command, is worth a look in particular. Including this in your prompt allows you to quickly refer to other commands you may have entered in your current session by history number with a ! prefix, without having to actually consult history:

$ PS1="(\!)\$"
(6705)$ echo "test"
test
(6706)$ !6705
echo "test"
test

Keep in mind that you can refer to history commands relatively rather than by absolute number. The previous command can be referenced by using !! or !-1, and the one before that by !-2, and so on, provided that you have left the histexpand option on.

Also of particular interest are the delimiters \[ and \]. You can use these to delimit sections of “non-printing characters”, typically things like terminal control characters to do things like moving the cursor around, starting to print in another color, or changing properties of the terminal emulator such as the title of the window. Placing these non-printing characters within these delimiters prevents the terminal from assuming an actual character has been printed, and hence correctly manages things like the user deleting characters on the command line.

Colors

There are two schools of thought on the use of color in prompts, very much a matter of opinion:

  • Focus should be on the output of commands, not on the prompt, and using color in the prompt makes it distracting, particularly when output from some programs may also be in color.
  • The prompt standing out from actual program output is a good thing, and judicious use of color conveys useful information rather than being purely decorative.

If you agree more with the first opinion than the second, you can probably just skip to the next section of this article.

Printing text in color in terminals is done with escape sequences, prefixed with the \e character as above. Within one escape sequence, the foreground, background, and any styles for the characters that follow can be set.

For example, to include only your username in red text as your prompt, you might use the following PS1 definition:

PS1='\[\e[31m\]\u\[\e[0m\]'

Broken down, the opening sequence works as follows:

  • \[ -- Open a series of non-printing characters, in this case, an escape sequence.
  • \e -- An ASCII escape character, used to confer special meaning to the characters that follow it, normally to control the terminal rather than to enter any characters.
  • [31m -- Defines a display attribute, corresponding to foreground red, for the text to follow; opened with a [ character and terminated with an m.
  • \] — Close the series of non-printing characters.

The terminating sequence is very similar, except it uses the 0 display attribute to reset the text that follows back to the terminal defaults.

The valid display attributes for 16-color terminals are:

  • Style
    • 0 — Reset all attributes
    • 1 — Bright
    • 2 — Dim
    • 4 — Underscore
    • 5 — Blink
    • 7 — Reverse
    • 8 — Hidden
  • Foreground
    • 30 — Black
    • 31 — Red
    • 32 — Green
    • 33 — Yellow
    • 34 — Blue
    • 35 — Magenta
    • 36 — Cyan
    • 37 — White
  • Background
    • 40 — Black
    • 41 — Red
    • 42 — Green
    • 43 — Yellow
    • 44 — Blue
    • 45 — Magenta
    • 46 — Cyan
    • 47 — White

Note that blink may not work on a lot of terminals, often by design, as it tends to be unduly distracting. It’ll work on a linux console, though. Also keep in mind that for a lot of terminal emulators, by default “bright” colors are printed in bold.

More than one of these display attributes can be applied in one hit by separating them with semicolons. The below will give you underlined white text on a blue background, after first resetting any existing attributes:

\[\e[0;4;37;44m\]

It’s helpful to think of these awkward syntaxes in terms of opening and closing, rather like HTML tags, so that you open before and then reset after all the attributes of a section of styled text. This usually amounts to printing \[\e[0m\] after each such passage.

These sequences are annoying to read and to type, so it helps to put commonly used styles into variables:

color_red='\[\e[31m\]'
color_blue='\[\e[34m\]'
color_reset='\[\e[0m\]'

PS1=${color_red}'\u'${color_reset}'@'${color_blue}'\h'${color_reset}

A more robust method is to use the tput program to print the codes for you, and put the results into variables:

color_red=$(tput setaf 1)
color_blue=$(tput setaf 4)

This may be preferable if you use some esoteric terminal types, as it queries the terminfo or termcap databases to generate the appropriate escapes for your terminal.

The set of eight colors — or sixteen, depending on how you look at it — can be extended to 256 colors in most modern terminal emulators with a little extra setup. The escape sequences are not very different, but their range is vastly increased to accommodate the larger palette.

A useful application for prompt color can be a quick visual indication of the privileges available to the current user. The following code changes the prompt to green text for a normal user, red for root, and yellow for any other user attained via sudo:

color_root=$(tput setaf 1)
color_user=$(tput setaf 2)
color_sudo=$(tput setaf 3)
color_reset=$(tput sgr0)

if (( EUID == 0 )); then
    PS1="\\[$color_root\\]$PS1\\[$color_reset\\]"
elif [[ $SUDO_USER ]]; then
    PS1="\\[$color_sudo\\]$PS1\\[$color_reset\\]"
else
    PS1="\\[$color_user\\]$PS1\\[$color_reset\\]"
fi

Otherwise, you could simply use color to make different regions of your prompt more or less visually prominent.

Variables

Provided the promptvars option is enabled, you can reference Bash variables like $? to get the return value of the previous command in your prompt string. Some people find this valuable to show error codes in the prompt when the previous command given fails:

$ shopt -s promptvars
$ PS1='$? \$'
1 $ echo "test"
test
0 $ fail
-bash: fail: command not found
127 $

The usual environment variables like USER and HOME will work too, though it’s preferable to use the escape sequences above for those particular variables. Note that it’s important to quote the variable references as well, so that they’re evaluated at runtime, and not during the assignment itself.

Commands

If the information you want in your prompt isn’t covered by any of the escape codes, you can include the output of commands in your prompt as well, using command substitution with the syntax $(command):

$ PS1='[$(uptime) ]\$ '
[ 01:21:59 up 7 days, 13:04,  7 users, load average: 0.30, 0.40, 0.36 ]$

This requires the promptvars option to be set, the same way variables in the prompt do. Again, note that you need to use single quotes so that the command is run when the prompt is being formed, and not immediately as part of the assignment.

This can include pipes to format the data with tools like awk or cut, if you only need a certain part of the output:

$ PS1='[$(uptime | cut -d: -f5) ]\$ '
[ 0.36, 0.34, 0.34 ]$

For clarity, during prompt setup in .bashrc, it makes sense to use functions instead for reasonably complex commands:

prompt_load() {
    uptime | cut -d: -f5
}
PS1='[$(prompt_load) ]'$PS1

Note that as a normal part of command substitution, trailing newlines are stripped from the output of the command, so here the output of uptime appears on a single line.

A common usage of this pattern is showing metadata about the current directory, particularly if it happens to be a version control repository or working copy; you can use this syntax with functions to show the type of version control system, the current branch, and whether any changes require committing. Working with Git, Mercurial, and Subversion most often, I include the relevant logic as part of my prompt function.

When appended to my PS1 string, $(prompt vcs) gives me prompts that look like the following when I’m in directories running under the appropriate VCS. The exclamation marks denote that there are uncommitted changes in the repositories.

[tom@conan:~/.dotfiles](git:master!)$
[tom@conan:~/Build/tmux](svn:trunk)$
[tom@conan:~/Build/vim](hg:default!)$

In general, where this really shines is adding pieces to your prompt conditionally, to make them collapsible. Certain sections of your prompt therefore only show up if they’re relevant. This snippet, for example, prints the number of jobs running in the background of the current interactive shell in curly brackets, but only if the count is non-zero:

prompt_jobs() {
    local jobc
    while read -r _; do
        ((jobc++))
    done < <(jobs -p)
    if ((jobc > 0)); then
        printf '{%d}' "$jobc"
    fi
}

It’s important to make sure that none of what your prompt does takes too long to run; an unresponsive prompt can make your terminal sessions feel very clunky.

Note that you can also arrange to run a set of commands before the prompt is evaluated and printed, using the PROMPT_COMMAND. This tends to be a good place to put commands that don’t actually print anything, but that do need to be run before or after each command, such as operations working with history:

PROMPT_COMMAND='history -a'

Switching

If you have a very elaborate or perhaps even computationally expensive prompt, it may occasionally be necessary to turn it off to revert to a simpler one. I like to handle this by using functions, one of which sets up my usual prompt and is run by default, and another which changes the prompt to the minimal $ or # character so often used in terminal demonstrations. Something like the below works well:

prompt_on() {
    PS1="$color_prompt"'\u@\h:\w'"$(prompt_jobs)"'\$'"${color_reset}"' '
}
prompt_off() {
    PS1='\$'
}
prompt_on

You can then switch your prompt whenever you need to by typing prompt_on and prompt_off.

This can also be very useful if you want to copy text from your terminal into documentation, or into an IM or email message; it removes your distracting prompt from the text, where it would otherwise almost certainly differ from that of the user following your instructions. This is also occasionally helpful if your prompt does not work on a particular machine, or the machine is suffering a very high load average that means your prompt is too slow to load.

Further reading

Predefined prompt strings are all over the web, but the above will hopefully enable you to dissect what they’re actually doing more easily and design your own. To take a look at some examples, the relevant page on the Arch Wiki is a great start. Ramesh Natarajan over at The Geek Stuff has a great article with some examples as well, with the curious theme of making your prompt as well-equipped as Angelina Jolie.

Finally, please feel free to share your prompt setup in the comments (whether you’re a Bash user or not). It would be particularly welcome if you explain why certain sections of your prompt are so useful to you for your particular work.

Default grep options

When you’re searching a set of version-controlled files for a string with grep, particularly if it’s a recursive search, it can get very annoying to be presented with swathes of results from the internals of the hidden version control directories like .svn or .git, or include metadata you’re unlikely to have wanted in files like .gitmodules.

GNU grep uses an environment variable named GREP_OPTIONS to define a set of options that are always applied to every call to grep. This comes in handy when exported in your .bashrc file to set a “standard” grep environment for your interactive shell. Here’s an example of a definition of GREP_OPTIONS that excludes a lot of patterns which you’d very rarely if ever want to search with grep:

GREP_OPTIONS=
for pattern in .cvs .git .hg .svn; do
    GREP_OPTIONS="$GREP_OPTIONS --exclude-dir=$pattern
done
export GREP_OPTIONS

Note that --exclude-dir is a relatively recent addition to the options for GNU grep, but it should only be missing on very legacy Linux machines by now. If you want to keep your .bashrc file compatible, you could apply a little extra hackery to make sure the option is available before you set it up to be used:

GREP_OPTIONS=
if grep --help | grep -- --exclude-dir &>/dev/null; then
    for pattern in .cvs .git .hg .svn; do
        GREP_OPTIONS="$GREP_OPTIONS --exclude-dir=$pattern"
    done
fi
export GREP_OPTIONS

Similarly, you can ignore single files with --exclude. There’s also --exclude-from=FILE if your list of excluded patterns starts getting too long.

Other useful options available in GNU grep that you might wish to add to this environment variable include:

  • --color — On appropriate terminal types, highlight the pattern matches in output, among other color changes that make results more readable
  • -s — Suppresses error messages about files not existing or being unreadable; helps if you find this behaviour more annoying than useful.
  • -E, -F, or -P — Pick a favourite “mode” for grep; devotees of PCRE may find adding -P for grep‘s experimental PCRE support makes grep behave in a much more pleasing way, even though it’s described in the manual as being experimental and incomplete

If you don’t want to use GREP_OPTIONS, you could instead simply set up an alias:

alias grep='grep --exclude-dir=.git'

You may actually prefer this method as it’s essentially functionally equivalent, but if you do it this way, when you want to call grep without your standard set of options, you only have to prepend a backslash to its call:

$ \grep pattern file

Commenter Andy Pearce also points out that using this method can avoid some build problems where GREP_OPTIONS would interfere.

Of course, you could solve a lot of these problems simply by using ack … but that’s another post.

Typo correction in Bash

If you make a typo in a long command line in Bash, instead of typing it all out again, you can either recall it from your history, or use caret notation to repeat the command with an appropriate substitution. This looks like the following:

# sudo apache2ctl restrat
Action 'restrat' failed.
The Apache error log may have more information.
# ^strat^start
sudo apache2ctl restart

The string after the first caret is the text to match, and the one after the second string is the text with which it should be replaced. This provides a convenient method of not only quickly correcting typos, but to change small parts of the command line in general quickly:

$ touch filea.txt
$ ^filea^fileb
touch fileb.txt
$ ^fileb^filec
touch filec.txt

For the particular case of correcting small errors in long paths for cd calls, it’s helpful to use the cdspell option for Bash, which I discuss in my earlier article on smarter directory navigation.

Bash command existence

If you set environment variables like your EDITOR in your .bashrc file that refer to commands that you expect to be available on the system, it’s prudent to check that an appropriate command actually exists before making the setting.

A common way of approaching this is using which, in a syntax similar to the below, which sets EDITOR to the first executable instance of vi found in PATH, or blank if not found:

EDITOR=$(which vi)

Because the behaviour of which can be unexpected, it’s better practice to use one of Bash’s builtins to do this, either command, type, or hash. I prefer using hash, which searches PATH for a command of the given name, and loads it into Bash’s command hash table if found. An implementation like the below works well:

if hash vi 2>/dev/null; then
    export EDITOR=vi
fi

This ignores any error output from the hash call by redirecting it to the null device, and only defines the value for EDITOR if a matching command is found.

You can compact this syntax into one line:

hash vi 2>/dev/null && export EDITOR=vi

Thanks to commenter Yu-Jie Lin for pointing out the above abbreviation.

Calculating with Bash

If you’re at a terminal or writing a script and need to do some quick integer calculations, Bash offers arithmetic expansion with the $((...)) syntax.

The usual arithmetic operations of addition, subtraction, multiplication, and division are supported, but the name of arithmetic expansion is a bit misleading as it also supports operations well beyond the elementary arithmetic set, including exponentiation, equality and inequality checks, and logical and bitwise NOT/AND/OR/XOR:

  • VAR++ VAR-- — post-increment, post-decrement
  • ++VAR --VAR — pre-increment, pre-decrement
  • + - — addition, subtraction
  • ** — exponentiation
  • * / % — multiplication, division, remainder
  • == != — equality, inequality
  • < > <= >= — comparison
  • ! && || — logical NOT, AND, OR
  • ~ & | ^ — bitwise NOT, AND, OR, XOR

These operators and their precedence are all the same as used in most C-style languages. Pivotally, the expansion can include variables defined elsewhere in the command or script:

$ number=1234
$ printf '%s\n' "$((number * 2))"
2468

One place this can be very useful is in calculating filesizes, since most of the time shell commands work in either bytes or potentially arbitrary disk block sizes. If you work with SI decimal prefixes, you can use powers of 10 to calculate the sizes:

kilobytes=$((bytes / 10**3))
megabytes=$((bytes / 10**6))
gigabytes=$((bytes / 10**9))

If you prefer to work with IEC binary prefixes, i.e. multiples of 1024:

kilobytes=$((bytes / 2**10))
megabytes=$((bytes / 2**20))
gigabytes=$((bytes / 2**30))

Or you could use bit shifting:

kilobytes=$((bytes >> 10))
megabytes=$((bytes >> 20))
gigabytes=$((bytes >> 30))

Lazier Tab completion

Bash completion allows you to complete paths, commands, and filenames, and where implemented can even expand the syntax of commands like git and svn. It’s very convenient by default, but there are a couple of tweaks that can make it much faster and easier to use.

Single Tab press

With Bash completion enabled, the default is to complete the current path to the longest unique string it can on the first press of the Tab key, and then show a list of all the possible completions if the Tab key is then pressed a second time.

If you don’t like pressing Tab twice, you can fix this with a line in .inputrc so that the line is both completed to the longest unambiguous pattern, and then possible values are also printed:

set show-all-if-ambiguous on

Log out and then in again, or re-read your .inputrc file with Ctrl+X then Ctrl+R, and you should find that you now only need to press Tab once on completions to get both behaviours in one hit.

If you want Tab completion of filenames to work the same way in Vim’s command mode, you can include the below in your .vimrc. Note that the separator is a colon, rather than a comma, which means to perform both the command completion and suggestion at the first Tab press:

set wildmode=longest:list

Complete case-insensitively

If you commonly Tab your way through long paths that sometimes include mixed-case names, you can make Readline handle that for you by making the completion case-insensitive, with the following in your .inputrc:

set completion-ignore-case on

You can supplement this to make the completion apply similar insensitivity between hyphens and underscores:

set completion-map-case on

If you were tabbing your way through an ImageMagick directory, for example, and forgot the capital letters in typing imagem, Readline would quietly complete it to ImageMagick for you. Similarly, if you typed lib-undersc when the actual filename was lib_underscore, the shell would quietly fix that for you too.

You can get case-insensitivity for the filename completion in Vim with the following. I suggest wrapping it in a conditional as below, since it’s a reasonably new option:

if exists("&wildignorecase")
    set wildignorecase
endif

Unfortunately, there doesn’t seem to be a way that I can find to reproduce the underscore/hyphen mapping. If you know of one, please comment.

Don’t prompt for many possible completions

If you’re on a fast terminal and you’re not bothered by having the screeds of texts it can sometimes generate being spat at you, you can turn off the sometimes annoying prompt that checks with you whether you want to show more than a hundred results for possible completion, with the following in .inputrc:

set completion-query-items -1

If you do this, you should probably set Readline’s built-in pager to be off, otherwise it’ll attempt to walk you through the possible completions page-by-page:

set page-completions off

If you do happen to deal with directories with more than a thousand files in them now and then (which doesn’t tend to happen much in routine system administration), it might be a bit safer to set it to some number much higher than the default of 100:

set completion-query-items 1000

These three tips combined make tabbing your way through path names much more pleasant and intuitive; you won’t find yourself irritably tapping Tab over and over nearly as much. Even if you’re very much used to playing ball with the stricter idiosyncrasies of the usual form of tab completion, you may find making the shell do the work for you in this way starts to feel very natural very quickly.

Vi mode in Bash

Because the Bash shell uses GNU Readline, you’re able to set options in .bashrc and .inputrc to extensively customise how you enter your command lines, including specifying whether you want the default Emacs-like keybindings (e.g. Ctrl+W to erase a word), or bindings using a modal interface familiar to vi users.

To use vi mode in Bash and any other tool that uses GNU Readline, such as the MySQL command line, you need only put this into your .inputrc file, then log out and in again:

set editing-mode vi

If you only want to use this mode in Bash, an alternative is to use the following in your .bashrc, again with a login/logout or resourcing of the file:

set -o vi

You can check this has applied correctly with the following, which will spit out a list of the currently available bindings for a set of fixed possible actions for text:

$ bind -P

The other possible value for editing-mode is emacs. Both options simply change settings in the output of bind -P above.

This done, you should find that you open a command line in insert mode, but when you press Escape or Ctrl+[, you will enter an emulation of Vi’s normal mode. Enter will run the command in its present state while in either mode. The bindings of most use in this mode are the classic keys for moving to positions on a line:

  • ^ — Move to start of line
  • $ — Move to end of line
  • b — Move back a word
  • w — Move forward a word
  • e — Move to the end of the next word

Deleting, yanking, and pasting all work too. Also note that k and j in normal mode allow you to iterate through your history in the same way that Ctrl+P and Ctrl+N do in Emacs mode. Searching with ? and / doesn’t work by default, but the Ctrl+R and Ctrl+S bindings still seem to work.

If you are reasonably used to working with the modeless Emacs for most of your shell work but do occasionally find yourself in a situation where being able to edit a long command line in vi would be handy, you may prefer to press Ctrl+X Ctrl+E to bring up the command line in your $EDITOR instead, affording you the complete power of Vim to edit rather than the somewhat sparse vi emulation provided by Readline. If you want to edit a command you just submitted, the fc (fix command) Bash builtin works too.

The shell is a very different environment for editing text, being inherently interactive and fixed to one line rather than a static multi-line buffer, which leads some otherwise diehard vi users such as myself to prefer the Emacs mode for editing commands. For one thing, vi mode in Bash trips on the vi anti-pattern of putting you in insert mode by default, and at the start of every command line. But if the basic vi commands are etched indelibly into your memory and have become automatic, you may appreciate being able to edit your command line using these keys instead. It’s certainly worth a try at any rate, and I do still occasionally see set -o vi in the .bashrc files of a few of the pros on GitHub.

Arabesque passed 100,000 visits today, having existed little over a month, with over 60,000 unique visitors. Thanks very much to everyone who reads and comments on the articles.