Shell branches and loops¶
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! |
Customising the shell¶
This section is optional for those who want to make their terminal environment a bit more friendly — ask the demonstrators for more details if you get this far and have the interest.
Setting your PATH
as you learned earlier will only last until you close the terminal. You don’t want to have to do that every time you open a shell session! For this, you can add shell snippets to dot files in your home directory HOME
. Files that start with a dot (kbd{.}) are usually hidden when you use ls, but the -a
switch will reveal them. Try:
$ ls -a $HOME
The important ones for customising bash are .bash_profile
and .bashrc
. You can read these into the shell (rather than executing them:
that would run in a child process and not affect your shell session) using the
source or . command:
$ source ~/.bashrc
or
$ . ~/.bashrc
You can also run reset to reset your shell which will automatically re-read the setup files.
Have a look at the contents of the .bashrc
file using less or cat (or gedit).
Aliases can be used to override commands — they are a kind of extra layer in
command execution before the PATH
gets read. Here’s how to set an alias:
$ alias ls="ls -l"
That sets an alias which changes ls to use the long listing mode by default. Try ls now.
To access a command and guarantee that you aren’t using an alias, you can prefix the command with command:
$ command ls
You can get rid of this rather annoying alias with unalias ls
!
Shell functions are like aliases but are a bit more powerful: the main difference is that they can take arguments. Ask for more information.