Bash Tips
Today I'd like to share a few tips and tricks for Bash. These are things I discovered from reading the manual. Clever one-liners are not my thing; the goal is to showcase useful features of the shell.
A word of caution: Bash isn't the most forgiving language. The expansion rules in particular are tricky. Also don't mistake Bash for a general purpose scripting language. It runs commands, and it does it well. That's all you should be using it for.
On Syntax
Shell syntax is terse and uses arbitrary symbols, as most DSLs tend to do.
Here's a list, or complex command. It runs programs a
and b
in parallel (feeding a
's output to b
), then checks b
's exit status, and finally runs program c
if there was no error.
$ (a | b) && c
Commands are baked into Bash just like arithmetic expressions are in most other programming languages. Imagine C without arithmetic expressions.
int average(int a, int b) {
return div(add(a, b), 2);
}
Arithmetic expressions have universal appeal because they match the language of mathematics, but don't fool yourself; the choice of + and - for addition and subtraction is just as arbitrary as the choice of | and & by the shell. You can view arithmetic expressions as a small DSL tacked on to C for convenience.
Unix pipes are not as widely useful as arithmetic. And so it is less convenient to launch processes from most languages, like with Python's subprocess library. Efforts to improve the experience tend to end up reinventing a subset of Bash's syntax inside the host language. That's approximately what envoy or iterpipes do, both for Python.
There are other, more ambitious efforts to bridge the gap, but I mention them as a curiosity. They both involve some level of meta-programming.
The point is that DSLs are niche by definition. Wait until it makes sense to pick up shell syntax, but don't wait your whole life. If you program for a living, especially on a Unix system, there's a good chance you will type enough shell commands to make it worthwhile.
Exit status
When a Unix program terminates, it leaves an exit status behind; 0 for
success, non-zero for failure. Bash stores the previous command's exit status
in the shell parameter $?
. You can add it to the prompt for easier
debugging.
$ PS1="\$?\n$PS1" # add latest exit status to prompt
Usually you're only interested if the status is non-zero. Here are some ways to include only non-zero statuses in the prompt.
Redirection
The pipe operator is one example of redirection. It is a key mechanism in the Unix philosophy of many small tools working together.
$ cat | tr -cs 'a-z' '\n' | sort | uniq | wc -l
count unique words in input
<C-d>
5
Here is how to merge file descriptor 2 (stderr) with file descriptor 1 (stdout):
$ some-command 2>&1
That turns out to be useful when you need to log program output to file, including any error messages. I also use it to disable all output from chatty programs like eclipse.
# avoid random error messages
$ eclipse > /dev/null 2>&1 &
Shell parameters
You can use shell parameters to hold anything, like a git hash that might be needed later.
$ git log --oneline --graph
* a6534a3 Merge branch 'master' of github.com:silmawien/dotfiles
|\
| * b79f913 Provide 32-bit tconn to avoid LD errors
| * 1d9068f Git log graph alias
| * 08b14c6 Transconnect setup for work
* | 9f2f7ee Exit status in prompt, vim tabs
|/
* ee18d8e One more
$ base=`git merge-base b79f913 9f2f7ee` # save for later
.
.
.
$ echo $base # what was that commit again?
ee18d8ef3ad9e1c37e3e53293c0e55b3a844ad24
As long as you don't export them, parameters are not visible to programs that
you run. If you use lower-case names, you won't confuse them with program
specific variables like CLASSPATH
.
If you want to temporarily override part of the environment, that's easy too.
$ HOME=/some/dir python
>>> os.environ['HOME']
'/some/dir'
The change to HOME
only affects the environment of Python. Once we return
to the shell, HOME
is back to it's old value.
>>> <CTRL-D>
$ echo $HOME
/home/mattias
Expansion
Here's a few examples of history expansion - referring to (part of) an earlier command.
$ shutdown -h now
shutdown: Need to be root
$ sudo !! # !! = previous command
sudo shutdown -h now
$ git diff -- some/rather/long/filename.txt
$ vim !$ # !$ = last parameter of previous command
vim some/rather/long/filename.txt
$ ls /some/long/and/incorrectly/spelled/path
ls: cannot access /some/long/and/incorrectly/spelled/path
$ ^incor^cor # replace in previous command
ls /some/long/and/correctly/spelled/path
The first one I use regularly, but only ever with sudo. Up-arrow or incremental history search covers most everything else.
Brace notation is nice for managing file extensions.
$ echo /path/to/image.{png,bak}
/path/to/image.png /path/to/image.bak
Parameter expansion is really flexible, and can do similar things.
${var%pattern}
expands to the value of $var
, after removing a suffix that
matches pattern
.
# 'cp file.jpg file.bak' for all jpg files
$ for img in *.jpg; do cp $img ${img%jpg}bak; done
Job Control
Job control is occasionally useful.
$ eclipse # oops, forgot to launch in the background with '&'
<C-z> # suspend current job
[1]+ Stopped eclipse
$ bg # resume in the background
Just a word of caution. If you want to truly understand job control, you need to learn a few things about signals and terminals as well, which might be more than you bargained for.
History
My favorite Bash feature is the interactive history search, by default on
C-r
. If you can remember any part of a previous
command, C-r
will find it.
# Hmm, what was the command to restart Apache?
$ <C-r>
(reverse-i-search)'':
(reverse-i-search)'ap': sudo service apache2 restart # found it!
Increase the value of HISTSIZE
and HISTFILESIZE
to something really large,
to avoid losing old commands. 20000 is not unreasonable.
Command Line Editing
Without the readline library it would be incredibly painful to type in commands at the prompt. It is the IDE of Bash, so to speak.
Moving forward M-f
and backward M-b
by words might a habit worth picking up.
I also like yank-last-arg
which copies the last parameter of the previous
command, just like !$
does. It is bound to M-.
by
default. You can provide a numerical argument to this command. For instance,
M-2
M-.
yanks the 2nd
parameter of the last command.
If readline fails you, C-x
C-e
brings up an editor for the current command line. It's the best way to convert a
one-off command to a script.
Other Resources
There's a really neat site catonmat.net, more or less entirely based on cool things you can do with Bash and the standard Unix utilities.
Zsh
If you are serious about the shell, you may want to consider switching to zsh. It's slightly more radical than Bash. But don't worry, it's mostly compatible.
Here's a teaser:
$ find . -name "*.txt" | xargs grep foo # bash
$ grep foo **/*.txt # zsh