Creating
Black Box Functions in the Korn and Bash Shells
Ed Schaefer
The article "Creating Global Functions with the Korn Shell"
by Rainer Raab (Sys Admin, March 2001, http://www.sysadminmag.com/documents/sam0103f/)
describes using and autoloading global Korn shell functions. Although
global functions are useful and definitely serve a purpose, good
software design dictates using loosely coupled, "black box",
user-defined functions.
Loosely coupled functions are defined as having flexible relationships
to other objects depending little, if at all, on other functions.
My design philosophy is to limit the use of globals. Steven McConnell,
in his book Code Complete, states that using one global in
a function as being acceptable, but accessing ten globals is pathological.
User-defined functions are "black box" if they do one
thing and only one thing and limit accessing globals. Also, if the
function does that one thing well, a programmer should only have
to know what the function does, not how it does it.
Building on Rainer's work, in this article I present function
examples using local variables, passing parameters to functions,
returning values from functions, and setting the exit code from
functions. The function examples presented are:
- yesorno -- Identifies user input parameter as 1 or 0.
- user_exist -- Identifies whether user id parameter is assigned
to the OS.
- down_shift -- Down shifts the input parameter to lower
case.
- new_down_shift -- Down shifts the input parameter to lower
case, but handles positional parameters containing white space.
- is_digit -- Determines whether the input parameter is a
number.
- is_regex_set -- Matches an input parameter and regular
expression parameter.
- readkey -- Returns a single keystroke eliminating the carriage
return.
- recursion_test -- Tests at what level recursion fails in
the shell.
Also, I discuss dealing with positional parameters containing
white space and conclude by commenting on function recursion in
the shell.
The Development Environment
The functions in this article run under the Korn shell with SCO
Open Server V, the Korn shell with Solaris 7 on Intel Architecture,
and Red Hat 7.1 Bash shell on Intel Architecture.
The yesorno Function Example
In an interactive script requiring yes or no answers, do you want
a consistent method for processing user input? The following function,
yesorno, satisfies this requirement and demonstrates a simple black
box function:
# yesorno: This function takes one argument and if the
# value is Y or y returns 1 else it returns 0. Pressing
# <CR> returns 0;
# usage: $(yesorno $answer)
function yesorno {
case $1 in
y|Y) echo 1;;
*)
echo 0;;
esac
} # end yesorno
Think of a user-defined function as a mini shell script with the function
parameters emulating the shell positional parameters. Thus, function
parameter 1 is $1, function parameter 2 is $2, etc. Also, special
characters $#, argument count, and $@, positional parameter list,
are set for the function. More on the special characters later.
The echo command in the function serves to return the value.
Instead of being sent to standard output, it is trapped and returned
to the calling function where, optionally, it's assigned to
a shell variable.
The following code block prompts the user for an answer, reads
the input, calls the yesorno function with the answer as an argument,
and returns 1 or 0, depending on the user's choice:
printf "Answer the question: Y/N: "
read answer
ret_val=$(yesorno $answer)
echo $ret_val
Calling the function in an if statement eliminates the need for the
"ret_val" variable:
if [ $(yesorno $answer) -eq 0 ]
then
echo "answered No"
else
echo "answered Yes"
fi
Since the shell traps the output of a called function, command substitution
works as an alternative to the $() function syntax:
retval='yesorno $answer' # grave mark or back-tick, not single quote
echo $retval
Command substitution syntax works adequately, but consider it obsolete
because a future shell upgrade may eliminate its use.
The user_exist Function Example
Besides the output trapping method, shell functions may return
integer values 0 to 255 via the UNIX exit code $?. Using
the return keyword, return an integer value from the function and
immediately check the exit code. Returning values greater than 255
or less than 0 yields inconsistent and inaccurate results.
The user_exist function determines whether the user id parameter
has been system assigned by checking the beginning of the colon-delimited
/etc/passwd file. Return 1 if the user exists or return 0
if not:
# user_exist: This function takes an argument which is the
# user id, greps the first field of /etc/passwd and sets
# the exit code, 1 if user exists and 0 if not.
# sample the exit code $? from calling program.
function user_exist
{
if grep "^$1:" /etc/passwd > /dev/null 2>&1
then return 1
else return 0
fi
} # end user_exist
Sample the exist code immediately after returning from the user_exist
function:
loginame=alexb
user_exist $loginame
if [ "$?" -eq 0 ]
then printf "User %s does NOT exist\n" $loginame
else printf "User %s exists\n" $loginame
fi
The down_shift Function Example
Use the UNIX typeset command to force a variable to lower
or upper case:
typeset -l x # x defined always lower case
typeset -u y # y defined always upper case
If your shell's typeset command doesn't support the
upper- and lowercase arguments (Red Hat 7.1's Bash shell doesn't),
use the down_shift function:
# downshift: this function return the argument as downshifted string
# usage: cmmd=$(down_shift $cmmd)
function down_shift {
# alternate translate command
#echo $1 | tr "[:upper:]" "[:lower:]"
echo $1 | tr '[A-Z]' '[a-z]'
} # end down_shift
string="ALLLOWERCASE"
string=$(down_shift $string)
printf "%s\n" $string
Executing the down_shift function test:
string="ALLLOWERCASE"
string=$(down_shift $string)
echo $string
The down_shift function declares string variable to be local. This
local declaration guarantees any other invocation of string in the
script is not affected.
Dealing with Multiple Positional Parameters
If a parameter contains white space, the shell interprets the
argument as multiple parameters, one parameter for each word.
Here's an example given a variable comprising of five words:
newstring="STRING OF LOWER CASE WORDS"
Executing the down_shift function with newstring as a parameter
returns only the down-shifted word string, the first parameter.
Since the shell evaluates newstring as five parameters, use
the special character $@ to down shift all the parameters:
# new_down_shift: This function down shifts all positional
# parameters to lowercase and returns the string
function new_down_shift {
echo $@ | tr '[A-Z]' '[a-z]'
} # end new_down_shift
string="STRING OF LOWER CASE WORDS"
string=$(new_down_shift $string)
echo $string
The is_digit Function Example
The is_digit function uses a regular expression to determine whether
an argument contains all digits:
# is_digit: This function matches an all digits regular expression
# against an argument and returns 1 if the argument is digits else 0
# if isn't.
# Note the use of nawk. Use gawk for the Bash shell.
function is_digit
{
echo $1|nawk ' { if ($0 ~ /^[0-9]+$/)
print 1
else
print 0 } '
} # end is_digit
num="345"
ret_val=$(is_digit $num)
echo $ret_val
The is_regex_set Function Example
Because of the white space problem, I seldom write functions that
require more than one argument. The is_regex_set function is an
exception. This function, a modification of is_digit, matches the
first argument against the second argument regular expression:
# is_regex_set: This function takes two arguments:
# argument 1 is the object to check
# argument 2 is a regular expression to match arg 1 against.
# arg 2 is passed to the awk interpreter as a variable.
# if the match is true return 1 else return 0.
# Check for the argument count not equal 2.
function is_regex_set {
local pattern
if [ "$#" -ne 2 ]
then
printf "ARGCNTNE2:"
fi
echo $1|nawk ' { if ($0 ~ pattern)
print 1
else
print 0 } ' pattern="$2"
} # end is_regex_set
To test is_regex_set, use a pattern that is a decimal with optional
sign and fraction:
pattern="^[+-]?[0-9]+[.]?[0-9]*$"
number="-345.03"
ret_val=$(is_regex_set $number $pattern)
echo $ret_val
Remember that white space in either variable number or pattern
can break the above function call. In a function where the parameter
count is an issue, I often enter a warning string if the count isn't
what it should be.
The readkey Function Example
The readkey function returns a single keystroke from standard
input (i.e., the keyboard, without pressing the carriage return):
# readkey: This function saves the current terminal settings and
# sets the terminal to raw mode with the Unix stty command. Retrieve
# 1 character with the dd command and reset original terminal settings
# with stty before returning the character to calling script.
function readkey {
local anykey oldstty
oldstty='stty -g'
stty -icanon -echo min 1 time 0
anykey='dd bs=1 count=1 <&0 2>/dev/null'
stty $oldstty
echo $anykey
} # end read_key
The following code block prompts the user for one character. Pressing
uppercase "Y" tests whether anychar variable is really
set:
printf "Get one character from the keyboard: "
anychar=$(readkey)
echo $anychar
if [ $anychar = 'Y' ]
then
printf "Pressed Upper case Y\n"
fi
The readkey function returns a single character by manipulating
the UNIX terminal driver. For more information, see my article "Returning
a Single Character in a UNIX Shell Script" (Sys Admin,
April 1997, http://www.sysadminmag.com/documents/sam9704f/).
Recursive Functions and the Shell
The shell supports recursion, but not very deeply. The following
recursion test adds one to a local variable until it reaches an
iteration level value passed on the command line:
iteration_level=$1 # How many times to call recursion_test
# recursion_test: This function adds one to it's argument and keeps
# calling itself until it equals the global iteration_level value
# or the function fails.
function recursion_test {
local x
x=$(($1+1))
if [[ x -ge $iteration_level ]]
then
echo $x
exit 1
fi
x=$(recursion_test $x)
echo $x
} # end recursion_test
x=0
recursion_test $x
This recursion test doesn't fail gracefully. Expect a "process
fork failing", an "out of swap space", "too many
open files", or an "out of memory" error. My minimal
testing produces failures at these levels:
iteration_level
Open Server 5 Korn: 429
Solaris 7 Korn: 440
Red Hat 7.1 Linux Bash: 3500
Conclusion
This discussion has presented seven user-defined shell functions
illustrating passing parameters and returning values. Another shell
function tested the recursion limits of the shell.
Some UNIX professionals might complain that shell functions hinder
performance. Possibly, but is not a small sacrifice in system performance
worth easing shell script maintenance later?
References
McConnell, Steven C. 1993. Code Complete. Redmond, WA:
Microsoft Press.
O'Brien, Dennis and David Pitts 2001. Korn Shell Programming
by Example. Indianapolis, IN: Que.
Rosenblatt, Bill 1993. Learning the Korn Shell. 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 hosts the UnixReview.com
monthly Shell Corner column. He can be reached at: olded@ix.netcom.com.
|