This is the second article of a series focused in Gnu Bash scripting. On the first bash scripting article we’ve just created the most simple script: simple commands, one after another. We also saw some variables use.This article will cover bash control structures.
Control structures
As I’ve said on the previous article, laziness is the key to success. If I have to run some commands, a while later the same commands in the same order, when the third time comes I’ll open a text editor and paste those commands to save it as a script.
After a couple of days later, that script start to grow, both in extension and complexity. I need ways to control the execution flow, for example:
- if something happens do something, or if something doesn’t happens do another thing;
- while something is happening, do something over and over, or do something over and over;
- or depending on the value of a variable or what happened, do this or that or another,
- etc
This is what bash control structures are intended for (actually, in bash or any other programing language). But first, we need to know about:
Exit status
From the bash manpage:
«The exit status of an executed command is the value returned by the waitpid system call or equivalent function. Exit statuses fall between 0 and 255.(…) An exit status of zero indicates success (or, equals to ‘true’). A non-zero exit status indicates failure (or, equals to ‘false’).(…. For example:) If a command is not found, the child process created to execute it returns a status of 127. If a command is found but is not executable, the return status is 126».
tl;dr: exit statuts of 0 means true, success; any other value means false. It’s important to learn this because to perform true/false decisions is to check the exit status of a command.
Bash stores the exit status of the last executed command in the $? variable.
if-then-else
This is the if something happens (something happens means a command with an exit status of zero) do something, or else do another thing. The syntax is:
if command then command1 command2 ... else commandA commandB ... fi
As you can see on the screenshot, it can be written on a single line, separating every command with a semi-colon:
if command; then command1; command2;...; else commandA; commandB;...;fi
Depending on the complexity of the if-then block it could be easier to read or harder to read the ‘oneliner’ style.
What if you want to run some command if something happens, run another command only if something else happens? use the keyword elif (else if):
if command then command1 command2 ... elif other_command commandA commandB ... else another block of commands fi
An if-then-else block finishes with the keyword fi (if backwards).
String and number comparison
Remember variables? frequently the something happens is that depending on the value of a variable we want to run some commands or other.
To compare if a string equals to some word or a number es lower than other we use the command test. The general syntax is:
test expression
If expression is omitted its considered false (exit status > 0). If expression is just a string of text it’s true. This could be tricky, for example:
$ test false; echo $? 0
We are not testing the exit status of the command false (there is a command false) but the string false against nothing. The other way to run the test command is enclosing expression with square brackets. This other way is easier to read inside an if-then-else block.
operands | true if |
expression | true |
! expression | false (negates expression) |
expressionA -a expressionB | And. both expressions are true |
expressionA -o expressionB | Or. one of the expressions are true |
string1 = string2 | both strings are equal |
string1 != string2 | strings are different |
int1 -eq int2 | integer1 is equal to integer2 |
int1-ge int2 | int1 >= int2 |
int1 -gt int2 | int1 > int2 |
int1 -le int2 | int1 <= int2 |
int1 -lt int2 | int1 < int2 |
int1 -ne int2 | int1 is not equal to int2 |
-e file | file exists |
-d directory | file exists and is a directory |
There are more expressions, those are the ones that I consider more important. Read the manpage for test to learn the other expressions.
Pay attention that there is an space between both square brackets and the expression. Now we can use test to do something based on the value of a variable. For example:
case
When we want to test a variable against several values we can add multiple elif to our if-then-else block like this:
if [ $a = "value1" ] then command1 for value1 ... commandN for value1 elif [ $a = "value2" ] then command1 for value2 ... commandN for value2 ... elif [ $a = "valueN" ] then command1 for valueN ... commandN for valueN else command1 for every other value ... commandN for every other value fi
Or we can replace with a case command which is easier to read, and to write:
case expression in value1) command1 for value1 ... commandN for value1 ;; value2) command1 for value2 ... commandN for value2 ;; ... *) command1 for every other value ... commandN for every other value ;; esac
Also, less keystrokes, don’t forget to be lazy enough to make the computer work for you. The last value * is the default case, every other value not matched before. Case backwards, esac, is analogous to he fi statement. If the ;; operator is used, no subsequent matches are attempted after the first pattern match. To test the next pattern use the ;;* operator. For example:
while and until
While is do something over and over while something happens. And until is the same but negated (do something over and over while something is not happening). The general syntax is:
while command do command1 command2 ... commandN done
To do something while is not happening (i.e. the exist status is different from 0), replace while with until. See some examples:
Notice they didn’t produce the same output. On the while block, when $a reach the value 4, is not more lower than 4, so the test is evaluated false (and exit code is > 0). On the until block, when $a reach the value 4, is not greater than 4.
A clock in your terminal
Sometimes I’m testing a cronjob and that makes me anxious until it gets executed. I use the true command (that, as you may figured, produces a 0 exit status) as condition, and run the command date over and over:
while true do date sleep 1 clear done
This would run foverer, but I can interrupt with ctrl-c.
for-in-do
This kind of loop iterates over a list provided assigning each item to a variable. Syntax:
for i in list of words do command1 ... commandN done
This is easier to see in an example:
Stay tuned
Things are getting complex, but we want our scripts to take some decisions to do this or that. I was to cover other subjects, but this article about bash control structures has grown too much.
On the next ones I’ll cover pipes and redirection. I’ll probably back to this bash control structures with the examples, something like use the output of a command in a for-in loop and stuff like that.