Bash Shell Scripting/Conditional Expressions
Very often, we want to run a certain command only if a certain condition is met. For example, we might want to run the command cp source.txt destination.txt
("copy the file source.txt
to location destination.txt
") if, and only if, source.txt
exists. We can do that like this:
#!/bin/bash
if [[ -e source.txt ]] ; then
cp source.txt destination.txt
fi
The above uses two built-in commands:
- The construction
[[ condition ]]
returns an exit status of zero (success) ifcondition
is true, and a nonzero exit status (failure) ifcondition
is false. In our case,condition
is-e source.txt
, which is true if and only if there exists a file namedsource.txt
. - The construction
if command1 ; then
command2
fi
command1
; if that completes successfully (that is, if its exit status is zero), then it goes on to runcommand2
.
In other words, the above is equivalent to this:
#!/bin/bash
[[ -e source.txt ]] && cp source.txt destination.txt
except that it is more clear (and more flexible, in ways that we will see shortly).
In general, Bash treats a successful exit status (zero) as meaning "true" and a failed exit status (nonzero) as meaning "false", and vice versa. For example, the built-in command true
always "succeeds" (returns zero), and the built-in command false
always "fails" (returns one).
Caution: Be sure to include spaces before and after |
if
statements
[edit | edit source]if
statements are more flexible than what we saw above; we can actually specify multiple commands to run if the test-command succeeds, and in addition, we can use an else
clause to specify one or more commands to run instead if the test-command fails:
#!/bin/bash
if [[ -e source.txt ]] ; then
echo 'source.txt exists; copying to destination.txt.'
cp source.txt destination.txt
else
echo 'source.txt does not exist; exiting.'
exit 1 # terminate the script with a nonzero exit status (failure)
fi
The commands can even include other if
statements; that is, one if
statement can be "nested" inside another. In this example, an if
statement is nested inside another if
statement's else
clause:
#!/bin/bash
if [[ -e source1.txt ]] ; then
echo 'source1.txt exists; copying to destination.txt.'
cp source1.txt destination.txt
else
if [[ -e source2.txt ]] ; then
echo 'source1.txt does not exist, but source2.txt does.'
echo 'Copying source2.txt to destination.txt.'
cp source2.txt destination.txt
else
echo 'Neither source1.txt nor source2.txt exists; exiting.'
exit 1 # terminate the script with a nonzero exit status (failure)
fi
fi
This particular pattern — an else
clause that contains exactly one if
statement, representing a fallback-test — is so common that Bash provides a convenient shorthand notation for it, using elif
("else-if") clauses. The above example can be written this way:
#!/bin/bash
if [[ -e source1.txt ]] ; then
echo 'source1.txt exists; copying to destination.txt.'
cp source1.txt destination.txt
elif [[ -e source2.txt ]] ; then
echo 'source1.txt does not exist, but source2.txt does.'
echo 'Copying source2.txt to destination.txt.'
cp source2.txt destination.txt
else
echo 'Neither source1.txt nor source2.txt exists; exiting.'
exit 1 # terminate the script with a nonzero exit status (failure)
fi
A single if
statement can have any number of elif
clauses, representing any number of fallback conditions.
Lastly, sometimes we want to run a command if a condition is false, without there being any corresponding command to run if the condition is true. For this we can use the built-in !
operator, which precedes a command; when the command returns zero (success or "true"), the !
operator changes returns a nonzero value (failure or "false"), and vice versa. For example, the following statement will copy source.txt
to destination.txt
unless destination.txt
already exists:
#!/bin/bash
if ! [[ -e destination.txt ]] ; then
cp source.txt destination.txt
fi
All those examples above are examples using the test
expressions. Actually if
just runs everything in then
when the command in the statement returns 0:
# First build a function that simply returns the code given
returns() { return $*; }
# Then use read to prompt user to try it out, read `help read' if you have forgotten this.
read -p "Exit code:" exit
if (returns $exit)
then echo "true, $?"
else echo "false, $?"
fi
So the behavior of if
is quite like the logical 'and' &&
and 'or' ||
in some ways:
# Let's reuse the returns function.
returns() { return $*; }
read -p "Exit code:" exit
# if ( and ) else fi
returns $exit && echo "true, $?" || echo "false, $?"
# The REAL equivalent, false is like `returns 1'
# Of course you can use the returns $exit instead of false.
# (returns $exit ||(echo "false, $?"; false)) && echo "true, $?"
Always notice that misuse of those logical operands may lead to errors. In the case above, everything was fine because plain echo
is almost always successful.
Conditional expressions
[edit | edit source]In addition to the -e file
condition used above, which is true if file
exists, there are quite a few kinds of conditions supported by Bash's [[ … ]]
notation. Five of the most commonly used are:
-d file
- True if
file
exists and is a directory. -f file
- True if
file
exists and is a regular file. -e file
- True if
file
exists, whatever it is. string == pattern
- True if
string
matchespattern
. (pattern
has the same form as a pattern in filename expansion; for example, unquoted*
means "zero or more characters".) string != pattern
- True if
string
does not matchpattern
. string =~ regexp
- True if
string
contains Posix extended regular expressionregexp
. See Regular_Expressions/POSIX-Extended_Regular_Expressions for more information.
In the last three types of tests, the value on the left is usually a variable expansion; for example, [[ "$var" = 'value' ]]
returns a successful exit status if the variable named var
contains the value value
.
The above conditions just scratch the surface; there are many more conditions that examine files, a few more conditions that examine strings, several conditions for examining integer values, and a few other conditions that don't belong to any of these groups.
One common use for equality tests is to see if the first argument to a script ($1
) is a special option. For example, consider our if
statement above that tries to copy source1.txt
or source2.txt
to destination.txt
. The above version is very "verbose": it generates a lot of output. Usually we don't want a script to generate quite so much output; but we may want users to be able to request the output, for example by passing in --verbose
as the first argument. The following script is equivalent to the above if
statements, but it only prints output if the first argument is --verbose
:
#!/bin/bash
if [[ "$1" == --verbose ]] ; then
verbose_mode=TRUE
shift # remove the option from $@
else
verbose_mode=FALSE
fi
if [[ -e source1.txt ]] ; then
if [[ "$verbose_mode" == TRUE ]] ; then
echo 'source1.txt exists; copying to destination.txt.'
fi
cp source1.txt destination.txt
elif [[ -e source2.txt ]] ; then
if [[ "$verbose_mode" == TRUE ]] ; then
echo 'source1.txt does not exist, but source2.txt does.'
echo 'Copying source2.txt to destination.txt.'
fi
cp source2.txt destination.txt
else
if [[ "$verbose_mode" == TRUE ]] ; then
echo 'Neither source1.txt nor source2.txt exists; exiting.'
fi
exit 1 # terminate the script with a nonzero exit status (failure)
fi
Later, when we learn about shell functions, we will find a more compact way to express this. (In fact, even with what we already know, there is a more compact way to express this: rather than setting $verbose_mode
to TRUE
or FALSE
, we can set $echo_if_verbose_mode
to echo
or :
, where the colon :
is a Bash built-in command that does nothing. We can then replace all uses of echo
with "$echo_if_verbose_mode"
. A command such as "$echo_if_verbose_mode" message
would then become echo message
, printing message
, if verbose-mode is turned on, but would become : message
, doing nothing, if verbose-mode is turned off. However, that approach might be more confusing than is really worthwhile for such a simple purpose.)
Combining conditions
[edit | edit source]To combine multiple conditions with "and" or "or", or to invert a condition with "not", we can use the general Bash notations we've already seen. Consider this example:
#!/bin/bash
if [[ -e source.txt ]] && ! [[ -e destination.txt ]] ; then
# source.txt exists, destination.txt does not exist; perform the copy:
cp source.txt destination.txt
fi
The test-command [[ -e source.txt ]] && ! [[ -e destination.txt ]]
uses the &&
and !
operators that we saw above that work based on exit status. [[ condition ]]
is "successful" if condition
is true, which means that [[ -e source.txt ]] && ! [[ -e destination.txt ]]
will only run ! [[ -e destination.txt ]]
if source.txt
exists. Furthermore, !
inverts the exit status of [[ -e destination.txt ]]
, so that ! [[ -e destination.txt ]]
is "successful" if and only if destination.txt
doesn't exist. The end result is that [[ -e source.txt ]] && ! [[ -e destination.txt ]]
is "successful" — "true" — if and only if source.txt
does exist and destination.txt
does not exist.
The construction [[ … ]]
actually has built-in internal support for these operators, such that we can also write the above this way:
#!/bin/bash
if [[ -e source.txt && ! -e destination.txt ]] ; then
# source.txt exists, destination.txt does not exist; perform the copy:
cp source.txt destination.txt
fi
but the general-purpose notations are often more clear; and of course, they can be used with any test-command, not just the [[ … ]]
construction.
Notes on readability
[edit | edit source]The if
statements in the above examples are formatted to make them easy for humans to read and understand. This is important, not only for examples in a book, but also for scripts in the real world. Specifically, the above examples follow these conventions:
- The commands within an
if
statement are indented by a consistent amount (by two spaces, as it happens). This indentation is irrelevant to Bash — it ignores whitespace at the beginning of a line — but is very important to human programmers. Without it, it is hard to see where anif
statement begins and ends, or even to see that there is anif
statement. Consistent indentation becomes even more important when there areif
statements nested withinif
statements (or other control structures, of various kinds that we will see). - The semicolon character
;
is used beforethen
. This is a special operator for separating commands; it is mostly equivalent to a line-break, though there are some differences (for example, a comment always runs from#
to the end of a line, never from#
to;
). We could writethen
at the beginning of a new line, and that is perfectly fine, but it's good for a single script to be consistent one way or the other; using a single, consistent appearance for ordinary constructs makes it easier to notice unusual constructs. In the real world, programmers usually put; then
at the end of theif
orelif
line, so we have followed that convention here. - A newline is used after
then
and afterelse
. These newlines are optional — they need not be (and cannot be) replaced with semicolons — but they promote readability by visually accentuating the structure of theif
statement. - Regular commands are separated by newlines, never semicolons. This is a general convention, not specific to
if
statements. Putting each command on its own line makes it easier for someone to "skim" the script and see roughly what it is doing.
These exact conventions are not particularly important, but it is good to follow consistent and readable conventions for formatting your code. When a fellow programmer looks at your code — or when you look at your code two months after writing it — inconsistent or illogical formatting can make it very difficult to understand what is going on.