Cover V13, i03

Article

mar2004.tar

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.