Evaluating
Conditional Expressions with Functions
Ed Schaefer
When I was a less experienced shell programmer, I didn't
think about my Korn shell function's return value. My functions
tended to look like these two examples:
# Using ksh pattern-matching operators, this function
# replaces the unix basename command. If a 2nd argument
# exists, strip off the extension.
function basename {
typeset v x
v=${1##*/}
x=${2#.} # get rid of the '.'
v=${v%.$x}
echo $v
}
# Using ksh pattern-matching operators, this function
# replaces the unix dirname command.
function dirname {
typeset v="$@"
echo ${v%/*}
}
# basename and dirname function examples
var=/usr/eds/ddir/ttest.c
bn=$(basename $var)
echo $bn # displays: ttest.c
bn=$(basename $var .c)
echo $bn # displays: ttest
dn=$(dirname $var)
echo $dn # displays: /usr/eds/ddir
The above functions serve a purpose -- pass a parameter, perform
a task, and return a result. This is fine, but this function style
doesn't take advantage of the shell's power, namely evaluating
shell conditional expressions. To demonstrate, the following hackneyed
newwho function calls the Unix who command:
function newwho
{
who -uTH
}
The function implicitly returns success, provided the who command
executes properly and returns failure if it doesn't. This command
uses this implicit return value to deliver more information to the
user:
if newwho
then
printf "The above users are logged in\n"
fi
The "The above users..." blurb appears at the end of the
list because newwho successfully executes. Also, the appearance
of this final blurb verifies that the command ran to completion, and
didn't terminate at some intermediate point in the list.
In this article, I discuss other techniques for employing the
power of exit codes. Furthermore, I explain implementing shell conditional
expressions by using the test command or brackets. Simulating if-then
and if-then-else statements with boolean expressions is detailed.
Finally, I conclude by presenting some realistic commands and functions
that use conditional expressions.
Using the Test Utility vs. Using Brackets Around Expressions
Unix programs terminate with the exit code, $?, set from
0 to 255. Zero (true) indicates success, and any other value indicates
a failure. In the same way, conditional expressions, using the test
command or the brackets, evaluate to true or false.
There's no difference between using test and using brackets.
The following code fragments are equivalent:
-> if test -f "myfile"
then
...
fi
-> if [ -f "myfile" ]
then
...
fi
In the early Unix days, a program called /bin/[ enabled the bracket
syntax emulating the same functionality as the test utility. With
either case, most modern shells implement both test and [
as built-in commands, so there is no performance advantage to using
one over the other. However, it's best to use double brackets,
[[..]], instead of [..] when evaluating conditional expressions. Why?
I quote The New KornShell Command and Programming Language
(Bolsky and Korn, Prentice Hall):
Since words inside [[..]] do not undergo field splitting and pathname
expansion, many of the errors associated with test and [ are eliminated.
The authors further state that && and ||
are used for logical operators, and unquoted parentheses are used
for grouping. This example is from The New KornShell:
[[ ! (-w file || -x file) ]]
is equivalent to:
test ! \( -w file -o -x file \)
Simple Bracket Tests
The man page for "test" lists a large number of options.
For example, the following checks whether "/tmp" is a
directory:
if [[ -d "/tmp" ]]
then
echo "/tmp is a directory."
else
echo "/tmp is NOT a directory."
fi
or if you prefer to use test:
if test -d "/tmp"
then
echo "/tmp is a directory."
else
echo "/tmp is NOT a directory."
fi
Negation Tests
Sometimes you want to know whether something is not true. The
shell uses the exclamation point (called a "bang") to
indicate a negation condition. Using negation, the previously example
is:
if [[ ! -d "/tmp" ]]
then
echo "/tmp is NOT a directory."
else
echo "/tmp is a directory."
fi
Using Shell Conditional Expressions and Short-circuit Evaluation
Similar to C and Java, the shell evaluates combined conditional
expressions only to the point that the shell determines the value
of the expression. The mathematical definition for this is short-circuit
evaluation. The && ("and" operator) and
|| ("or" operator) are both short-circuit evaluators.
The "and" evaluation stops when the first value is false,
and the "or" stops when the first value is true.
The New KornShell expresses it another way:
|| Runs the command following the || only if the command preceding
the || returns False.
&& Runs the command following the && only if the
command preceding the && returns True.
Short-circuit Evaluation Examples
In the following nonsensical examples:
x=0
(( 0 == 1 )) && { echo This will never display; x=$((x+1)); }
(( 0 == 0 )) || { echo Also, This will never display; x=$((x+1)); }
the second part of the expression never executes. The first test stops
because the expression preceding the "and", (( 0 == 1
)), always returns false. The second test stops because the expression
preceding the "or", (( 0 == 0 )), always returns
true. These examples illustrate a side effect when evaluating conditional
expressions. Variable "x" remains zero because the short-circuit
evaluation guarantees the second part of the expression is ignored.
Remember that the last expression evaluated in a function is its
implicit return value. To demonstrate the "or" condition,
I define a shell function called LE2 for "less than or equal
2":
function LE2 {
(( $1 < 2 )) || (( $1 == 2 ))
}
Executing "LE2 1" returns exit code true (0), and executing
"LE2 5" returns exit code false (1). This example is obviously
contrived to illustrate a point. This is a more realistic example:
function LE2 {
(( $1 <= 2 ))
}
To clarify an "and" example, I use shell function INSEQ
to check whether three numbers are in sequence (first less than second,
and second less than third):
function INSEQ {
(( $1 < $2 )) && (( $2 < $3 ))
}
Executing INSEQ 1 2 3 returns exit code true (0), and executing
INSEQ 2 2 3 returns exit code false (1).
Using && and || to Simulate if-then-else Statements
The short-circuit property of these operators also allows the
creation of compact if-then and if-then-else statements.
The following if statement:
if (( $x < $y ))
then
echo "X is less than Y"
fi
is more succinctly stated as:
(( $x < $y )) && echo "X is still less than Y"
and an if statement with an else:
if test $space -gt $size
then
echo "Your file will fit here."
else
echo "Try backing up to /dev/null. Your file will fit there.";
fi
can be rewritten as:
(( $space > $size )) && echo "Your file will fit here."
|| echo "The file is too large. Your file won't fit here."
A "real world" if example is terminating a script
if there is not 1 command-line argument:
if (( $# != 1 ))
then
echo "requires 1 command-line argument. Terminating!"
exit 1
fi
You can emulate the same functionality by grouping shell commands
with braces:
# white space around the braces and the ";" before the closing
# brace is required.
(( $# != 1 )) &&
{ echo "requires 1 command-line argument. Terminating!"; exit 1; }
Function Examples
The isatty, Test for a Terminal Function
There's no sense executing commands relating to terminal
control if the script is not connected to a terminal device. The
test -t option returns true if the associated file descriptor
is associated with a terminal. The following function emulates C
library function isatty test for a terminal device:
# Implicitly return true if open file descriptor $1 is
# connected to a terminal
function isatty {
[[ -t $1 ]]
}
The following stub code doesn't allow querying the user for input
if file descriptor 0 isn't standard input:
if isatty 0
then
echo "input variable x: "
read x
fi
The is_leap_year, Leap Year Function
The following leap year check example also implicitly checks the
exit code. The four-digit year is a leap year if it is divisible
by 4, unless it is also divisible by 100 (2100, 2200) except if
it a year also divisible by 400, which is a leap year (1600, 2000).
As long as the arithmetic evaluations don't fail, the exit
code is always implicitly true. If the evaluation does fail, the
code is false:
function is_leap_year {
(($1 % 4 == 0)) && (($1 % 100 != 0)) || (($1 % 400 == 0))
}
# test: "2000 is a leap year"
dyear=2000
is_leap_year $dyear && echo "$dyear is a leap year" ||
echo "$dyear is NOT a leap year"
We can rewrite is_leap_year combining the expressions:
function is_leap_year {
(($1 % 4 && $1 % 100 != 0)) || (($1 % 400 == 0))
}
The streq, String Equal Function
In addition to arithmetic evaluations setting the exit code, a
string comparison evaluation also sets the exit code, as follows:
function streq { [[ $1 == $2 ]]; }
# test "NOT equals"
str="xtext string"
streq "$str" "text string" && echo "equals" || echo "NOT equals"
The echo_n, No Echo Function
An irritating difference between the GNU echo, (i.e., Linux)
and other Unix echo versions, is how the trailing newline
is suppressed. Under traditional Unix, echo \c eliminates
the newline, while GNU echo -n performs the same function.
Defining an echo_n function based on how the shell executes
echo -n hides the echo usage syntax:
[[ "$(echo -n)" == "-n" ]]
&& function echo_n { echo "$@\c"; } || function echo_n { echo \
-n "$@"; }
The echo_n function definition is based on whether the echo
-n command echos -n (Unix) or just a null (GNU). Using
the echo command seems to be part of the Unix consciousness,
but if script portability between Linux and Unix is an issue, consider
using the printf command in place of echo or echo_n.
The lss, Listing Function
With the ls command and help from the Korn shell string
operator ${varname:+word}, the lss function returns
null if an object doesn't exist:
function lls { ls -1 ${1:+"$@"} 2>/dev/null; }
Consider the string operator description from Learning the Korn
Shell (Rosenblatt, O'Reilly & Associates):
${varname:+word} If varname exists and isn't null, return word;
otherwise return null.
To illustrate using the lss function, in the following function,
trap the output of lss $1 using command substitution. If the
negation -z check signals that the return value is not null,
the implicit exit code is true and the object exists:
function does_obj_exist { [[ ! -z "$(lls $1)" ]] ; }
# test if file dc.ss exists
s=dc.ss
does_obj_exist "$s" && echo "$s exists" || echo "$s does NOT exist"
Since the "test -f" unary file expression performs
this same operation, the does_obj_exist function is superfluous.
A more useful function is a directory empty check:
function is_dir_empty { [[ ! -d $1 || -z "$(lls -A $1)" ]] ;}
# test if the directory is not empty,
# change permissions on all objects in the directory
obj=/home/eds/tmps
is_dir_empty $obj && echo $obj is empty || chmod 666 $obj/*
If object is not a directory or the results of the lss function
are null, the object is empty.
By default, when using ls, I prefer one object per line
using the -1 option, but note that I can override this. The -A
option includes all hidden objects that start with a dot (.), except
the working directory (.) and the parent directory (..). As previously
stated, if the test command or single brackets are used, change
the boolean "or" identifier. The following is_dir_empty
rendition uses the single bracket syntax:
function is_dir_empty { [ ! -d $1 -o -z "$(lls -A $1)" ]; }
The askyorn Function
Most interactive shell programs require yes or no input from the
user. Instead of returning a Y or an N, the askyorn function
returns a true or false exit code for yes or no, respectively:
# this function down shifts string case to lower
function down_shift {
echo $@ | tr '[A-Z]' '[a-z]'
}
function askyorn {
local d=
local lower=
local ques="y/n"
local reply=
lower=$(down_shift $1)
if streq "$lower" "-y" || streq "$lower" "-n"
then
d=${lower#-}; shift
case $d in y)ques="Y/n";; n)ques="y/N";; esac
fi
while echo_n "$@" "($ques)? "
do
read reply
case "${reply:=$d}" in [yYnN]*)break;; esac
done
case "$reply" in [yY]*)return 0;; *)return 1;; esac
}
# test
askyorn -Y Do you want to make more changes && echo Pressed Yes
|| echo Pressed No
The askyorn function is similar to the Solaris internal ckyorn
command. Call askyorn with an optional "-y for yes"
or "-n for no" question (upper or lower case), and a prompt
string. If the option exists, the function downshifts it and removes
the "-" with Korn shell pattern ${lower#-}. Executing
shift removes the option from the rest of the command line. The question
is set to "y/n" with the default option upshifted. For example,
if -Y or -y is the default, the question equals "Y/n".
Using the echo_n function displays the prompt and the question
within the while loop. The user may answer Y or N, regardless of
case. Pressing RETURN sets the user reply to the default if one
was passed. If no default, the user must press [Yy] or [Nn] to terminate
the loop. Finally, the exit code returns true for [Yy] and false
for [Nn].
Conclusion
Do you really need to write functions that only use the exit code
as a return value? Certainly not, but I find this function and corresponding
test more aesthetically pleasing:
function mySillyFunction1 {
return 0
}
# test
if mySillyFunction1
then
echo "mySillyFunction1 is always true"
fi
than this function and test:
function mySillyFunction2 {
echo 1
}
# test
if (( $(mySillyFunction2) == 1 ))
then
echo "mySillyFunction2 always returns a value of 1"
fi
Also, if your maintenance and sustaining career is long, you may have
to edit conditional expressions and change functions that don't
explicitly return a value, so it's best to know how.
References
Tutorial Notes on Logic and Short Circuit Evaluations, by Dr.
Andrew Kwok Fai Lui, Open University of Hong Kong: http://learn.ouhk.edu.hk/~mt258/mt258/selfstudy/document/ShortCircuitEvaluation.pdf
Bolsky, Morris I., David G. Korn. The New Kornshell Command
and Programming Language, 1995. Upper Saddle River, NJ: Prentice
Hall PTR.
Rosenblatt, Bill. Learning the Korn Shell, 1993. Sebastopol,
CA: O'Reilly & Associates, Inc.
Ed Schaefer is a frequent contributor to Sys Admin.
He is a software developer and DBA for Intel's Factory Integrated
Information Systems, FIIS, in Aloha, Oregon. Ed also edits the UnixReview.com
monthly Shell Corner column. He can be contacted at: shellcorner@comcast.net.
|