Bash Shell Scripting: Control Flow Statements
When a program finishes, it returns a integer value. This code is known as the exit status or exit code. Depending on its value, we can see if the program terminated successfully or not.
If the value is
0, that means the program ran without a problem (this is why you return 0 in C programs). However, any value of
1 or greater means that it terminated unsuccessfully.
So...what do exit statuses have to do with control flow? Great question! We'll how all this ties in together real soon when we learn if-else statements.
Last exit status $?
To find the last exit status, simply use the
Be cautious in that if you run
echo $? twice in a row, the second command will always return a
0. Can you guess why?
Let's try some examples.
$ rm /; echo $? rm: /: is a directory 1 $ ls -l; echo $? # Lists folder contents 0 $ echo $? # Running 'echo $?' twice always produces a success 0
POSIX exit statueses
Here is a list of POSIX exit statuses.
- > 0
- Failure due to redirection or word expansion.
- Unsuccessful (for the most part, but depends on command).
- File not executable.
- Command not found.
- Unspecified, but failure.
- Command failed due to receiving a signal.
$ ls -l hello_world # Not executable -rw-r--r-- 1 johnPC staff 0 Jul 3 09:41 hello_world $ ./hello_world -bash: ./hello_world: Permission denied $ echo $? 126 $ asdf $ echo $? 127
Passing an exit value per script
To terminate your script with a specific exit code, use the
exit code, followed by an integer.
if [ ! -e $SAMP_FILE ]; then exit 32 fi
if statements real soon, but this snippet simply checks if the file does not exist.
Exceptions to the 0 successful rule
In some cases, a program can exit with a value other than
0, and still be considered a successful run.
For example, the
grep command returns a
0 if a matching pattern is found, and a
1 if no matching pattern is found. A value of
2 or greater is reported for real errors.
To ensure of no bugs in your code, make sure to check out a command's EXIT VALUE or DIAGNOSTICS section of its man page!
Logical Expressions [, test
Beware of the disparities of shell scripting!
We are now about to wade into the murkey waters of logical expressions. Do not try to apply other programming language's syntax or concepts to shell scripting, or you'll be unpleasantly surprised... Let's get started!
To evaluate logical expressions, we use the
[ ] brackets. You may think that these brackets are just for syntax, but they're actually a shortcut for the
test command takes in some evaluation and (depending on the result of the statement) returns some exit code.
Unary and boolean operators
Let's first look at the unary and boolean operators before we learn about specific tests.
Inverting a test
To invert a test, place an exclamation mark as the first argument of the
[ command (remember it's not syntax!). For example, the
-e operator checks for a file's existence.
# Test if file1 does not exist [ ! -e file1 ]
The boolean operators AND and OR can be modeled with the
# Test if both file1 and file2 exists [ -e file1 -a file2 ]
Let's now take a look at some examples to see what some logical expressions evaluate to.
test call does not explicitly return what its exit code is, we need to use the
$? command to retrieve the last exit code.
Remember that using the
test command is the same as using brackets.
$ test 'hi' = 'hello'; echo $? 1 $ [ 'hi' = 'hello' ]; echo $? 1 $ test 'hi' != 'hello'; echo $? 0 $ [ 'hi' != 'hello' ]; echo $? 0 $ [ 'hi' === 'hello' ]; echo $? # === is not a real option, so returns an exit code of 2 -bash: test: ===: binary operator expected 2
Notice that if an evaluation is true, it returns a
0, but if false, it returns a
1. Any errors result in an error code of
You can see a list of all test options with the
man test command, but we'll go through them step by step over the following lessons.
Spacing is important!
Be sure to space out your operators properly - the Command Line is particular picky about this! Joining arguments together (
1=3 instead of
1 = 3) can mean errors all over the place, so be cautious!
Handling more than one test with && and ||
If we have more than one command, we can bring them together logically with the && operator or || operators. The evaluation of these boolean operators depend on the exit statuses of the commands.
The shell is designed to be efficient, and so wastes no time running needless commands.
For example, if two commands -
command2 are joined with an && operator, and
command1 returns an exit status other than
command2 is not run.
$ command1 && command2
Similarly, in the case two commands are joined by a || operator,
command2 won't be run if
command1 already has an exit status of
$ command1 || command2
We can use these two properties above to create a simple but efficient if/else-like statement.
$ command1 && command2 || command3
command1 is first executed, then
command2is also executed if
command1 has an exit status of
command3 is run.
Hopefully these evaluations make sense - let's now move onto the different types of tests (file, string and arithemetic) which give much more flexibility to control flow.
With file tests, we can check simple properties of file attributes. The file tests that require just one argument are called unary operators.
There are two simple tests to see if a file exists and if it's empty.
- File exists
- File is not empty
$ [ -e testFile.txt ]; echo $? 1 $ touch testFile.txt $ [ -e testFile.txt ]; echo $? 0 $ [ -s testFile.txt ]; echo $? 1 # File is empty $ echo "Hello world" > testFile.txt $ [ -s testFile.txt ]; echo $? 0
The following checks for the file's type. If any of the options below are used on a file that doesn't exist, the exit code returns a nonzero value.
- Block device
- Character device
- Regular file
- Symbolic link
- Group matches the effective group id of this process
- Named pipe
$ [ -d Downloads ]; echo $? 0 $ touch testFile.txt $ [ -f testFile.txt ]; echo $? 0
We may also check the permissions settings of a file.
- uid has been set
- gid has been set
- Sticky bit is set
$ touch testFile.txt $ ls -l testFile.txt -rw-r--r-- 1 johnPC staff 0 Jul 3 11:02 testFile.txt $ [ -r testFile.txt ]; echo $? 0 $ [ -w testFile.txt ]; echo $? 0 $ [ -x testFile.txt ]; echo $? 1 $ chmod a+x testFile.txt $ [ -x testFile.txt ]; echo $? 0
Lastly, we have comparison operators, which looks at two files.
[ file1 -nt file2 ]
- file1 is newer than file2
- file1 is older than file2
- file1 and file2 share inode numbers
$ touch oldFile.txt $ touch newFile.txt $ [ oldFile.txt -nt newFile.txt ]; echo $? 1 $ [ oldFile.txt -ot newFile.txt ]; echo $? 0
Great! All this will come in handy once we learn key control-flow commands such as if-else statements and loops.
String Tests =, !=
String tests can be useful for checking user's input.
Checking for string equality
To check if two strings are equal or not, simply use the
$ [ 'hi' = 'hi' ]; echo $? 0 $ [ 'hello' = 'hi' ]; echo $? 1
Checking if argument is empty or not
Two unary operators are used to check if a string is empty or not.
- Argument is empty
- Argument is not empty
$ [ -z "" ]; echo $? 0 $ [ -n "" ]; echo $? 1
Here's a simple script that reads an input, and depending on the string put in, returns a specific response.
#!/bin/bash printf 'Please enter a word: ' read input if [ $input = 'hi' ]; then printf 'Hi to you too!' else printf 'You input %s.\n' $input fi
We'll go over if-then-else statements soon, but notice how we can use tests and expressions such as these to control logic in our scripts.
Arithmetic expansions and tests
There are two ways we can have the shell evaluate an expression. Either using
$((expression)) syntax or the
The former does not require proper spacing, while the latter does.
$ echo $((2+3)) 5 $ expr 2 + 3 5
Arithmetic expasion only supports integer values and no decimals.
$ expr 2.3 + 2 expr: not a decimal number: '2.3'
Here are a list of arithmetic operators.
- ++ --
- Increment, Decrement
- + -
- Unary plus and minus
- * / %
- Multiplication, division and remainder
- == !=
- Equal and not equal
- ! && ||
- Logical negation, AND, and OR
- << >>
- Bit-shift left and right
- ~ & ^ |
- Bitwise negation, AND, exclusive OR, and OR
- \= += -= *= /= %=
- Assignment operators
- &= ^- <<= >>= |=
- Bitwise assignment operators
It's important to avoid using the
= symbol when comparing numerals. The
= looks for string equality, not numeric equality. Thus, when working with arithmetic equations, be sure to use
$ [ 1 = 1 ]; echo $? 0 $ [ 092 = 92 ]; echo $? 1 $ [ 092 -eq 92 ]; echo $? 0
Here is a list of more comparison tests we can use to compare two numbers. Be sure to not use canonical notations such as
- Equal to
- Not equal to
- Less than
- Greater than
- Less than or equal to
- Greater than or equal to
$ [ 92 > 32 ]; echo $? 0 # Looks good...(but wrong) $ [ 92 < 32 ]; echo $? 0 # Wrong $ [ 92 -gt 32 ]; echo $? 0 # There we go!
If-else statements if, then, elif, else, fi
Just like any other language, the shell provides if-else statements to handle logic flow. However, the syntax is quite different - let's see how.
No curly braces!
Instead of curly braces or indentations to enclose logical constructs, the shell encloses if-statements within its keywords
fi. There is also the
elif keyword that is the shorthand for else-if.
if command; then # If test condition returns exit status of 0 (success) elif another command; then # Another command is true else # Both commands have exit status not 0 fi
Notice anything different from other programming languages? Instead of using some logical expression bound by parentheses, the shell runs commands and checks their exit statuses! This concept may seem foreign and strange at first, but it'll grow on you with practice.
Be sure to get this concept down since it's used in loops as well! It can be confusing since many
if statements use bracket notations
, which look like syntax. However, recall that the open bracket
[ is just a shortcut for the
When stringing together a group of test commands, use the following syntax:
if [ $value -lt 2 ] && [ $value -gt 0 ]; then # value is between 0 and 2 fi
Sample script - flip a coin!
Now that we know enough control flow logic, let's create a simple coin tossing game! We'll have the user pick heads or tails, and the shell toss a coin. If the user is correct, then he/she wins; if not, he/she loses.
Try implementing it on your own with what we have learned thus far, then compare your answer below.
#!/bin/sh printf "Choose (h)eads or (t)ails: " read user_choice # Make sure user chooses between heads or tails if [ $user_choice != h ] && [ $user_choice != t ]; then echo "Invalid choice. Defaulting to (h)eads." user_choice=h fi # Value of 1 is heads, 2 is tails computer_choice=$((RANDOM % 2 + 1)) if [ $computer_choice -eq 1 ]; then echo "Computer chose heads." else echo "Computer chose tails." fi if [ $computer_choice -eq 1 ] && [ $user_choice = h ]; then # Correct echo "You win!" elif [ $computer_choice -eq 1 ] && [ $user_choice = t | ]; then # Incorrect echo "You lose!" elif [ $computer_choice -eq 2 ] && [ $user_choice = t ]; then # Correct echo "You win!" else # Incorrect echo "You lose!" fi
Great! You're well on your way to becoming a shell scripting master. Let's move onto case statements.
Case statements case, esac
In some cases, a
case statement can be more appropriate. For example, when you have a variable and need to execute lines of code depending on its value, the
case statement makes for clean code.
Case statements do not execute any test commands and therefore do not use exit codes.
Sample script - Hello!
Here's a script that outputs a greetings depending on which country a user is from.
#!/bin/sh echo "Please select which country you are from: " printf "1: Canada 2: Croatia 3: Czech Republic 4: France 5: Germany 6: Greece 7: Italy 8: Netherlands 9: Poland 10: Sweden 11: United States " read -p "Country number code: " input case $input in 1|11) echo 'Hello!' ;; 2) echo 'Bog!' ;; 3) echo 'Ahoj!' ;; 4) echo 'Salut!' ;; 5|8) echo 'Hallo!' ;; 6) echo 'YAH sahs!' ;; 7) echo 'Ciao!' ;; 9) echo 'Czesc!' ;; 10) echo 'Hej!' ;; *) echo 'Please enter a valid number.' ;; esac
Notice the use of the
| operand. We may also use regular expressions if we are trying to match a string!
We may simplify this script with the
select command, which we'll see next.
Sample Script 2 - with Regular Expressions
We can also use case-statements and match input strings with regular expressions!
Let's try to build a script where the user inputs his/her name, and the response changes depending on whether the input starts with a vowel or consonant.
#!/bin/sh printf "What is your first name? " read first_name case $first_name in [aeiouAEIOU]*) echo 'Your name starts with a vowel!' ;; [0-9]*) echo 'Strange that your name would start with a number...' ;; [a-zA-Z]*) echo 'Your name starts with a consonant!' ;; *) echo 'Your name starts with some funky punctuation.' ;; esac