Functions

Advanced Bash-Scripting Guide: An in-depth exploration of the art of shell scripting
Prev |  | Next

# Chapter 24. Functions

Like "real" programming languages, Bash has functions, though in a somewhat limited implementation. A function is a subroutine, a code block that implements a set of operations, a "black box" that performs a specified task. Wherever there is repetitive code, when a task repeats with only slight variations in procedure, then consider using a function.

function `function_name` {
`command`...
}

or

`function_name` () {
`command`...
}

This second form will cheer the hearts of C programmers (and is more portable).

As in C, the function's opening bracket may optionally appear on the second line.

`function_name` ()
{
`command`...
}

| A function may be "compacted" into a single line.
       1 fun () { echo "This is a function"; echo; }
       2 #                                 ^     ^
In this case, however, a semicolon must follow the final command in the function.
       1 fun () { echo "This is a function"; echo } # Error!
       2 #                                       ^
       3
       4 fun2 () { echo "Even a single-command function? Yes!"; }
       5 #                                                    ^

Functions are called, triggered, simply by invoking their names. A function call is equivalent to a command.

Example 24-1. Simple functions

       1 #!/bin/bash
       2 # ex59.sh: Exercising functions (simple).
       3
       4 JUST_A_SECOND=1
       5
       6 funky ()
       7 { # This is about as simple as functions get.
       8   echo "This is a funky function."
       9   echo "Now exiting funky function."
      10 } # Function declaration must precede call.
      11
      12
      13 fun ()
      14 { # A somewhat more complex function.
      15   i=0
      16   REPEATS=30
      17
      18   echo
      19   echo "And now the fun really begins."
      20   echo
      21
      22   sleep $JUST_A_SECOND    # Hey, wait a second!
      23   while [ $i -lt $REPEATS ]
      24   do
      25     echo "----------FUNCTIONS---------->"
      26     echo "<------------ARE-------------"
      27     echo "<------------FUN------------>"
      28     echo
      29     let "i+=1"
      30   done
      31 }
      32
      33   # Now, call the functions.
      34
      35 funky
      36 fun
      37
      38 exit $?

The function definition must precede the first call to it. There is no method of "declaring" the function, as, for example, in C.

       1 f1
       2 # Will give an error message, since function "f1" not yet defined.
       3
       4 declare -f f1      # This doesn't help either.
       5 f1                 # Still an error message.
       6
       7 # However...
       8
       9
      10 f1 ()
      11 {
      12   echo "Calling function \"f2\" from within function \"f1\"."
      13   f2
      14 }
      15
      16 f2 ()
      17 {
      18   echo "Function \"f2\"."
      19 }
      20
      21 f1  #  Function "f2" is not actually called until this point,
      22     #+ although it is referenced before its definition.
      23     #  This is permissible.
      24
      25     # Thanks, S.C.

| Functions may not be empty!
       1 #!/bin/bash
       2 # empty-function.sh
       3
       4 empty ()
       5 {
       6 }
       7
       8 exit 0  # Will not exit here!
       9
      10 # $ sh empty-function.sh
      11 # empty-function.sh: line 6: syntax error near unexpected token `}'
      12 # empty-function.sh: line 6: `}'
      13
      14 # $ echo $?
      15 # 2
      16
      17
      18 # Note that a function containing only comments is empty.
      19
      20 func ()
      21 {
      22   # Comment 1.
      23   # Comment 2.
      24   # This is still an empty function.
      25   # Thank you, Mark Bova, for pointing this out.
      26 }
      27 # Results in same error message as above.
      28
      29
      30 # However ...
      31
      32 not_quite_empty ()
      33 {
      34   illegal_command
      35 } #  A script containing this function will *not* bomb
      36   #+ as long as the function is not called.
      37
      38 not_empty ()
      39 {
      40   :
      41 } # Contains a : (null command), and this is okay.
      42
      43
      44 # Thank you, Dominick Geyer and Thiemo Kellner.

It is even possible to nest a function within another function, although this is not very useful.

       1 f1 ()
       2 {
       3
       4   f2 () # nested
       5   {
       6     echo "Function \"f2\", inside \"f1\"."
       7   }
       8
       9 }
      10
      11 f2  #  Gives an error message.
      12     #  Even a preceding "declare -f f2" wouldn't help.
      13
      14 echo
      15
      16 f1  #  Does nothing, since calling "f1" does not automatically call "f2".
      17 f2  #  Now, it's all right to call "f2",
      18     #+ since its definition has been made visible by calling "f1".
      19
      20     # Thanks, S.C.

Function declarations can appear in unlikely places, even where a command would otherwise go.

       1 ls -l | foo() { echo "foo"; }  # Permissible, but useless.
       2
       3
       4
       5 if [ "$USER" = bozo ]
       6 then
       7   bozo_greet ()   # Function definition embedded in an if/then construct.
       8   {
       9     echo "Hello, Bozo."
      10   }
      11 fi
      12
      13 bozo_greet        # Works only for Bozo, and other users get an error.
      14
      15
      16
      17 # Something like this might be useful in some contexts.
      18 NO_EXIT=1   # Will enable function definition below.
      19
      20 [[ $NO_EXIT -eq 1 ]] && exit() { true; }     # Function definition in an "and-list".
      21 # If $NO_EXIT is 1, declares "exit ()".
      22 # This disables the "exit" builtin by aliasing it to "true".
      23
      24 exit  # Invokes "exit ()" function, not "exit" builtin.
      25
      26
      27
      28 # Or, similarly:
      29 filename=file1
      30
      31 [ -f "$filename" ] &&
      32 foo () { rm -f "$filename"; echo "File "$filename" deleted."; } ||
      33 foo () { echo "File "$filename" not found."; touch bar; }
      34
      35 foo
      36
      37 # Thanks, S.C. and Christopher Head

Function names can take strange forms.

       1   _(){ for i in {1..10}; do echo -n "$FUNCNAME"; done; echo; }
       2 # ^^^         No space between function name and parentheses.
       3 #             This doesn't always work. Why not?
       4
       5 # Now, let's invoke the function.
       6   _         # __________
       7 #             ^^^^^^^^^^   10 underscores (10 x function name)!
       8 # A "naked" underscore is an acceptable function name.
       9
      10
      11 # In fact, a colon is likewise an acceptable function name.
      12
      13 :(){ echo ":"; }; :
      14
      15 # Of what use is this?
      16 # It's a devious way to obfuscate the code in a script.

See also Example A-56

| What happens when different versions of the same function appear in a script?
       1 #  As Yan Chen points out,
       2 #  when a function is defined multiple times,
       3 #  the final version is what is invoked.
       4 #  This is not, however, particularly useful.
       5
       6 func ()
       7 {
       8   echo "First version of func ()."
       9 }
      10
      11 func ()
      12 {
      13   echo "Second version of func ()."
      14 }
      15
      16 func   # Second version of func ().
      17
      18 exit $?
      19
      20 #  It is even possible to use functions to override
      21 #+ or preempt system commands.
      22 #  Of course, this is *not* advisable.

# 24.1. Complex Functions and Function Complexities

Functions may process arguments passed to them and return an exit status to the script for further processing.

       1 function_name $arg1 $arg2

The function refers to the passed arguments by position (as if they were positional parameters), that is, `$1`, `$2`, and so forth.

Example 24-2. Function Taking Parameters

       1 #!/bin/bash
       2 # Functions and parameters
       3
       4 DEFAULT=default                             # Default param value.
       5
       6 func2 () {
       7    if [ -z "$1" ]                           # Is parameter #1 zero length?
       8    then
       9      echo "-Parameter #1 is zero length.-"  # Or no parameter passed.
      10    else
      11      echo "-Parameter #1 is \"$1\".-"
      12    fi
      13
      14    variable=${1-$DEFAULT}                   #  What does
      15    echo "variable = $variable"              #+ parameter substitution show?
      16                                             #  ---------------------------
      17                                             #  It distinguishes between
      18                                             #+ no param and a null param.
      19
      20    if [ "$2" ]
      21    then
      22      echo "-Parameter #2 is \"$2\".-"
      23    fi
      24
      25    return 0
      26 }
      27
      28 echo
      29
      30 echo "Nothing passed."
      31 func2                          # Called with no params
      32 echo
      33
      34
      35 echo "Zero-length parameter passed."
      36 func2 ""                       # Called with zero-length param
      37 echo
      38
      39 echo "Null parameter passed."
      40 func2 "$uninitialized_param"   # Called with uninitialized param
      41 echo
      42
      43 echo "One parameter passed."
      44 func2 first           # Called with one param
      45 echo
      46
      47 echo "Two parameters passed."
      48 func2 first second    # Called with two params
      49 echo
      50
      51 echo "\"\" \"second\" passed."
      52 func2 "" second       # Called with zero-length first parameter
      53 echo                  # and ASCII string as a second one.
      54
      55 exit 0

| The shift command works on arguments passed to functions (see Example 36-18).

But, what about command-line arguments passed to the script? Does a function see them? Well, let's clear up the confusion.

Example 24-3. Functions and command-line args passed to the script

       1 #!/bin/bash
       2 # func-cmdlinearg.sh
       3 #  Call this script with a command-line argument,
       4 #+ something like $0 arg1.
       5
       6
       7 func ()
       8
       9 {
      10 echo "$1"   # Echoes first arg passed to the function.
      11 }           # Does a command-line arg qualify?
      12
      13 echo "First call to function: no arg passed."
      14 echo "See if command-line arg is seen."
      15 func
      16 # No! Command-line arg not seen.
      17
      18 echo "============================================================"
      19 echo
      20 echo "Second call to function: command-line arg passed explicitly."
      21 func $1
      22 # Now it's seen!
      23
      24 exit 0

In contrast to certain other programming languages, shell scripts normally pass only value parameters to functions. Variable names (which are actually pointers), if passed as parameters to functions, will be treated as string literals. Functions interpret their arguments literally.

Indirect variable references (see Example 37-2) provide a clumsy sort of mechanism for passing variable pointers to functions.

Example 24-4. Passing an indirect reference to a function

       1 #!/bin/bash
       2 # ind-func.sh: Passing an indirect reference to a function.
       3
       4 echo_var ()
       5 {
       6 echo "$1"
       7 }
       8
       9 message=Hello
      10 Hello=Goodbye
      11
      12 echo_var "$message"        # Hello
      13 # Now, let's pass an indirect reference to the function.
      14 echo_var "${!message}"     # Goodbye
      15
      16 echo "-------------"
      17
      18 # What happens if we change the contents of "hello" variable?
      19 Hello="Hello, again!"
      20 echo_var "$message"        # Hello
      21 echo_var "${!message}"     # Hello, again!
      22
      23 exit 0

The next logical question is whether parameters can be dereferenced after being passed to a function.

Example 24-5. Dereferencing a parameter passed to a function

       1 #!/bin/bash
       2 # dereference.sh
       3 # Dereferencing parameter passed to a function.
       4 # Script by Bruce W. Clare.
       5
       6 dereference ()
       7 {
       8      y=\$"$1"   # Name of variable (not value!).
       9      echo $y    # $Junk
      10
      11      x=`eval "expr \"$y\" "`
      12      echo $1=$x
      13      eval "$1=\"Some Different Text \""  # Assign new value.
      14 }
      15
      16 Junk="Some Text"
      17 echo $Junk "before"    # Some Text before
      18
      19 dereference Junk
      20 echo $Junk "after"     # Some Different Text after
      21
      22 exit 0

Example 24-6. Again, dereferencing a parameter passed to a function

       1 #!/bin/bash
       2 # ref-params.sh: Dereferencing a parameter passed to a function.
       3 #                (Complex Example)
       4
       5 ITERATIONS=3  # How many times to get input.
       6 icount=1
       7
       8 my_read () {
       9   #  Called with my_read varname,
      10   #+ outputs the previous value between brackets as the default value,
      11   #+ then asks for a new value.
      12
      13   local local_var
      14
      15   echo -n "Enter a value "
      16   eval 'echo -n "[$'$1'] "'  #  Previous value.
      17 # eval echo -n "[\$$1] "     #  Easier to understand,
      18                              #+ but loses trailing space in user prompt.
      19   read local_var
      20   [ -n "$local_var" ] && eval $1=\$local_var
      21
      22   # "And-list": if "local_var" then set "$1" to its value.
      23 }
      24
      25 echo
      26
      27 while [ "$icount" -le "$ITERATIONS" ]
      28 do
      29   my_read var
      30   echo "Entry #$icount = $var"
      31   let "icount += 1"
      32   echo
      33 done
      34
      35
      36 # Thanks to Stephane Chazelas for providing this instructive example.
      37
      38 exit 0

Exit and Return

exit status

  Functions return a value, called an exit status. This is analogous to the exit status returned by a command. The exit status may be explicitly specified by a return statement, otherwise it is the exit status of the last command in the function (0 if successful, and a non-zero error code if not). This exit status may be used in the script by referencing it as $?. This mechanism effectively permits script functions to have a "return value" similar to C functions.

return

  Terminates a function. A return command [1] optionally takes an integer argument, which is returned to the calling script as the "exit status" of the function, and this exit status is assigned to the variable $?.

  Example 24-7. Maximum of two numbers

         1 #!/bin/bash
         2 # max.sh: Maximum of two integers.
         3
         4 E_PARAM_ERR=250    # If less than 2 params passed to function.
         5 EQUAL=251          # Return value if both params equal.
         6 #  Error values out of range of any
         7 #+ params that might be fed to the function.
         8
         9 max2 ()             # Returns larger of two numbers.
        10 {                   # Note: numbers compared must be less than 250.
        11 if [ -z "$2" ]
        12 then
        13   return $E_PARAM_ERR
        14 fi
        15
        16 if [ "$1" -eq "$2" ]
        17 then
        18   return $EQUAL
        19 else
        20   if [ "$1" -gt "$2" ]
        21   then
        22     return $1
        23   else
        24     return $2
        25   fi
        26 fi
        27 }
        28
        29 max2 33 34
        30 return_val=$?
        31
        32 if [ "$return_val" -eq $E_PARAM_ERR ]
        33 then
        34   echo "Need to pass two parameters to the function."
        35 elif [ "$return_val" -eq $EQUAL ]
        36   then
        37     echo "The two numbers are equal."
        38 else
        39     echo "The larger of the two numbers is $return_val."
        40 fi
        41
        42
        43 exit 0
        44
        45 #  Exercise (easy):
        46 #  ---------------
        47 #  Convert this to an interactive script,
        48 #+ that is, have the script ask for input (two numbers).

  | For a function to return a string or array, use a dedicated variable.
         1 count_lines_in_etc_passwd()
         2 {
         3   [[ -r /etc/passwd ]] && REPLY=$(echo $(wc -l < /etc/passwd))
         4   #  If /etc/passwd is readable, set REPLY to line count.
         5   #  Returns both a parameter value and status information.
         6   #  The 'echo' seems unnecessary, but . . .
         7   #+ it removes excess whitespace from the output.
         8 }
         9
        10 if count_lines_in_etc_passwd
        11 then
        12   echo "There are $REPLY lines in /etc/passwd."
        13 else
        14   echo "Cannot count lines in /etc/passwd."
        15 fi
        16
        17 # Thanks, S.C.

  Example 24-8. Converting numbers to Roman numerals

         1 #!/bin/bash
         2
         3 # Arabic number to Roman numeral conversion
         4 # Range: 0 - 200
         5 # It's crude, but it works.
         6
         7 # Extending the range and otherwise improving the script is left as an exercise.
         8
         9 # Usage: roman number-to-convert
        10
        11 LIMIT=200
        12 E_ARG_ERR=65
        13 E_OUT_OF_RANGE=66
        14
        15 if [ -z "$1" ]
        16 then
        17   echo "Usage: `basename $0` number-to-convert"
        18   exit $E_ARG_ERR
        19 fi
        20
        21 num=$1
        22 if [ "$num" -gt $LIMIT ]
        23 then
        24   echo "Out of range!"
        25   exit $E_OUT_OF_RANGE
        26 fi
        27
        28 to_roman ()   # Must declare function before first call to it.
        29 {
        30 number=$1
        31 factor=$2
        32 rchar=$3
        33 let "remainder = number - factor"
        34 while [ "$remainder" -ge 0 ]
        35 do
        36   echo -n $rchar
        37   let "number -= factor"
        38   let "remainder = number - factor"
        39 done
        40
        41 return $number
        42        # Exercises:
        43        # ---------
        44        # 1) Explain how this function works.
        45        #    Hint: division by successive subtraction.
        46        # 2) Extend to range of the function.
        47        #    Hint: use "echo" and command-substitution capture.
        48 }
        49
        50
        51 to_roman $num 100 C
        52 num=$?
        53 to_roman $num 90 LXXXX
        54 num=$?
        55 to_roman $num 50 L
        56 num=$?
        57 to_roman $num 40 XL
        58 num=$?
        59 to_roman $num 10 X
        60 num=$?
        61 to_roman $num 9 IX
        62 num=$?
        63 to_roman $num 5 V
        64 num=$?
        65 to_roman $num 4 IV
        66 num=$?
        67 to_roman $num 1 I
        68 # Successive calls to conversion function!
        69 # Is this really necessary??? Can it be simplified?
        70
        71 echo
        72
        73 exit

  See also Example 11-29.

  | The largest positive integer a function can return is 255. The return command is closely tied to the concept of exit status, which accounts for this particular limitation. Fortunately, there are various workarounds for those situations requiring a large integer return value from a function. Example 24-9. Testing large return values in a function
         1 #!/bin/bash
         2 # return-test.sh
         3
         4 # The largest positive value a function can return is 255.
         5
         6 return_test ()         # Returns whatever passed to it.
         7 {
         8   return $1
         9 }
        10
        11 return_test 27         # o.k.
        12 echo $?                # Returns 27.
        13
        14 return_test 255        # Still o.k.
        15 echo $?                # Returns 255.
        16
        17 return_test 257        # Error!
        18 echo $?                # Returns 1 (return code for miscellaneous error).
        19
        20 # =========================================================
        21 return_test -151896    # Do large negative numbers work?
        22 echo $?                # Will this return -151896?
        23                        # No! It returns 168.
        24 #  Version of Bash before 2.05b permitted
        25 #+ large negative integer return values.
        26 #  It happened to be a useful feature.
        27 #  Newer versions of Bash unfortunately plug this loophole.
        28 #  This may break older scripts.
        29 #  Caution!
        30 # =========================================================
        31
        32 exit 0
  A workaround for obtaining large integer "return values" is to simply assign the "return value" to a global variable.
         1 Return_Val=   # Global variable to hold oversize return value of function.
         2
         3 alt_return_test ()
         4 {
         5   fvar=$1
         6   Return_Val=$fvar
         7   return   # Returns 0 (success).
         8 }
         9
        10 alt_return_test 1
        11 echo $?                              # 0
        12 echo "return value = $Return_Val"    # 1
        13
        14 alt_return_test 256
        15 echo "return value = $Return_Val"    # 256
        16
        17 alt_return_test 257
        18 echo "return value = $Return_Val"    # 257
        19
        20 alt_return_test 25701
        21 echo "return value = $Return_Val"    #25701
  A more elegant method is to have the function echo its "return value to `stdout`," and then capture it by command substitution. See the discussion of this in Section 36.7. Example 24-10. Comparing two large integers
         1 #!/bin/bash
         2 # max2.sh: Maximum of two LARGE integers.
         3
         4 #  This is the previous "max.sh" example,
         5 #+ modified to permit comparing large integers.
         6
         7 EQUAL=0             # Return value if both params equal.
         8 E_PARAM_ERR=-99999  # Not enough params passed to function.
         9 #           ^^^^^^    Out of range of any params that might be passed.
        10
        11 max2 ()             # "Returns" larger of two numbers.
        12 {
        13 if [ -z "$2" ]
        14 then
        15   echo $E_PARAM_ERR
        16   return
        17 fi
        18
        19 if [ "$1" -eq "$2" ]
        20 then
        21   echo $EQUAL
        22   return
        23 else
        24   if [ "$1" -gt "$2" ]
        25   then
        26     retval=$1
        27   else
        28     retval=$2
        29   fi
        30 fi
        31
        32 echo $retval        # Echoes (to stdout), rather than returning value.
        33                     # Why?
        34 }
        35
        36
        37 return_val=$(max2 33001 33997)
        38 #            ^^^^             Function name
        39 #                 ^^^^^ ^^^^^ Params passed
        40 #  This is actually a form of command substitution:
        41 #+ treating a function as if it were a command,
        42 #+ and assigning the stdout of the function to the variable "return_val."
        43
        44
        45 # ========================= OUTPUT ========================
        46 if [ "$return_val" -eq "$E_PARAM_ERR" ]
        47   then
        48   echo "Error in parameters passed to comparison function!"
        49 elif [ "$return_val" -eq "$EQUAL" ]
        50   then
        51     echo "The two numbers are equal."
        52 else
        53     echo "The larger of the two numbers is $return_val."
        54 fi
        55 # =========================================================
        56
        57 exit 0
        58
        59 #  Exercises:
        60 #  ---------
        61 #  1) Find a more elegant way of testing
        62 #+    the parameters passed to the function.
        63 #  2) Simplify the if/then structure at "OUTPUT."
        64 #  3) Rewrite the script to take input from command-line parameters.
  Here is another example of capturing a function "return value." Understanding it requires some knowledge of awk.
         1 month_length ()  # Takes month number as an argument.
         2 {                # Returns number of days in month.
         3 monthD="31 28 31 30 31 30 31 31 30 31 30 31"  # Declare as local?
         4 echo "$monthD" | awk '{ print $'"${1}"' }'    # Tricky.
         5 #                             ^^^^^^^^^
         6 # Parameter passed to function  ($1 -- month number), then to awk.
         7 # Awk sees this as "print $1 . . . print $12" (depending on month number)
         8 # Template for passing a parameter to embedded awk script:
         9 #                                 $'"${script_parameter}"'
        10
        11 #    Here's a slightly simpler awk construct:
        12 #    echo $monthD | awk -v month=$1 '{print $(month)}'
        13 #    Uses the -v awk option, which assigns a variable value
        14 #+   prior to execution of the awk program block.
        15 #    Thank you, Rich.
        16
        17 #  Needs error checking for correct parameter range (1-12)
        18 #+ and for February in leap year.
        19 }
        20
        21 # ----------------------------------------------
        22 # Usage example:
        23 month=4        # April, for example (4th month).
        24 days_in=$(month_length $month)
        25 echo $days_in  # 30
        26 # ----------------------------------------------
  See also Example A-7 and Example A-37. `Exercise:` Using what we have just learned, extend the previous Roman numerals example to accept arbitrarily large input.

Redirection

`Redirecting the stdin of a function`

  A function is essentially a code block, which means its `stdin` can be redirected (as in Example 3-1).

  Example 24-11. Real name from username

         1 #!/bin/bash
         2 # realname.sh
         3 #
         4 # From username, gets "real name" from /etc/passwd.
         5
         6
         7 ARGCOUNT=1       # Expect one arg.
         8 E_WRONGARGS=85
         9
        10 file=/etc/passwd
        11 pattern=$1
        12
        13 if [ $# -ne "$ARGCOUNT" ]
        14 then
        15   echo "Usage: `basename $0` USERNAME"
        16   exit $E_WRONGARGS
        17 fi
        18
        19 file_excerpt ()    #  Scan file for pattern,
        20 {                  #+ then print relevant portion of line.
        21   while read line  # "while" does not necessarily need [ condition ]
        22   do
        23     echo "$line" | grep $1 | awk -F":" '{ print $5 }'
        24     # Have awk use ":" delimiter.
        25   done
        26 } <$file  # Redirect into function's stdin.
        27
        28 file_excerpt $pattern
        29
        30 # Yes, this entire script could be reduced to
        31 #       grep PATTERN /etc/passwd | awk -F":" '{ print $5 }'
        32 # or
        33 #       awk -F: '/PATTERN/ {print $5}'
        34 # or
        35 #       awk -F: '($1 == "username") { print $5 }' # real name from username
        36 # However, it might not be as instructive.
        37
        38 exit 0

  There is an alternate, and perhaps less confusing method of redirecting a function's `stdin`. This involves redirecting the `stdin` to an embedded bracketed code block within the function.

         1 # Instead of:
         2 Function ()
         3 {
         4  ...
         5  } < file
         6
         7 # Try this:
         8 Function ()
         9 {
        10   {
        11     ...
        12    } < file
        13 }
        14
        15 # Similarly,
        16
        17 Function ()  # This works.
        18 {
        19   {
        20    echo $*
        21   } | tr a b
        22 }
        23
        24 Function ()  # This doesn't work.
        25 {
        26   echo $*
        27 } | tr a b   # A nested code block is mandatory here.
        28
        29
        30 # Thanks, S.C.

  | Emmanuel Rouat's sample `bashrc` file contains some instructive examples of functions.

### Notes

[1] | The return command is a Bash builtin.

Prev | Home | Next
Process Substitution | Up | Local Variables