if
, then
, else
, elif
, test
, [...]
, shift
, while
, do
, done
, case
, esac
Updated: 2017-04-13 01:34 EDT
if
, then
, else
, fi
if
… then
… fi
branch statement (no else
)!
if
… then
… else
… fi
branch statementif
… else
instead of !
to preserve the exit statusif
statements: if
statements inside other if
statementstest
helper program – file tests, string tests, and integer expressions-f
test
command are built-in to the shelltest
to test pathname properties, e.g. test -f
test
to compare text strings, e.g. test -z
test
is a non-empty string testtest
to compare integer numbers, e.g. -eq
test
expressions using logical AND -a
and OR -o
test
expressions using exclamation mark !
test
expressionstest
with variables[...]
as a synonym for test
(syntactic sugar):
, true
and false
if
…else
using elif
case
… esac
statement with GLOB patternswhile
, for
, do
, done
shift
so $2
becomes $1
&&
and ||
read
echo 1>&2
Reference: Chapter 7. Conditional statements http://tldp.org/LDP/Bash-Beginners-Guide/html/chap_07.html
Shell scripts are files of command lines that by default execute one-by-one from the first line of the file to the last line. If a human were to run the lines in a shell script by typing them into a shell manually, s/he might notice that a line failed, and thus would not proceed to do the other lines in the script.
Let’s use this three-line example shell script called example.sh
:
$ cat example.sh
mkdir foo
cd foo
date >date.txt
If these lines were typed by a human, it might look like this:
$ mkdir foo
mkdir: cannot create directory ‘foo’: File exists
At this point, the human would stop. The human would not go on to type cd foo
or any of the following commands, because the human notices the error of the failed command and does not proceed.
A non-human shell program reading the same three-line shell script would run all the commands in the script one after the other, even though the first command failed. The cd
would also fail and the date
would then be written into the current directory, not into the new foo
directory:
$ sh -u example.sh
mkdir: cannot create directory ‘foo’: File exists
example.sh: 2: cd: can't cd to foo
$ ls -l
-rw-r--r-- 1 idallen idallen 0 Nov 19 15:25 foo
-rw-r--r-- 1 idallen idallen 32 Nov 19 15:26 example.sh
-rw-r--r-- 1 idallen idallen 29 Nov 19 15:26 date.txt
Control structures (also called flow control statements) are lines you can place in shell scripts to change the order that the shell executes the command lines, so that the shell can do things such as avoid some commands when things go wrong.
Other control structures let the shell repeat a list of commands multiple times, allowing the script to operate on multiple command line arguments or until some exit status is satisfied.
if
statement to check the return codeHere is the same script as above, rewritten with an added if
control structure that checks the result (exit code) of the mkdir
and only does the other two commands if the mkdir
was successful (has exit status zero):
$ cat example.sh
if mkdir foo ; then
cd foo
date >date.txt
fi
The if
control statement in the script file runs the mkdir
command and checks its return code. If the return code is good (zero), the shell runs the two indented statements between the then
and the closing fi
that ends the if
statement. If the exit status is bad (non-zero), the shell skips the two statements in the file and does not run them.
The if
statement automatically checks the return code of the mkdir
command without printing the code on your screen. The if
statement doesn’t need to print the return status; it simply tests it and acts on it. (You can use echo $?
to see a previous return code.)
If we run this new version of the script, the shell does not execute the two commands inside the if
statement if the if
statement detects that the mkdir
fails (with a non-zero exit code):
$ sh -u example.sh
mkdir: cannot create directory ‘foo’: File exists
$ ls -l
-rw-r--r-- 1 idallen idallen 0 Nov 19 15:25 foo
-rw-r--r-- 1 idallen idallen 53 Nov 19 15:36 example.sh
The shell does not print the return status of the commands it executes. If you want to actually see the return status of a command, you need to have the shell
echo
the value of the$?
variable right after running the command.
if
, then
, else
, fi
Unix/Linux shells are designed to find and run commands. This means that the programming control structures of shells are based on the exit statuses of running commands. (Most other programming language control structures are based on mathematical expressions, not running commands.)
A basic shell control structure is a branch that chooses which commands to run based on the success/failure of a command return code.
if
… then
… fi
branch statement (no else
)The simplest control structure is the if
statement that checks the return status of a command (or a list of commands) and only runs another command (or list of commands) if the return status of the original command is successful (zero):
if testing_command_list ; then
zero_command_list
fi
The testing_comamnd_list
is executed, and the return status is tested, then:
zero_command_list
are executed.zero_command_list
is not executed and the shell continues with the rest of the shell script, if any.Either command list can be multiple commands separated by newlines, semicolons, or logical &&
or ||
operators. If the testing_command_list
is multiple commands, only the return status of the last command is tested. Any list may include commands connected by pipelines.
if false ; false ; false ; false ; true ; then
echo "This is true"
fi
After the shell keyword
if
comes atesting_command_list
, not an arithmetic or logical expression as would be found in most programming languages. The shell executes commands; it does not do arithmetic.
Here are some examples of what these simple if
statements might look like inside shell scripts:
#!/bin/sh -u
if mkdir foo ; then
cd foo
date >date.txt
fi
#!/bin/sh -u
if fgrep "$1" /etc/passwd ; then # search for first script argument
echo "I found $1 in file /etc/passwd"
fi
#!/bin/sh -u
if who | fgrep "idallen" ; then
echo "idallen is online"
echo "Hello Ian" | write idallen
fi
Things to notice about the syntax of this control structure:
if
and ends with fi
on a line by itself. The fi
is if
spelled backwards.testing_command_list
from the keyword then
.zero_command_list
are indented right several spaces (or one tab stop).fi
keyword is lined up directly under the if
keyword.testing_command_list
may be a pipeline or a list of several commands. Only the exit status of the last command in a pipeline or list is checked by the shell.Always write if
statements using the above form in this course.
!
Any command’s return status may be logically negated/inverted (turn success to failure or failure to success) by preceding the command with an exclamation mark:
$ cd
$ echo $?
0
$ ! cd
$ echo $?
1
$ rm nosuchfile
rm: cannot remove ‘nosuchfile’: No such file or directory
$ echo $?
1
$ ! rm nosuchfile
rm: cannot remove ‘nosuchfile’: No such file or directory
$ echo $?
0
Inverting the exit status of a command is useful in an if
statement when you want to execute some commands when a command fails, not when it succeeds. The syntax is to precede the testing_command_list
with an exclamation mark to invert the return status:
if ! testing_command_list ; then
nonzero_command_list # list executes on FAILURE, not SUCCESS
fi
For example, the same mkdir
script used above could be rewritten as shown below, so that the indented commands in the if
statement execute when the mkdir
command fails, not when it succeeds:
if ! mkdir foo ; then # note use of ! to invert exit status
echo "$0: mkdir foo failed"
exit 1
fi
cd foo
date >date.txt
The leading exclamation mark !
above turns a failure exit status from mkdir
into a success status (and vice-versa) for the if
statement.
The leading !
means that when mkdir
fails, the non-zero exit status is inverted to a zero exit status and the nonzero_command_list
executes and the script prints a failure message and exits.
Conversely, the leading !
means that when mkdir
succeeds, the zero exit status is inverted to a non-zero exit status and the nonzero_command_list
is not executed. The script does not exit and it continues after the fi
line with the cd
and date
commands.
Inverting the exit status of a command that has different types of non-zero exit statuses (such as the grep
and fgrep
commands) will hide the difference between the exit statuses – all types of non-zero exit status will be inverted to the same success exit status (zero):
$ fgrep "nosuchstring" /etc/passwd
$ echo $?
1 # exit status 1 means "string not found"
$ fgrep "nosuchstring" nosuchfile
fgrep: nosuchfile: No such file or directory
$ echo $?
2 # exit status 2 means "error in pathname"
$ ! fgrep "nosuchstring" /etc/passwd
$ echo $?
0 # exit status 1 inverts to zero
$ ! fgrep "nosuchstring" nosuchfile
fgrep: nosuchfile: No such file or directory
$ echo $?
0 # exit status 2 also inverts to zero
Do not invert an exit status if you need to use it later or if you need to know the difference between different exit statuses:
if ! fgrep "nosuchstring" nosuchfile ; then
echo "fgrep failed with exit status $?" # WRONG: ALWAYS ZERO EXIT STATUS!
fi
The above message always prints “exit status 0” on command failure, since the leading !
always logically negates/inverts a non-zero exit code to a zero exit code that is then placed in the $?
variable.
if
… then
… else
… fi
branch statementWe can include an else
clause inside the if
statement that will contain commands to run if the testing_command_list
fails (has a non-zero exit status). This gives the shell a choice of which commands to run:
if testing_command_list ; then
zero_command_list
else
nonzero_command_list
fi
The testing_comamnd_list
is executed, and the return status is tested, then:
zero_command_list
are executed. The shell skips over (does not run) the commands in the nonzero_command_list
(after the else
keyword).nonzero_command_list
are executed. The shell skips over (does not run) the commands in the zero_command_list
(before the else
keyword).&&
or ||
operators. If the testing_command_list
is multiple commands, only the return status of the last command is tested. Any list may include commands connected by pipelines.After the shell keyword
if
comes atesting_command_list
, not an arithmetic or logical expression as would be found in most programming languages. If the exit status of the command list is zero (success), the zero branch of theif
is taken, otherwise a non-zero exit status causes the non-zero branch to be taken. There are always exactly two branches: zero, and non-zero, and only the commands in one of the branches will execute, never both.
Here are some examples of what these if-else
statements might look like inside shell scripts:
#!/bin/sh -u
if mkdir foo ; then
cd foo
date >date.txt
else
echo 1>&2 "$0: Cannot create the foo directory; nothing done."
exit 1 # exit the script non-zero (failure)
fi
#!/bin/sh -u
if fgrep "$1" /etc/passwd ; then # search for first script argument
state='found' # set variable indicating success
else
state='did not find' # set variable indicating failure
fi
echo "I $state the text '$1' in the file /etc/passwd" # use the variable
Things to notice about the syntax of this control structure:
if
and ends with fi
on a line by itself. The fi
is if
spelled backwards.testing_command_list
from the keyword then
.else
keyword is on a line by itself. It separates the zero_command_list
from the nonzero_command_list
.fi
keyword is lined up directly under the else
keyword that is lined up directly under the if
keyword.zero_command_list
and the nonzero_command_list
are all indented right several spaces (or one tab stop) with respect to the if
, else
, and fi
keywords.Always write if-else
statements using the above form in this course.
if
… else
instead of !
to preserve the exit statusIf you want to know the exact non-zero exit status of a command, you can’t use !
in front of the command to negate/invert the status to zero:
if ! fgrep "nosuchstring" nosuchfile ; then
echo "fgrep failed with exit status $?" # WRONG: ALWAYS ZERO EXIT STATUS!
fi
You can use an if-else
syntax to preserve the exit code for use in $?
:
if fgrep "nosuchstring" nosuchfile ; then
: # do nothing on success
else
echo "fgrep failed with exit status $?" # show correct $? exit status
exit 1
fi
The shell built-in command :
(also called true
) does nothing.
if
statements: if
statements inside other if
statementsOften a shell script needs to test the return code of a command used in one of the branches of an if
statement. For example:
#!/bin/sh -u
# First try: search for first argument in passwd file
# Second try : search for first argument in group file
#
if fgrep "$1" /etc/passwd ; then
echo "I found '$1' in file /etc/passwd"
else
if fgrep "$1" /etc/group ; then
echo "I found '$1' in file /etc/group"
else
echo "I did not find '$1' in /etc/passwd or /etc/group"
echo "Please try again"
fi
fi
A nested if
statement is an if
or if-else
statement that is contained inside one of the two branches of an outer if-else
statement, as shown the example above. We inserted another complete if-else
statement in the nonzero_command_list
branch of the outer if
statement. The second if-else
statement searches for the first command line argument in the /etc/group
file, but it only does the search if the argument was not found in the /etc/passwd
file.
We can nest another if/else
statement into the script:
#!/bin/sh -u
# First try: search for first argument in passwd file
# Second try : search for first argument in group file
# Third try : search for first argument in networks file
#
if fgrep "$1" /etc/passwd ; then
echo "I found '$1' in file /etc/passwd"
else
if fgrep "$1" /etc/group ; then
echo "I found '$1' in file /etc/group"
else
if fgrep "$1" /etc/networks ; then
echo "I found '$1' in file /etc/networks"
else
echo "I did not find '$1' in passwd, group, or networks"
echo "Please try again"
fi
fi
fi
You can nest control statements as much as you like, though deeply nested structures can be hard to read and understand. Keep things simple!
Pay careful attention to the indentation that makes the statement easier to read for humans inside shell scripts. The shell itself doesn’t care about indentation, but humans do when reading the scripts. At the shell command line, you can dispense with the indentation and type an if/else
statement all on one line, if you like:
$ if mkdir foo ; then echo OK ; else echo BAD ; fi
Don’t do the above one-line if
statements inside shell scripts! Space out the statements using proper indentation inside scripts, and don’t put multiple commands on the same line:
#!/bin/sh -u
# Using correct indentation is essential inside shell scripts
if mkdir foo ; then
echo OK
else
echo BAD
fi
Use comments lines to explain what your script is doing.
test
helper program – file tests, string tests, and integer expressionsDespite the command-oriented nature of the Unix/Linux shell, people often want shell scripts to make conditional decisions based on things that do not directly involve running commands:
--help
2
Since shell if
statements can only act on the exit status of a command (or command list), we need a helper command to do the comparison work and set an exit status if we want to do any of the above three kinds of tests.
In this course, we will follow the traditional shell syntax for doing the above three types of tests using a helper command named test
to do the tests for us. This traditional syntax works in all Bourne-style shells, at least back to 1972 or so. (Recent Bourne-style shells have added syntax to allow arithmetic expressions to directly follow the if
keywords; but, this is not universal and not all shells can do this.)
The test
helper command accepts blank-separated arguments to be tested or compared. It sets its own return code and exits depending on whether or not the supplied test comparison succeeded (exit zero) or not (exit non-zero).
-f
Here is the test
helper program being used to test to see if pathnames /bin/bash
and then /bin/nosuchfile
are existing files:
$ test -f "/bin/bash"
$ echo $?
0 # zero means success (is a file)
$ test -f "/bin/nosuchfile"
$ echo $?
1 # non-zero means failure (is not a file)
The test
command normally has no output, unless something goes wrong. Because test
only sets an exit status, it doesn’t print anything on your screen. It only sets its return code, based on the tests you ask it to do. You can use echo
to make the invisible exit status of test
visible at the command line, by displaying the command exit status left in shell’s $?
variable, as in the examples above.
You can use the test
command in an if
conditional control structure and check its return status just as you would use any other command:
$ cat example.sh
#!/bin/sh -u
if test -f "$1" ; then
echo "File '$1' is a file"
ls -l "$1"
else
echo "'$1' is not a file"
fi
Running the above script:
$ ./example.sh /bin/bash
File '/bin/bash' is a file
-rwxr-xr-x 1 root root 1037464 Aug 31 2015 /bin/bash
$ ./example.sh /bin/nosuchfile
'/bin/nosuchfile' is not a file
In the example above, the test
program silently tests to see if the pathname argument given as the first script argument is an existing file. If it is, test
returns a successful (zero) exit status and the success half of the if
statement is executed; otherwise, test
returns failure (non-zero) and the failure half of the if
statement is executed. The test
program itself has no output; it only sets a return code.
Critical section: There is a small period of time between the testing of the existence of the filename and the use of the filename, and someone could, in theory, remove the file in between the testing and the using, causing
ls
to give an error. This is unlikely to happen, but it’s not impossible. You must be wary of these possible faults in your programming logic.
test
command are built-in to the shellThe test
program has a large number of tests and operations it can do that are essential in control statements in shell scripts. Because it is used so often and is so important, many shells (including the bash
, dash
, and Ubuntu sh
shells) have a built-in version of test
that is documented in the manual page for the shell.
The shell built-in test
may be slightly different from the external one documented in the test
manual page. See the manual page for your sh
shell for the most accurate documentation. The test
command used by sh
scripts under modern versions of Ubuntu Linux is the one in the dash
shell manual page: see man sh
See the test
documentation for the full list of things this command can do. We will concentrate on a few key tests.
The test
helper command has three main categories of things it can test:
test -f
test -z
test 4 -lt 9
Some key examples from each category follow.
test
to test pathname properties, e.g. test -f
You can test most any property or attribute pertaining to an object in the file system. These tests are commonly used:
test -e "pathname" # true if pathname exists (any kind of path)
test -f "pathname" # true if pathname is a file
test -d "pathname" # true if pathname is a directory
test -r "pathname" # true if pathname is readable
test -w "pathname" # true if pathname is writable
test -x "pathname" # true if pathname is executable
test -s "pathname" # true if pathname has size larger than zero
All the pathname tests also fail if the pathname does not exist or is not accessible (because some directory prevents access to the pathname).
Here is a script example that tests if its first argument is accessible and is a file:
#!/bin/sh -u
if test -f "$1" ; then
echo "Pathname '$1' is an accessible file"
else
echo "Pathname '$1' is inaccessible, missing, nor not a file"
fi
test
pathname operator fails, it may also fail because you have no permission to search one of the directories in the pathname, or because the pathname simply doesn’t exist./dev/null
pathname that is neither a file nor a directory.)See the manual page for other less common types of pathname tests.
test
to compare text strings, e.g. test -z
Since the shell if
keyword must be followed by a command name, to compare strings we must execute the test
helper command to do the string comparison for us. The test
helper command will do the comparison of the strings given on its command line and set its return status depending on the result of the comparison:
test -z "$1" # true if length of argument $1 is zero (empty string)
test -n "$1" # true if length of argument $1 is not zero
test "$1" = "foo" # true if argument $1 and foo are the same strings
test "$1" != "foo" # true if argument $1 and foo are not the same strings
Here is an example that tests if the first script argument is the text string --help
:
#!/bin/sh -u
if test "$1" = '--help' ; then
echo "Usage: $0 [pathname]"
exit 3
fi
test
is a non-empty string testWARNING: Any time the test
program is given exactly one argument, it assumes the argument is preceded by -n
and it tests the single argument for a non-empty string, so these are equivalent:
test "$1" # true if length of argument $1 is not zero
test -n "$1" # true if length of argument $1 is not zero
Because it is not an error to forget to use -n
, this single-argument default to the -n
string test is the cause of many shell programming mistakes. In the example below, the command does not do what it looks like it does at first glance; it does not test equality between two strings:
test "abc"="def" # success because "abc=def" is one non-empty string argument!
If you use the above test
expression in a script, it is a one-argument test -n
command, not a three-argument string comparison! The resulting exit status will always be zero (success) because the single argument abc=def
is not an empty string!
The correct way to test string equality is to add blanks around the equals sign to make sure the test
command sees three separate arguments:
test "abc" = "def" # correct way to test if string1 is not equal to string2
Make sure you surround all test
operators with blanks on both sides so that the test
command sees separate arguments, or else everything will look like a one-argument string test and will always succeed:
test "abc"="def" # WRONG! missing blank - always succeeds (exit zero)
test -f/dev/null # WRONG! missing blank - always succeeds (exit zero)
test -s/dev/null # WRONG! missing blank - always succeeds (exit zero)
test -z"abc" # WRONG! missing blank - always succeeds (exit zero)
test
to compare integer numbers, e.g. -eq
Since the shell if
keyword must be followed by a command name, to compare numbers we must execute the test
helper command to do the numeric comparison for us. The test
helper command will do the numeric comparison of numbers given on its command line and set its return status depending on the result of the comparison.
The test
helper program can compare integers using one of six comparison operators. There are only six possible combinations for comparing two integer numbers n1
and n2
:
test n1 -eq n2 # true if n1 and n2 are the same numeric value
test n1 -ne n2 # true if n1 and n2 are not the same numeric value
test n1 -lt n2 # true if n1 is less than n2
test n1 -le n2 # true if n1 is less than or equal to n2
test n1 -gt n2 # true if n1 is greater than n2
test n1 -ge n2 # true if n1 is greater than or equal to n2
Example use of a numeric test on the number of command line arguments:
if test $# -ne 2 ; then
echo 1>&2 "$0: Expecting two arguments; found $# ($*)"
exit 1
fi
These six numeric test
operators only work on integers, not empty strings, letters, or other non-digits. If you try to compare a non-integer with one of these six operators, test
issues an error message. The error message is slightly different depending on which shell is reading your shell script:
sh$ test 1 -eq a
sh: 1: test: Illegal number: a
bash$ test 1 -eq a
bash: test: a: integer expression expected
bash$ test 1 -eq ""
bash: test: : integer expression expected
Boolean Logic Warning: Note that the opposite of the condition “less than” is “greater than or equal to”, not “greater than”. Don’t make this mistake!
Comparing numbers and comparing strings do not always give the same results:
test 0 = 00 # fails because s1 not equal to s2
test 0 -eq 00 # succeeds because zero equals zero
test 0 = " 0 " # fails because s1 not equal to s2
test 0 -eq " 0 " # succeeds because zero equals zero
You will often see people use a string equality test on two numbers in shell scripts, where the correct test should be the numeric equality test:
if test $# = 0 ; then ... # should use -eq for numbers
if test $# -eq 0 ; then ... # always use use -eq for numbers
In most cases, the results are the same, but be careful of cases where the strings may differ but the numbers may be equal. To be safe, always use one of the numeric comparison operators for integers.
test
expressions using logical AND -a
and OR -o
You can test more than one thing in a test
helper expression by separating each test expression using -o
for logical OR and -a
for logical AND:
test -f "path" -a -s "path" # true if path is a file AND path is not empty
test -d "path" -o -f "path" # true if path is a directory OR path is a file
Remember that each test expression on either side of the AND or the OR must be a complete and valid test expression:
test "$1" = 'dog' -o "$1" = 'cat' # correct use of two expressions separated by -o
test "$1" = 'dog' -o 'cat' # WRONG - second expression is always true
The last line, above is the OR of these two test expressions:
test "$1" = 'dog'
test 'cat'
The last expression – a single-argument test
expression – is always true because the argument is not an empty string, so the logical OR of the above two expressions is also always true, which is probably not what you want.
When both AND and OR are present, the -a
AND operator has higher precedence than the -o
OR operator. (You can think of AND as being similar to multiplication and OR being similar to addition in precedence.)
test
expressionsYou can create more complex combinations of AND and OR logic by using parentheses for grouping, but you must hide the parentheses from the shell using quoting.
Example A: The following logic succeeds if the path is not empty and is either a directory or a file:
# Example A - note the quoting of the parentheses
test -s "path" -a \( -d "path" -o -f "path" \)
Without parentheses, the conjunction -a
AND operator has precedence (binds more tightly) than the disjunction -o
OR operator. The Example A expression above, when used without parentheses, has a completely different meaning because logical -a
AND binds more tightly than logical -o
OR.
Example B: The expressions below are equivalent (and are not the same as the Example A expression above);
# Example B - same - logical -a binds before -o
test -s "path" -a -d "path" -o -f "path"
test \( -s "path" -a -d "path" \) -o -f "path" # same as above
The above Example B means “succeed if path is not empty and is a directory, OR succeed if path is a file”. Without parentheses, the file could be empty, which was not true in Example A that used parentheses to group the -o
expressions together.
You can always use parentheses to make sure your logic binds the way you intend, instead of relying on default precedence.
test
expressions using exclamation mark !
You can negate/invert the exit status of any test expression by inserting an exclamation point at the start of the test expression, e.g.
test ! -e "pathname" # true if pathname does *not* exist
test ! -w "pathname" # true if pathname does *not* exist or is *not* writable
test ! -s "pathname" # true if pathname does *not* exist or has size zero
The !
negation operator applies only to the single closest expression, not to the whole expression:
test ! -f path -o -d path # if path is not a file OR if path is a directory
In programming terms, the negation !
operator has highest precedence over all the other Boolean operators. If you want to negate an entire expression, you have to put the expression in parentheses:
test ! \( -f path -o -d path \) # if path is not a file or directory
You can always use parentheses to make sure your logic binds the way you intend, instead of relying on default precedence.
test
expressionsNegating a test
expression using the exclamation mark !
may sometimes have the same effect as using an exclamation mark at the start of the whole testing_command_list
to negate the whole test
command (as shown above), but negating a single test
expression applies just to that expression in the test
command, not to the exit status of the whole test
command.
These next two ways of negating (inverting) the exit status of a single test
expression are the same. The first negation is done by the test
command on the single expression; the second negation is done by the shell on the exit status of the test
command:
if test ! -e "path" ; then ... # true if pathname does *not* exist
if ! test -e "path" ; then ... # same as above; done by shell
In shell programming, we prefer the first syntax, with the negation happening inside the test
expression. We rarely negate the entire test
command (as in the second example, above). Use the first syntax.
test
expressionsThese complex negations below are not equivalent, since the second one negates the entire test
exit code (both expressions) and not just the first expression as in the first line:
if test ! -e "path" -o -d "path" ; then ... # only one expression is negated
if ! test -e "path" -o -d "path" ; then ... # WRONG! NOT THE SAME !
The second statement above is technically a negation of a disjunction (OR), so we need to call upon De Morgan’s Law to see what it really means:
If you know De Morgan’s laws, then you know that a negation of a disjunction (OR) is the same as a conjunction (AND) of the negations (and vice-versa), so these two lines are equivalent:
if ! test -e "path" -o -d "path" ; then ...
if test ! -e "path" -a ! -d "path" ; then ...
The deMorgan logic transform of the first statement above into the second statement above looks like this, where A
is -e "path"
and B
is -d "path"
:
if not test (A or B) ; then
if test (not A and not B) ; then # deMorgan !(A or B) --> (!A and !B)
These are also equivalent statements, with the first one being simpler and easier to understand:
if test ! -e "path" -o -d "path" ; then ... # only one expression is negated
if ! test -e "path" -a ! -d "path" ; then ... # deMorgan
Shell programming prefers to put the negations inside the test helper command, not in front of it. Keep thing simple!
!
in front of test
or [
In shell programming, we prefer to have the test
command do the negations inside its own expressions. We rarely would use the leading !
on the exit status of the test
command, so we prefer the first statements in each pair below:
if test ! -e "path" ; then ... # use this syntax (preferred)
if ! test -e "path" ; then ... # DO NOT USE THIS (bad form)
if [ ! -e "path" ] ; then ... # use this syntax (preferred)
if ! [ -e "path" ] ; then ... # DO NOT USE THIS (bad form)
test
with variablesScripts often use the test
helper command to test the contents of variables, so one or both of the arguments is often a double-quoted variable to be expanded inside the script:
if test "$1" = '/' ; then echo "Using ROOT directory" ; fi
if test "$1" = "$2" ; then echo "Two arguments are identical" ; fi
if test -r "$1" ; then echo "Argument $1 is a readable pathname" ; fi
Here is an example script that tests the number of positional parameters (arguments) to the script via the $#
variable:
#!/bin/sh -u
if test "$#" -eq 0 ; then
echo "$0: The script has no arguments"
fi
if test "$#" -ge 1 ; then
echo "$0: The first argument of $# is '$1'"
fi
if test "$#" -ge 2 ; then
echo "$0: The second argument of $# is '$2'"
fi
if test "$#" -ge 3 ; then
echo "$0: The script has three or more arguments: $#"
fi
Running the above script:
$ ./example.sh one two three four
./example.sh: The first argument of 4 is 'one'
./example.sh: The second argument of 4 is 'two'
./example.sh: The script has three or more arguments: 4
[...]
as a synonym for test
(syntactic sugar)Someone in the Unix past decided that shell if
and while
control statements should look more like the statements found in programming languages. For example, the C programming language uses parentheses around its conditional expressions:
if ( x > 3 ) { /* the C programming language */
They came up with the idea of making an alias for the test
helper command that would be named [
(left square bracket). The test
command was rewritten so that, if it were called by the name [
, it would ignore a final argument of ]
(right square bracket). We could now replace this ugly test
syntax:
if test "$1" = 'yes' ; then
echo "Argument $1 is 'yes'"
fi
with this more elegant bracket syntax, using [
as an alias for test
:
if [ "$1" = 'yes' ] ; then
echo "Argument $1 is 'yes'"
fi
This is still the test
command, executing under the alias of the command name [
, and ignoring the final argument ]
. Those square brackets look similar to the parentheses used in some programming languages; but, you must remember that they are not punctuation. Each one of those square brackets must be a separate blank-separated token to the shell, and that means both brackets must be surrounded by blanks on both sides (except that you don’t actually need a blank before the semicolon).
Ever since that day, most shell scripts now use the square-bracket [
form of test
because it looks nicer. Students of the shell must remember that this square bracket [
form is not punctuation; it is simply syntactic sugar that uses a command name alias for test
that happens to be a square bracket. Use blanks around the brackets!
Syntactic sugar is a feature added to the syntax of a language that makes it easier or more elegant for humans to use, but that does not increase the power or range of things that can already be done. Using
[...]
instead of the command nametest
is syntactic sugar.
Below is a complete script to check how many times the SSH port was attacked on a particular date. If no date is given on the command line, yesterday is assumed. The test
helper program is used multiple times via its square-bracket alias syntax:
#!/bin/sh -u
# $0 [ 'Mon dy' ]
# Count the SSH attacks on the (optional) given date in 'Mmm dy' form.
# If no date given, count attacks yesterday.
# Date must be six characters with leading month and space-padded day
# e.g. 'Nov 10', 'Nov 9', 'Jan 31', 'Jan 1', etc.
# -Ian! D. Allen - idallen@idallen.ca - www.idallen.com
PATH=/bin:/usr/bin ; export PATH
umask 022
# Check for too many arguments.
if [ $# -gt 1 ] ; then
echo 1>&2 "$0: Only expecting one optional date argument, found $# ($*)"
echo 1>&2 "Usage: $0 'Mon dy'"
exit 1
fi
# Make sure we can read the SSH log file.
AUTH=/var/log/auth.log
if [ ! -r "$AUTH" ] ; then
echo 1>&2 "$0: You are not allowed to read '$AUTH'"
exit 1
fi
# If no arguments, default the date to yesterday.
if [ $# -eq 0 ] ; then
date=$( date +"%b %e" --date=yesterday )
else
date=$1
fi
echo "Checking for attacks on '$date'"
# Extract and count lines on the given date that are attacking lines:
attacks=$( fgrep "$date " "$AUTH" | fgrep -c ": refused connect from " )
if [ "$attacks" -eq 0 ] ; then
echo "No attacks recorded on '$date'"
else
echo "Attacks recorded on '$date': $attacks"
fi
Running the above script:
$ ./example.sh
Checking for attacks on 'Nov 18'
Attacks recorded on 'Nov 18': 262
$ ./example.sh 'Nov 17'
Checking for attacks on 'Nov 17'
Attacks recorded on 'Nov 17': 367
$ ./example.sh too many arguments
./example.sh: Only expecting one optional date argument, found 3 (too many arguments)
Usage: ./example.sh 'Mon dy'
The script isn’t perfect, since it doesn’t tell you if you supply a date argument that isn’t in the correct format, or if the date you supply isn’t in the range of dates recorded in the SSH log file. An incorrect date format simply produces no results or the wrong results:
$ ./example.sh 'nov 1' # wrong date format
Checking for attacks on 'nov 1'
No attacks recorded on 'nov 1'
$ ./example.sh '' # wrong date format
Checking for attacks on ''
Attacks recorded on '': 1866
With more script programming effort, we could check the date format to make sure it was correct before using it inside the script.
The shell is not a normal programming language and it does not do arithmetic or mathematics well. To do these things, the shells used to require more helper programs.
There are many historic ways to use helper programs to allow the shell to do integer arithmetic.
expr
arithmetic helper programThe oldest and most universal arithmetic helper is the expr
external command, and it is often used in older scripts inside the historic back-quote form of Command Substitution to capture its output value:
$ expr 2 + 2 \* 9
20
$ x=`expr 2 + 2 \* 9` # historic `...` command substitution syntax
$ echo "$x"
20
The individual arguments to the expr
command constitute the arithmetic expression to be evaluated and the value is printed on standard output. Shell meta-characters such as *
(GLOB) must be quoted to hide them from the shell. All numbers and operators must be individual arguments, separated by blanks, otherwise expr
simply echoes the unrecognized expression back to the user without any error message and without doing any mathematics:
$ expr 2 + 2
4
$ expr 2+2
2+2
The expr
command is the original shell arithmetic helper command. It is standard and works in all Unix/Linux shells ever written.
Because expr
is an external command in many shells, not a shell built-in, it is slow. Using it in a loop that has to iterate hundreds or thousands of times will not be fast.
You can read more in the manual page expr(1)
.
let
built-in commandSome later Bourne shells invented and used the let
built-in helper command to do arithmetic:
$ let x=4 y=5 z=x+y
$ echo "$x $y $z"
4 5 9
$
=
is the basic assignment operatorbash
man pageThe let
helper command is not universally available in all Bourne shells. (In particular, the dash
shell used as /bin/sh
under Ubuntu does not support it!) Don’t use it in new scripts.
$((...))
syntax for Arithmetic ExpansionThe modern way to do arithmetic that we will use in this course does away with all helper programs. Arithmetic expressions are done directly by the shell using a special dollar-and-double-parenthesis Arithmetic Expansion syntax: $((
expression
))
The enclosed expression is designed for arithmetic and is not GLOB expanded. Arguments do not need surrounding spaces or special quoting and variables do not need leading $
:
$ echo $(( 2 + 2 * 9 ))
20
$ echo $((2+2*9))
20
$ x=2 y=3 ; echo $((x*y))
6
$((
expression
))
is an Arithmetic Expansion, like a variable expansion, and it can be used anywhere that you might use a variable.$((
expression
))
is is replaced by the results of the evaluated Arithmetic ExpressionExample (remember blanks are optional inside the expression):
#!/bin/sh -u
if [ $# -gt 3 ] ; then
echo 1>&2 "$0: Expecting max 3 arguments, not $# ($*)"
echo 1>&2 "$0: Remove $(( $# - 3 )) arguments from the command line."
fi
Running the above script:
$ ./example.sh k k k k k k k k
./example.sh: Expecting max 3 arguments, not 8 (k k k k k k k k)
./example.sh: Remove 5 arguments from the command line.
$[...]
syntax for Arithmetic ExpansionSome older versions of the Bourne shells use the old syntax $[
expression
]
for Arithmetic Expansion, but this is now obsolete and deprecated:
$ echo $[2+2*9] # deprecated; do not use this syntax
20
:
, true
and false
The shell built-in commands :
(colon) and true
do nothing and always exit with a success (zero) return status. You can use either one as a place-holder in cases where you don’t want to execute a real command, as in this example below when we want to echo the failure return status of a command but don’t want to do anything if the command succeeds:
if somecommand ; then
: # do nothing on success
else
echo "somecommand: failed with exit status $?" # show exit status
fi
The value in $?
would not be correct if we inverted the command exit status using !
, as in this shorter but incorrect version:
if ! somecommand ; then
echo "somecommand: failed with exit status $?" # WRONG: ZERO EXIT STATUS!
fi
The above message always prints “status 0” on command failure, since the leading !
always negates a non-zero exit code to a zero exit code that is then placed in the $?
variable.
The shell built-in command false
does nothing and always exits with a failure (non-zero) exit status. It’s the same as ! true
and I have no idea why anyone would use it, except as an example of a program that returns a non-zero exit code:
$ false
$ echo "Return code $?"
Return code 1
if
…else
using elif
To improve script readability, a set of nested if
statements can be simplified using the elif
keyword that combines else
and if
together:
Before:
if [ "$1" -eq "0" ] ; then
size='empty'
else
if [ "$1" -lt "10" ] ; then
size='small'
else
if [ "$1" -lt "100" ] ; then
size='medium'
else
size='large'
fi
fi
fi
echo "We classify '$1' as '$size'."
After combining every else
with its following if
:
if [ "$1" -eq "0" ] ; then
size='empty'
elif [ "$1" -lt "10" ] ; then
size='small'
elif [ "$1" -lt "100" ] ; then
size='medium'
else
size='large'
fi
echo "We classify '$1' as '$size'."
The above combined elif
syntax has the same meaning as the nested if
statement above, but it is four lines shorter and the indentation is only one level for the whole statement.
case
… esac
statement with GLOB patternsOften we want to see if a string (usually inside a variable) contains any one of a list of different things. Doing this with if
statements can be tedious. Here is a tedious example:
#!/bin/sh -u
if [ "$1" = "dog" ] ; then
kind='animal'
elif [ "$1" = "cat" ] ; then
kind='animal'
elif [ "$1" = "goat" ] ; then
kind='animal'
elif [ "$1" = "pig" ] ; then
kind='animal'
elif [ "$1" = "apple" ] ; then
kind='fruit'
elif [ "$1" = "peach" ] ; then
kind='fruit'
elif [ "$1" = "plum" ] ; then
kind='fruit'
elif [ "$1" = "cherry" ] ; then
kind='fruit'
else
kind='unknown'
fi
echo "We classify '$1' as '$kind'."
Running the above script:
$ ./example.sh plum
We classify 'plum' as 'fruit'."
$ ./example.sh pig
We classify 'pig' as 'animal'."
The above program works, but it’s too long. We can do better.
The shell provides a simplified way of testing one string against multiple other strings using the case
/esac
statement that has this syntax:
case "test-string" in
patterns1 )
command_list1
;;
patterns2 )
command_list2
;;
patterns3 )
command_list3
;;
* ) # the "default" if nothing else matches
command_list_default
;;
esac
pattern
is a shell GLOB pattern to be matched against the test-string
in order from top-to-bottom. The first match wins and the command_list
closest to the matching pattern is executed. Any other subsequent matches are not executed. Only the one command_list
is used.pattern
to match. If no pattern
matches, no command_list
is executed and the case
statement does nothing.pattern
must be a single unbroken word to the shell – no spaces or word-breaking characters such as semicolon allowed. If you want to match spaces or other special characters in a GLOB pattern, quote them all, e.g. use the quoted pattern "My Documents")
not My Documents)
*
matches anything. If present in a case
statement, it is always the last pattern in the case
statement and always matches the test-string
(if nothing else has matched first). Consider *
as the default match if nothing else matches.command_list
is only one line, it is often placed adjacent to its associated pattern instead of on a separate line. See the example below.pattern
can actually be a list of patterns to match separated by or |
characters, e.g. 'dog' | 'cat' | 'pig' )
case
statement GLOB patterns do match leading periods in the test-string
(because the test-string
could be any string, not just a file name).Below is the same example as before, eight lines shorter and much easier to read. None of the pattern matches are GLOB patterns in this example; they are all quoted fixed strings to be matched exactly against the first command line argument in the $1
positional parameter:
#!/bin/sh -u
case "$1" in
'dog' ) kind='animal' ;;
'cat' ) kind='animal' ;;
'goat' ) kind='animal' ;;
'pig' ) kind='animal' ;;
'apple' ) kind='fruit' ;;
'peach' ) kind='fruit' ;;
'plum' ) kind='fruit' ;;
'cherry' ) kind='fruit' ;;
* ) kind='unknown' ;; # the "default" if nothing else matches
esac
echo "We classify '$1' as '$kind'."
Running the above script:
$ ./example.sh plum
We classify 'plum' as 'fruit'.
$ ./example.sh pig
We classify 'pig' as 'animal'.
Since none of the patterns above contain any GLOB characters, we don’t actually need to quote any of them, but quoting them does show that we are not using any GLOB matching here.
|
for multiple GLOB patterns in case
statementsWe can condense the script even more by using or |
characters to put multiple GLOB patterns on the same line:
#!/bin/sh -u
case "$1" in
'dog' | 'cat' | 'goat' | 'pig' ) kind='animal' ;;
'apple' | 'peach' | 'plum' | 'cherry' ) kind='fruit' ;;
* ) kind='unknown' ;; # the "default" match
esac
echo "We classify '$1' as '$kind'."
This program is now about one-third the size of the equivalent program that used nested if
statements to do the same thing.
Below is an example using GLOB patterns to identify the first command-line argument in the positional parameter variable $1
:
#!/bin/sh -u
case "$1" in
'' ) type='missing (empty)' ;;
/* ) type='an Absolute Pathname' ;;
*/ ) type='a Relative Pathname ending in a slash' ;;
*/* ) type='a Relative Pathname in some directory' ;;
*' '* ) type='a Relative Pathname with blank(s)' ;;
* ) type='a Relative Pathname in the current directory' ;; # the "default" match
esac
echo "Pathname '$1' is $type"
case
statement GLOB pattern to match some text anywhere in the test-string
, you need to put GLOB *
characters at either end of the text: */*
*
in the pattern makes the text match only at the beginning of the test-string
: /*
*
makes the text match only at the end: */
test-string
, quote them all in the GLOB pattern to hide them from the shell: *' '*
Running the above script:
$ ./example.sh ''
Pathname '' is missing (empty)
$ ./example.sh /etc/passwd
Pathname '/etc/passwd' is an Absolute Pathname
$ ./example.sh a/b/c/
Pathname 'a/b/c/' is a Relative Pathname ending in a slash
$ ./example.sh foo/bar
Pathname 'foo/bar' is a Relative Pathname in some directory
$ ./example.sh "foo bar"
Pathname 'foo bar' is a Relative Pathname with blank(s)
$ ./example.sh foobar
Pathname 'foobar' is a Relative Pathname in the current directory
*
As when matching file names on a shell command line, GLOB pattern characters in case
statements must be unquoted to behave as GLOB characters. Quoting hides the GLOB characters from the shell and makes them into ordinary characters that must match the test-string
exactly:
#!/bin/sh -u
case "$1" in
'*' ) msg='a single asterisk (star)' ;;
'*'* ) msg='an asterisk at the beginning of the argument' ;;
*'*' ) msg='an asterisk at the end of the argument' ;;
*'*'* ) msg='an asterisk in the middle of the argument' ;;
* ) msg='an argument with no asterisk' ;; # the "default" match
esac
echo "You entered $msg: $1"
Running the above script:
$ ./example.sh '*'
You entered a single asterisk (star): *
$ ./example.sh '*foobar'
You entered an asterisk at the beginning of the argument: *foobar
$ ./example.sh 'foobar*'
You entered an asterisk at the end of the argument: foobar*
$ ./example.sh 'foo*bar'
You entered an asterisk in the middle of the argument: foo*bar
$ ./example.sh 'no star here'
You entered an argument with no asterisk: no star here
Below is another example using more complex GLOB patterns to match the number of digits inside a number, allowing a crude method for checking number ranges:
#!/bin/sh -u
case "$1" in
0 ) price='free' ;; # 0
[1-9] ) price='cheap' ;; # 1...9
[1-4][0-9] ) price='middle' ;; # 10...49
[5-9][0-9] ) price='upper' ;; # 50...99
[1-9][0-9][0-9] ) price='high' ;; # 100...999
[1-9][0-9][0-9][0-9] ) price='exorbitant' ;; # 1000...9999
* ) price='impossible' ;; # the "default" match
esac
echo "We classify '$1' as '$price'."
Running the above script:
$ ./example.sh 37
We classify '37' as 'middle'.
$ ./example.sh 2348
We classify '2348' as 'exorbitant'.
$ ./example.sh crap
We classify 'crap' as 'impossible'.
Using GLOB patterns to test numeric ranges has its limitations, and doesn’t always work as nicely as one would like (since we are comparing digits and not evaluating and comparing numeric values)
$ ./example.sh 037 # 037 is in range 10...49
We classify '037' as 'impossible'.
$ ./example.sh 3.14 # 3.14 is in range 1...9
We classify '3.14' as 'impossible'.
Shells find and run commands; they don’t do mathematics very well.
Since GLOB patterns can also match characters that are not in a range by adding !
inside a character class, we can use GLOB patterns to detect input that is, for example, not alphabetic:
#!/bin/sh -u
case "$1" in
'' ) echo "Empty string" ;;
*[![:alpha:]]* ) echo "Non-alphabetic '$1'" ;;
* ) echo "Alphabetic: '$1'" ;;
esac
The above script has a case
GLOB pattern that matches a non-alphabetic character anywhere in the string, by using the complement (inverse) of a POSIX character class named [:alpha:]
that represents alphabetic characters. If that pattern matches in the argument, we know the argument has a non-alphabetic character in it somewhere:
$ ./example.sh happy
Alphabetic: happy
$ ./example.sh abc0def
Non-alphabetic: abc0def
You can use case
statements and appropriate complemented POSIX character classes to make sure that arguments contain only the types of characters you want: letters, digits, spaces, printable characters, etc.
For a list of POSIX character classes, see POSIX Character Classes.
*[![:digit:]]*
Below are steps to show you how you can understand what the complemented POSIX character class means in this GLOB pattern: *[![:digit:]]*
Match a digit 0
anywhere in the argument:
case "$1" in
*0* ) do something here ... ;;
esac
Match digits 0
or 1
anywhere in the argument:
case "$1" in
*[01]* ) do something here ... ;;
esac
Match digits 0
through 9
anywhere in the argument using a GLOB range (only valid for digits; don’t use ranges for letters!):
case "$1" in
*[0-9]* ) do something here ... ;;
esac
Match digits 0
through 9
anywhere in the argument using a POSIX character class named [:digit:]
that replaces the range 0-9
:
case "$1" in
*[[:digit:]]* ) do something here ... ;;
esac
Match any character that is NOT a digit by inserting !
at the start of the character class:
case "$1" in
*[![:digit:]]* ) do something here ... ;;
esac
Other useful POSIX class names you can use inside GLOB character classes: [:digit:] [:alpha:] [:alnum:] [:space:]
For a list of POSIX character classes, see POSIX Character Classes.
Do not use letter ranges in character classes, e.g. [a-z]
! Internationalization and localization features will cause incorrect matches. Always use the POSIX class names for letter ranges inside character classes, e.g. [[:lower:]]
or [[:upper:]]
or [[:alpha:]]
while
, for
, do
, done
Reference: Chapter 9. Repetitive tasks http://tldp.org/LDP/Bash-Beginners-Guide/html/chap_09.html
Unix/Linux shells are designed to find and run commands. This means that the programming control structures of shells are based on the exit statuses of running commands.
A more complex shell control structure is a while loop that repeats running a list of commands over and over based on the success/failure of a command return code.
while
… do
… done
loop statementRecall the syntax of a simple if
branching statement:
if testing_command_list ; then
zero_command_list
fi
For example:
if who | fgrep "$1" ; then
echo "User '$1' is signed on"
fi
The shell while
loop is similar to a simple if
statement, except that the zero_command_list
is executed by the shell over and over as long as the testing_comamnd_list
succeeds. Instead of then
and fi
, the zero_command_list
of the while
loop is delimited by the new keywords do
and done
:
while testing_command_list ; do
zero_command_list
done
For example:
while who | fgrep "$1" ; do
echo "User '$1' is still signed on"
sleep 10
done
As with the if
statement, the testing_comamnd_list
is executed, and the return status of the last command in the list is tested, then:
zero_command_list
are executed, over and over.zero_command_list
is not executed any more and the shell continues after done
with the rest of the shell script, if any.&&
or ||
operators. If the testing_command_list
is multiple commands, only the return status of the last command is tested. Any list may include commands connected by pipelines.If the testing_command_list
never fails, the while
loop will repeat the zero_command_list
over and over, forever. To avoid the loop repeating forever, the testing_command_list
should eventually return a non-zero (failing) status.
Below are some examples of what these while
loop statements might look like inside shell scripts.
Example use of an integer expression to add one to a loop index variable to create 100 files with numbered names:
$!/bin/sh -u
# Create 100 files with names filename1.txt through filename100.txt
i=1
while [ $i -le 100 ] ; do
touch "filename$i.txt"
i=$(( i + 1 ))
done
Below is another example using a shell pipeline as the testing_comamnd_list
in the while
loop:
#!/bin/sh -u
# Loop while idallen is signed on, then exit.
while who | fgrep "idallen" ; do
echo "idallen is still online"
sleep 60
done
echo "idallen just signed off"
As you can see in the example above, the testing_comamnd_list
being executed can be a shell pipeline. Only the exit status of the last command in a pipeline is used by the shell. As long as the fgrep
command succeeds in finding the string idallen
in the output of the who
command (exit status zero), the loop will continue. When the fgrep
command does not find idallen
, it returns a non-zero exit status and the loop finishes and the message idallen just signed off
is echoed.
Below is a similar script the works the opposite to the one above. It uses the exclamation mark !
negation operator to negate the return code of the pipeline. It waits for idallen
to sign on, and loops waiting until he does. When he does sign on, the fgrep
command returns a success zero status that is inverted to failure by the exclamation mark !
and the script exits with the idallen just signed on
message.
#!/bin/sh -u
# Loop while idallen is NOT signed on, then exit.
while ! who | fgrep "idallen" ; do
echo "idallen is not online yet"
sleep 60
done
echo "idallen just signed on"
echo "Hello Ian" | write idallen
for
… do
… done
loop statementUnlike the while
loop that has to run a testing_comamnd_list
to know when to continue the loop, the for
loop does not execute any testing command. It simply iterates over a fixed list of words, one at a time. There are two kinds of for
loops, one with an implicit list of words (the command line arguments) and one with an explicit list of words:
# implicit list of words come from command line arguments
for name do # iterates over arguments $1 $2 $3 ...
command_list
done
# explicit list of words is supplied before semicolon
for name in word1 word2 word3 ; do # iterates over word1 word2 word3 ...
command_list
done
Using square brackets here to indicate optional text and ...
to indicate repeated text, as in the SYNOPSIS section of man
pages, we could write the for
syntax like this:
for name [ in word... ; ] do
command_list
done
name
is the name of a variable, called the index variable. The name is given here without a leading dollar sign!in
word...
;
is a list of words to iterate over, one at a time. The list must end with a semicolon.$1
, $2
, etc.) are used for the list.The variable $name
is set to the first word in the list and then the command_list
is executed. Then $name
is set to the second word and the command_list
is executed again. This repeats for each word in the list until the list is exhausted.
An example for
loop with a list of three words to iterate over:
#!/bin/sh -u
for i in dog cow pig ; do
echo "See the $i run!"
done
Running the above script:
$ ./example.sh
See the dog run!
See the cow run!
See the pig run!
Another example without an explicit list of words uses the positional parameters (command line arguments $1
, $2
, etc.) as the list of words:
#!/bin/sh -u
for j do
echo "See the $j run!"
done
Running the above script:
$ ./example.sh man nose
See the man run!
See the nose run!
The name of the index variable is arbitrary but should usually reflect the meaning of the items in the list of words:
#!/bin/sh -u
fruits=
for newfruit in apple pear plum cherry apricot banana ; do
fruits="$fruits $newfruit"
done
echo "So many:$fruits"
Running the above script:
$ ./example.sh
So many: apple pear plum cherry apricot banana
Often the shell script iterates over a list of file names on the command line:
#!/bin/sh -u
for file do
if [ ! -r "$file" ] ; then
echo "Cannot read '$file'"
fi
done
Running the above script:
$ ./example.sh /etc/*
Cannot read '/etc/group-'
Cannot read '/etc/gshadow'
Cannot read '/etc/gshadow-'
Cannot read '/etc/shadow'
Cannot read '/etc/shadow-'
Cannot read '/etc/sudoers'
Remember that the variable named at the start of a
for
loop does not start with a dollar sign! You only use the dollar sign to expand the variable inside the loop body.
break
and continue
You can modify control flow inside loops using break
and continue
.
You can break out of the middle of a loop using the break
statement:
#!/bin/sh
# $0 [ filenames... ]
# Create .bak copies of all the names.
# Does not overwrite existing non-empty .bak copies.
# Skips over missing or unreadable files.
# Script terminates on copy error; does not continue.
count=0
status=0
for name do
if [ ! -e "$name" ] ; then
echo "$0: '$name' is inaccessible or does not exist; skipped"
status=1
elif [ ! -f "$name" ] ; then
echo "$0: '$name' is not a file; skipped"
status=1
elif [ ! -r "$name" ] ; then
echo "$0: '$name' is not readable; skipped"
status=1
elif [ -s "$name.bak" ] ; then
echo "$0: '$name.bak' already exists; skipped"
status=1
elif cp -p "$name" "$name.bak" ; then
count=$(( count + 1 ))
echo "$count. Backed up to $name.bak"
else
echo 1>&2 "$0: Could not copy '$name' to '$name.bak'"
echo 1>&2 "$0: Script terminated"
status=1
break
fi
done
echo "Copied: $count"
exit "$status"
You can skip back to the top of a loop using the continue
statement. In the example below, we start a variable count
at zero and then loop over all the arguments counting how many are readable and adding one to the counter each time. When the loop finishes (all the arguments have been processed), we show the value of the counter.
#!/bin/sh
# $0 [ names... ]
count=0
for name do
if [ ! -e "$name" ] ; then
echo "$0: '$name' is inaccessible or does not exist"
continue # skip back to top of FOR loop
fi
if [ ! -f "$name" ] ; then
echo "$0: '$name' is not a file"
continue # skip back to top of FOR loop
fi
if [ ! -r "$name" ] ; then
echo "$0: '$name' is not readable"
continue # skip back to top of FOR loop
fi
count=$(( count + 1 )) # add one to the counter
echo "$count. A readable file: $name $(wc <"$name")"
done
echo "Number of readable files: $count"
shift
so $2
becomes $1
Often we want a script to process any number of command line arguments. This means we don’t know ahead of time how many arguments there will be. A nice way to do this is using the shell built-in shift
command inside a script.
We already know that the command line arguments to a script are assigned to the positional parameter variables $1
, $2
, $3
, etc.
The shell built-in shift
command behaves as if the shell throws away the first command line argument and then re-assigns all the positional parameters using the (fewer) arguments that are left. This has the effect that the argument that used to be in $2
is now in $1
and what used to be in $3
is now in $2
, etc. All the positional parameters have shifted down by one:
#!/bin/sh -u
echo "'$1' is the first argument of $#: $*"
shift
echo "'$1' is the first argument of $#: $*"
shift
echo "'$1' is the first argument of $#: $*"
shift
echo "'$1' is the first argument of $#: $*"
shift
echo "'$1' is the first argument of $#: $*"
Running the above script:
$ ./example.sh one two three four five six
'one' is the first argument of 6: one two three four five six
'two' is the first argument of 5: two three four five six
'three' is the first argument of 4: three four five six
'four' is the first argument of 3: four five six
'five' is the first argument of 2: five six
Every time shift
executes, the first command line argument disappears and all the other arguments move down one.
We can use this to process a huge number of command line arguments by coding a shell loop statement. Let’s look at a loop statement next.
This example loop uses the shell built-in shift
command to shift all the command line arguments over and over until no arguments are left (until $#
becomes zero):
#!/bin/sh -u
# Display all the command line arguments, no matter how many
while [ $# -gt 0 ] ; do
echo "'$1' is the first argument of $#: $*"
shift
done
echo "All the arguments have been processed."
The while
loop uses the test
helper command to test that the number of command line arguments is greater than zero. When the test fails (the number of arguments is zero), the loop terminates and the script continues on to print its final exit message.
Running the above script:
$ ./example.sh one two three four five six
'one' is the first argument of 6: one two three four five six
'two' is the first argument of 5: two three four five six
'three' is the first argument of 4: three four five six
'four' is the first argument of 3: four five six
'five' is the first argument of 2: five six
'six' is the first argument of 1: six
All the arguments have been processed.
Functions are named pieces of code that can be created inside the shell and passed parameters and executed by the shell just as if they were commands with arguments. A shell function operates just like a miniature shell script:
$ Foo () { echo "Hello $*" ; }
$ Foo a b c
Hello a b c
Like shell variables, shell functions are defined inside the shell itself and the definition is lost when the shell exits. (To have a function defined in all your interactive shells, put the definition in your .bashrc
file.)
No $PATH
lookup is needed to find and run a shell function, and functions are found before any $PATH
search is done. If a shell function has the same name as a system command, the function is used, not the system command.
The formal syntax definition shows that the keyword function
is optional when creating a function:
[function] name () {
list of commands
}
Any parameters you pass to a function when calling it by name on the command line it will become positional parameters ($1
, $2
, etc.) inside that function, just as if you were calling a shell script.
Functions are most often defined and used inside shell scripts to hold in one place some code that you want to use over and over in your shell script. Without the function, you would have to repeat the code over and over, making the script longer and harder to maintain.
Here is an example where we centralize most of an echo
statement and a sentence in a function and only pass in to the function the two parameters that change:
#!/bin/sh -u
Foo () {
echo "You are a $1 human being. I $2 you."
}
Foo nice adore
Foo melancholy "am indifferent to"
Foo terrible hate
Running the above script:
$ ./example.sh
You are a nice human being. I adore you.
You are a melancholy human being. I am indifferent to you.
You are a terrible human being. I hate you.
Using functions can make your shell scripts much smaller and easier to understand and maintain.
ErrorExit
function to exit a scriptThis example function below echoes all its arguments onto standard error, prints a usage message, and exits the script. Putting all this common code in the function makes the script shorter, easier to read, and easier to maintain.
#!/bin/sh -u
ErrorExit () {
echo 1>&2 "$0: $*"
echo 1>&2 "Usage $0 [filename]"
exit 1
}
if [ $# -ne 1 ]; then
ErrorExit "Expecting one filename argument; found $# ($*)"
fi
if [ "$1" = "" ]; then
ErrorExit "Argument is an empty string"
fi
if [ ! -f "$1" ]; then
ErrorExit "Argument is nonexistent, inaccessible, or not a file: '$1'"
fi
if [ ! -r "$1" ]; then
ErrorExit "File is not readable: '$1'"
fi
if [ ! -s "$1" ]; then
ErrorExit "File is empty: '$1'"
fi
if cp -p "$1" "$1.bak" ; then
echo "Backed up '$1' to '$1.bak'."
else
echo "Failed to back up '$1' to '$1.bak'."
fi
Running the above script:
$ ./example.sh
./example.sh: Expecting one filename argument; found 0 ()
Usage ./example.sh [filename]
$ ./example.sh /bin
./example.sh: Argument is nonexistent, inaccessible, or not a file: '/bin'
Usage ./example.sh [filename]
Functions can hold code in one place that would otherwise be repeated over and over in a script. Less code is better code.
&&
and ||
The if
statement isn’t the only way that the shell checks the return status of a command. If you separate two commands with ||
or &&
the shell checks the return status of the first command to decide whether to run the second:
# execute command_2 only if comamnd_1 fails (returns non-zero)
#
command_1 || command_2
# execute command_2 only if comamnd_1 succeeds (returns zero)
#
command_1 && command_2
These Boolean command operators are sometimes used inside scripts to avoid having to write an entire if
statement to check a command return status:
mkdir foo || exit $? # exit script if mkdir fails
[ -d bar ] || exit 1 # exit script if not a directory
[ -s foo ] && mv foo foo.bak # back up file only if not empty
read
The Bourne family of shells use a built-in command named read
to issue a prompt on standard error and read one single line of input from standard input. The input line is split up into words and assigned to one or more variables given on the command line:
#!/bin/sh -u
read -p "Enter a number: " num1
read -p "Enter a number: " num2
echo "The sum is $(( num1 + num2 ))"
Output:
$ ./example.sh
Enter a number: 10
Enter a number: 5
The sum is 15
The shell only issues the prompt if standard input is coming from a terminal (from a human via a keyboard). If input is coming from a file or a pipe, no prompt is needed:
$ echo 10 >input
$ echo 5 >>input
$ ./example.sh <input
The sum is 15
If only one variable name is given, the whole input line is put into the variable. Multiple variables cause word splitting of the input line into the separate variables:
#!/bin/sh -u
read -p "Enter two numbers: " num1 num2
echo "The sum is $(( num1 + num2 ))"
Output:
$ ./example.sh
Enter two numbers: 10 5
The sum is 15
Because the read
command is built-in to the shell, it is documented in the manual page for your shell, e.g. man bash
. BASH users may also use help read
to get a summary of the command usage.
echo 1>&2
Shell scripts often need to produce their own error messages. Error messages should always be sent to standard error so that they appear on your screen and don’t get redirected into pipes and output files.
We use the shell redirection trick 1>&2
to re-route echo
output normally headed for unit 1 (standard output) to actually appear on unit 2 (standard error):
echo 1>&2 "$0: Expecting 1 file name; found $# ($*)"
There are many good properties of the above error message. It gives the user all the information they may need to see what is wrong:
1>&2
$0
is the name used to invoke the script (remember, files can have more than one name so script names shouldn’t be hard-coded into the script)$#
is the number of arguments the user actually passed$*
shows the actual arguments given to the script, put in parentheses so the user can see spaces, etc.Error messages must obey these four rules. Error messages must:
1>&2
$0
$#
and $*
are often useful, e.g. “found 3 (a b c)”Never say just “illegal input” or “invalid input” or “too many”. Always specify how many is “too many” or “too few”:
echo 1>&2 "$0: Expecting 3 file names; found $# ($*)"
echo 1>&2 "$0: Student age $student_age is not between" \
"$min_age and $max_age"
echo 1>&2 "$0: Modify days $moddays is less than zero"
After detecting an error, the usual thing to do is to exit the script with a non-zero return code. Don’t keep processing bad data!
Good Error Messages must:
1>&2
$0