11. Loops and branches¶
In this section, we’ll be starting towards some basic programming using the
shell. We won’t be writing many programs using bash
, but we’ll introduce some useful control structures, such as loops and branches structures.
Exit codes and chaining commands¶
First, let’s start executing some commands in sequence. Last time, you tried
looking at the sizes of the 5 largest files by piping the output from
one command into another. Pipes aren’t the only way that processes can be chained: &&
will execute the command on the right only if the command on the left is successful. Successful is defined by the exit value of the command, which you can find by echo-ing the $?
variable:
$ ls
[...]
$ echo $?
0
0
is the successful exit value of a Unix process: non-zero numbers can be used to encode what went wrong. Try running some commands that might fail, like grep-ing for non-existent words or trying to touch a file in an area you can’t write to, then look at the exit code. The meanings of difference exit codes are usually documented in a program’s manpage.
Now let’s try the &&
construct:
$ echo "foo" > foo && echo "Done."
$ echo "foo" > /foo && echo "Done."
Get the idea?
The corresponding syntax for a command chain which continues only if the
preceding command failed is ||
, e.g.:
$ echo "foo" > /foo || echo "D'oh, that's write-protected"
if … then … else¶
The &&
and ||
constructs are hard to use together to work with both possible outcomes — there is a special if
syntax for this:
$ if test -e myfile; then echo "myfile exists"; else echo "No it doesn't"; fi
You can also use any number of optional elif
sections before else
, with syntax like the if
part above.
Note here that we have put multiple commands on one line by separating the commands with ;. This is a general feature in bash
shell syntax: we could have also left out the semi-colons and had line breaks each time.
test is a command with a lot more uses than we’ve shown here. You’ll have to read the man page, or guides on the Web to find out more, though.
for-loops¶
for
provides a very powerful cabability: a loop over a set of arguments. Try to understand what the following lines do before running them in your course directory:
$ for i in A B C; do echo $i > baz$i.txt; done
$ ls baz*
$ for i in $(seq 1 9); do echo $i > baz$i.txt; done
$ ls baz*
$ for filename in baz*; do echo -n "$(date): $(pwd)/$filename: "; cat $filename; done
Note that the for loop variable can have any name and that it takes the value of each of the separate words between in
and the semi-colon.
Let’s also see what the expression:
$ echo "foo $(pwd) bar"
does: the $( cmd )
construct uses the output of running cmd in place [1].
Note
Sometimes you’ll see `foo`
(with backtick characters, not apostrophes) instead of $(foo)
. Both are equivalent, but since the bracket form is only one keystroke longer and can be nested, there’s usually no reason to use backticks anywhere.
Finally, let’s do something that’s really tedious to do with a pointy-clicky graphical interface:
$ for i in $(seq 1 1000); do date > foo$i.txt; done
In a fraction of a second, we have created 1000 files with names like foo2.txt.
Now let’s rename them all from foo...txt
to oof...date
and append another line to the contents of each file:
$ for i in foo*.txt; do newname=${i/foo/oof}; newname=${newname%.txt}.date;
mv $i $newname; date >> $newname; done
Have a look at the resulting files. Wouldn’t that have been a nightmare to do by hand?
Real life tends to produce more realistic examples, but the principle is the same: it’s not that uncommon to have to resize 100 plot images or process 1000 data files in a systematic way.
Footnotes
[1] | In this use case, though, the $PWD variable is a better way to get the current directory! |