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.