|
Softpanorama |
May the source be with you, but remember the KISS principle ;-)
|
| Pipes in Loops | Examples | Advanced navigation | |||
| Arithmetic expressions | Comparison operators | BASH Debugging | Shell history | Etc |
The simplest type of flow control construct is the conditional, embodied in the BASH's if statement.
You use a conditional when you want to choose whether or not to do something, or to choose among a small number of things to do, according to the truth or falsehood of conditions. Conditions test values of shell variables, characteristics of files, whether or not commands run successfully, and other factors. The shell has a large set of built-in tests that are relevant to the task of shell programming.
The if construct has the following syntax:
if condition
then
statements
[elif condition
then statements...]
[else
statements]
fi
There are five forms of an IF statement in shell
The simplest form (without the elif and else parts, a.k.a. clauses) executes the statements only if the condition is true. For example:
if cd /fake; then echo "cd returned OK"; fi
If you add an else clause, you get the ability to execute one set of statements if a condition is true or another set of statements if the condition is false. You can use as many elif (a contraction of "else if") clauses as you wish; they introduce more conditions, and thus more choices for which set of statements to execute. If you use one or more elifs, you can think of the else clause as the "if all else fails" part.
Every UNIX command returns an integer code to its parent. This return code is called the exit status. 0 is usually the "OK" exit status, while anything else (1 to 255) usually denotes an error.
The simplest form of if statement checks the exit status of the last statement in the list following the if keyword (The list is usually just a single statement.) If the status is 0, the condition evaluates to true; if it is anything else, the condition is considered false. The same is true for each condition attached to an elif statement (if any).
This enables us to write code of the form:
if command ran successfully
then
normal processing
else
error processing
fi
For example:
if cd /fake; then echo "cd returned OK"; fi
Historically conditional expressions in Unix shells were introduced via test command. For instance, test can check whether a file is writable before your script tries to write to it. It can treat the string in a shell variable as a number and do comparisons ("Is that number less than 1000?"). You can combine tests, too ("If the file exists and it's readable and the message number is more than 500..."). Some versions of test have more tests than others.
The test command returns a zero status if the test was true and a nonzero status otherwise, so people usually use test with if , while, or until. Here's a way your program could check to see if the user has a readable file named .profile in the home directory:
if test -r $HOME/.profile
then
echo "$myname: You already have .profile file and its readable"
else
echo " you do not have .profile file. Copying ..."
cp /etc/skel/.profile $HOME/.profile
exit 1
fi
The test command also lets you test for something that isn't true. Add an exclamation point (!) before the condition you're testing. For example, the following test is true if the .profile file is not readable:
if test ! -r $HOME/.profile
... ... ...
The hack that was implemented is to link text to the file named [. Yes, that's a left bracket. It was a pretty interesting hack: you can use it interchangeably with the test command with one exception: there has to be a matching right bracket (]) at the end of the test. The second example above could be rewritten this way:
if [ ! -r $HOME/.profile ]
then
echo "$myname: Can't read your '.profile'. You need to create one and make it readable." 1>&2
exit 1
fi
Be sure to leave space between the brackets and other text. There are a couple of other common gotchas caused by empty arguments because shell attempts macro expansion before syntax analysis.
BASH allows you to combine exit statuses logically, so that you can test more than one thing at a time:
In both cases it's useful to think about them "short-circuit and" and "short-circuit or," respectively:
$ echo $(( 0 && 0 ))
0
$ echo $(( 1 && 0 ))
$ echo $(( 0 && 1 ))
$ echo $(( 1 && 1 ))
if statement1 && statement2
then
...
fi
statement1 is always executed.
But only if the first statement returns a return code 0 statement2 runs. The then clause will be executed only if statement1 and statement2 both succeeded.
In case of short-circuit or the situation is similar:
if statement1 || statement2
then
...
fi
statement1 is always executed. If it returns code zero (success), then statement2 will not be executed. Otherwise statement2 will be executed and its return code will be used for the deciding whether to execute then clause of the if statement or else clause. In other words then clause runs either statement1 or statement2 returns a zero code.
$ true && echo "Yes."
$ true || echo "No."
No
Note: These constructs can be used outside if statement as well.
Let's assume that we need to write a script that checks a /etc/passwd file for the presence of two users who left the company. We can use grep for this: it returns exit status 0 if it found the given string in its input, non-0 if not:
user=$1
if grep $user /etc/passwd || -e $home/$user
then
print "user $user is not fully removed from the server."
fi
Later we can generalize this on a arbitrary number of users using for loop.
The [[ ]] construct was introduced in ksh88 as a way to compensate for some shortcomings of the [ ] solution. Essentially it makes [ ] construct redundant except for running a program to get a return code.
The [[ ]] construct expects expression. What is important delimiters [[ and ]] serve as single quotes so you do not have macro expansion inside: variable substitution and wildcard expansion aren't done within [[ and ]], making quoting less necessary.
The main problem with the [[ ]] construct is that it stupidly redefined === as a parttern matching operation.
Like [ ] construct [[ ]] construct can be used as a separate statement that returns an exit status depending upon whether condition is true or not. With && and || constructs discussed above this provides an alternative syntax for if-then and if-else constructs
if [[ -e $HOME/$user ]] ; then echo " Home for user $user exists..."; fi
can be written simpler as
[[ -e $HOME/$user ]] && echo " Home for user $user exists...";
There are several types of expressions that can be used inside [[ ... ]] construct:
String comparisons
Some of them are listed below:
| Operator | True if... |
|---|---|
str = pat[5] str == pat[5] |
str matches pat. Note that that's not what you expect !!! |
| str != pat | str does not match pat. |
| str1 < str2 | str1 is less than str2 is collation order used |
| str1 > str2 | str1 is greater than str2. |
| -n str | str is not null (has length greater than 0). |
| -z str | str is null (has length 0). |
| file1 -ef file2 | file1 is another name for file2 (hard or symbolic link) |
While we're cleaning up code we wrote in the last chapter, let's fix up the error handling in the highest script The code for that script is:
filename=${1:?"filename missing."}
howmany=${2:-10}
sort -nr $filename | head -$howmany
Recall that if you omit the first argument (the filename), the shell prints the message highest: 1: filename missing. We can make this better by substituting a more standard "usage" message:
if [[ -z $1 ]]; then
print 'usage: howmany filename [-N]'
else
filename=$1
howmany=${2:-10}
sort -nr $filename | head -$howmany
fi
It is considered better programming style to enclose all of the code in the if-then-else, but such code can get confusing if you are writing a long script in which you need to check for errors and bail out at several points along the way. Therefore, a more usual style for shell programming is this:
if [[ -z $1 ]]; then
print 'usage: howmany filename [-N]'
return 1
fi
filename=$1
howmany=${2:-10}
sort -nr $filename | head -$howmany
The shell also provides a set of arithmetic tests. These are different from character string comparisons like < and >, which compare lexicographic values of strings, not numeric values. For example, "6" is greater than "57" lexicographically, just as "p" is greater than "ox," but of course the opposite is true when they're compared as integers.
Generally numeric comparisons in double square brackets are obsolete. You should use (( ... )) construct.
| Test | Comparison |
|---|---|
| -lt | Less than |
| -le | Less than or equal |
| -eq | Equal |
| -ge | Greater than or equal |
| -gt | Greater than |
| -ne | Not equal |
You'll find these to be of the most use in the context of the integer variables we'll see in the next chapter. They're necessary if you want to combine integer tests with other types of tests within the same conditional expression.
However, the shell has a separate syntax for conditional expressions that involve integers only. It's considerably more efficient, so you should use it in preference to the arithmetic test operators listed above. Again, we'll cover the shell's integer conditionals in the next chapter.
The other kind of operator that can be used in conditional expressions checks a file for certain properties. There are approximatly two dozens of such operators. Most common are listed below; the rest refer to arcana like sticky bits, sockets, and file descriptors, and thus are of interest only to systems programmers and/or hackers.
| Operator | True if... |
|---|---|
| -a file | file exists |
| -d file | file is a directory |
| -f file | file is a regular file (i.e., not a directory or other special type of file) |
| -r file | You have read permission on file |
| -s file | file exists and is not empty |
| -w file | You have write permission on file |
| -x file | You have execute permission on file, or directory search permission if it is a directory |
| -O file | You own file |
| -G file | Your group ID is the same as that of file |
| file1 -nt file2 | file1 is newer than file2 |
| file1 -ot file2 | file1 is older than file2 |
Before we get to an example, you should know that conditional expressions inside [[ and ]] can also be combined using the logical operators && and ||, just as we saw with plain shell commands above, in the section entitled "Combinations of Exit Statuses."
It's also possible to combine shell commands with conditional expressions using logical operators, like this:
ifcommand&& [[condition]]; then ...
You can also negate the truth value of a conditional expression by preceding it with an exclamation point (!), so that ! expr evaluates to true only if expr is false. Furthermore, you can make complex logical expressions of conditional operators by grouping them with parentheses. construct (statement list) runs the statement list in a subshell, whose exit status is that of the last statement in the list, It can also be used outside if statement.
Write a script that prints essentially the same information as ls -l but in a more user-friendly way.
Although this task requires relatively long-winded code, it is a straightforward application of many of the file operators:
if [[ ! -a $1 ]]; then
print "file $1 does not exist."
return 1
fi
if [[ -d $1 ]]; then
print -n "$1 is a directory that you may "
if [[ ! -x $1 ]]; then
print -n "not "
fi
print "search."
elif [[ -f $1 ]]; then
print "$1 is a regular file."
else
print "$1 is a special type of file."
fi
if [[ -O $1 ]]; then
print 'you own the file.'
else
print 'you do not own the file.'
fi
if [[ -r $1 ]]; then
print 'you have read permission on the file.'
fi
if [[ -w $1 ]]; then
print 'you have write permission on the file.'
fi
if [[ -x $1 && ! -d $1 ]]; then
print 'you have execute permission on the file.'
fi
We'll call this script fileinfo. Here's how it works:
As an example of fileinfo's output, assume that you do an ls -l of your current directory and it contains these lines:
-rwxr-xr-x 1 billr other 594 May 28 09:49 bob -rw-r-r- 1 billr other 42715 Apr 21 23:39 custom.tbl drwxr-xr-x 2 billr other 64 Jan 12 13:42 exp -r-r-r- 1 root other 557 Mar 28 12:41 lpst
custom.tbl and lpst are regular text files, exp is a directory, and bob is a shell script. Typing fileinfo /etc/passwd produces this output:
function mess
{
if (( "$1" > 0 )) ; then>
total=$1
else
total=100
fi
tail -$total /var/adm/messages | more
}
The if/then construct tests whether a condition is true, and if so, executes one or more commands. Note that in this context, 0 (zero) will evaluate as true, as will a random string of alphanumeric. Puzzling out the logic of this is left as an exercise for the reader.
Exercise. Explain the behavior of Example 3-9, above.if grep "can't find" <(nslookup sandbox.rockaway.basf-corp.com) ; then echo DNS does not exists ; fi
Example 3-9. What is truth?
1 #!/bin/bash 2 3 if [ 0 ] 4 #zero 5 then 6 echo "0 is true." 7 else 8 echo "0 is false." 9 fi 10 11 if [ ] 12 #NULL (empty condition) 13 then 14 echo "NULL is true." 15 else 16 echo "NULL is false." 17 fi 18 19 if [ xyz ] 20 #string 21 then 22 echo "Random string is true." 23 else 24 echo "Random string is false." 25 fi 26 27 if [ $xyz ] 28 #string 29 then 30 echo "Undeclared variable is true." 31 else 32 echo "Undeclared variable is false." 33 fi 34 35 exit 0
1 if [ condition-true ] 2 then 3 command 1 4 command 2 5 ... 6 else 7 # Optional (may be left out if not needed). 8 # Adds default code block executing if original condition tests false. 9 command 3 10 command 4 11 ... 12 fi |
Add a semicolon when 'if' and 'then' are on same line.
1 if [ -x filename ]; then |
1 if [ condition ] 2 then 3 command 4 command 5 command 6 elif 7 # Same as else if 8 then 9 command 10 command 11 else 12 default-command 13 fi |
The test condition-true construct is the exact equivalent of if [condition-true ]. The left bracket [ is, in fact, an alias for test. (The closing right bracket ] in a test should not therefore be strictly necessary, however newer versions of bash detect it as a syntax error and complain.)
Example 3-10. Equivalence of [ ] and test
1 #!/bin/bash 2 3 echo 4 5 6 if test -z $1 7 then 8 echo "No command-line arguments." 9 else 10 echo "First command-line argument is $1." 11 fi 12 13 # Both code blocks are functionally identical. 14 15 if [ -z $1 ] 16 # if [ -z $1 17 # also works, but outputs an error message. 18 then 19 echo "No command-line arguments." 20 else 21 echo "First command-line argument is $1." 22 fi 23 24 25 echo 26 27 exit 0 |
Returns true if...
This usually refers to stdin, stdout, and stderr (file descriptors 0 - 2).
Example 3-11. Tests, command chaining, redirection
1 #!/bin/bash 2 3 # This line is a comment. 4 5 filename=sys.log 6 7 if [ ! -f $filename ] 8 then 9 touch $filename; echo "Creating file." 10 else 11 cat /dev/null > $filename; echo "Cleaning out file." 12 fi 13 14 # Of course, /var/log/messages must have 15 # world read permission (644) for this to work. 16 tail /var/log/messages > $filename 17 echo "$filename contains tail end of system log." 18 19 exit 0 |
integer comparison
string comparison
Note that the "<" needs to be escaped.
Note that the ">" needs to be escaped.
See Example 3-91 for an application of this comparison operator.
![]() |
This test requires that the string be quoted within the test brackets. You may use ! -z instead, or even just the string itself, without a test operator (see Example 3-13). |
Example 3-12. arithmetic and string comparisons
1 #!/bin/bash 2 3 a=4 4 b=5 5 6 # Here a and b can be treated either as integers or strings. 7 # There is some blurring between the arithmetic and integer comparisons. 8 # Be careful. 9 10 if [ $a -ne $b ] 11 then 12 echo "$a is not equal to $b" 13 echo "(arithmetic comparison)" 14 fi 15 16 echo 17 18 if [ $a != $b ] 19 then 20 echo "$a is not equal to $b." 21 echo "(string comparison)" 22 fi 23 24 echo 25 26 exit 0 |
Example 3-13. testing whether a string is null
1 #!/bin/bash 2 3 # If a string has not been initialized, it has no defined value. 4 # This state is called "null" (not the same as zero). 5 6 7 if [ -n $string1 ] # $string1 has not been declared or initialized. 8 then 9 echo "String \"string1\" is not null." 10 else 11 echo "String \"string1\" is null." 12 fi 13 # Wrong result. 14 # Shows $string1 as not null, although it was not initialized. 15 16 echo 17 18 # Lets try it again. 19 20 if [ -n "$string1" ] # This time, $string1 is quoted. 21 then 22 echo "String \"string1\" is not null." 23 else 24 echo "String \"string1\" is null." 25 fi 26 27 echo 28 29 if [ $string1 ] # This time, $string1 stands naked. 30 then 31 echo "String \"string1\" is not null." 32 else 33 echo "String \"string1\" is null." 34 fi 35 # This works fine. 36 # The [ ] test operator alone detects whether the string is null. 37 38 echo 39 40 string1=initialized 41 42 if [ $string1 ] # This time, $string1 stands naked. 43 then 44 echo "String \"string1\" is not null." 45 else 46 echo "String \"string1\" is null." 47 fi 48 # Again, gives correct result. 49 50 51 exit 0 52 53 # Thanks to Florian Wisser for pointing this out. |
Example 3-14. zmost
1 #!/bin/bash
2
3 #View gzipped files with 'most'
4
5 NOARGS=1
6
7 if [ $# = 0 ]
8 # same effect as: if [ -z $1 ]
9 then
10 echo "Usage: `basename $0` filename" >&2
11 # Error message to stderr.
12 exit $NOARGS
13 # Returns 1 as exit status of script
14 # (error code)
15 fi
16
17 filename=$1
18
19 if [ ! -f $filename ]
20 then
21 echo "File $filename not found!" >&2
22 # Error message to stderr.
23 exit 2
24 fi
25
26 if [ ${filename##*.} != "gz" ]
27 # Using bracket in variable substitution.
28 then
29 echo "File $1 is not a gzipped file!"
30 exit 3
31 fi
32
33 zcat $1 | most
34
35 exit 0
36
37 # Uses the file viewer 'most'
38 # (similar to 'less')
|
compound comparison
exp1 -a exp2 returns true if both exp1 and exp2 are true.
exp1 -o exp2 returns true if either exp1 or exp2 are true.
These are simpler forms of the comparison operators && and ||, which require brackets to separate the target expressions.
Refer to Example 3-15 to see compound comparison operators in action.
Work the Shell - Conditional Statements and Flow Control
Conditional Expressions- Korn Shell Reference
The Answer Guy 32- Conditional Execution Based on Host Availability
Chapter 7. Conditional statements