Bash functions

Read in 16 minutes

Shell functions in bash allow you to name groups of commands to be run at a later time. Once named, command groups may be executed by using the function name. Each function has its own set of positional parameters when called. Variable attributes such as local may apply within a function.


function help

help function tells you how to create functions in bash, about positional parameters, and exit statuses, which is pretty much all that you need to know about bash functions to get started.

help function
function: function name { COMMANDS ; } or name () { COMMANDS ; }
    Define shell function.

    Create a shell function named NAME.  When invoked as a simple command,
    NAME runs COMMANDs in the calling shell's context.  When NAME is invoked,
    the arguments are passed to the function as $1...$n, and the function's
    name is in $FUNCNAME.

    Exit Status:
    Returns success unless NAME is readonly.

function types

  • simple functions
  • returns non-empty standard output else standard error without a non-zero exit code
  • dynamic functions
  • functions with variable definitions
  • setup function
  • sets variable from a function
  • wrapper functions
  • functions serving as a wrapper to other functions, external, builtin commands
  • oop functions
  • child functions
  • parent functions
  • nested functions
  • functions inside functions
  • once functions
  • functions that have payloads that execute at most once
  • echo function
  • functions that write to standard output
  • lazyload function
  • functions that have bodies read only before execution
  • export function
  • functions attributed as export
  • anonymous functions
  • functions that are once and nameless
  • exit functions
  • functions used for a trap of exit signal
  • read functions
  • functions implemented using read
  • recursive functions
  • functions that call themselves
  • map functions
  • functions that apply an arbitrary function to elements in a list of arguments, returning the resulting list

simple functions

Functions are simple in bash. At least this one is. It puts a string on the screen.

Commands

simple-function() { 
  echo as simple as it gets  
}
simple-function

Output

as simple as it gets

dynamic functions

There are three ways to make functions more dynamic in bash. The first being, passing function names as parameters to other function, the second being using the source, and the third using the function as a router to subcommands.

The first requires that a function be declared beforehand unless used with the second way.

The second way requires file I/O. It may be used to implement dynamic function name assignment in bash.

the first way: pass a function name as a parameter

I know what you are thinking, wouldn't it be nice to declare functions on the fly. Yes, it would but I would argue dynamic functions is something else and good enough. Don't get me wrong, I've been there.

Your program, bash script, may contain a few function pre-defined with meaningful names; or it may contain hundreds or even thousands. Every function is there for a reason, each with the potential to be called. That is your function universe.

Let there be an instance in your function universe that a function holds a reference, variable, holding the name of another function in the universe or itself.

Apply static analysis and fail. That is your dynamic function.

In more concrete terms, we may have real functions as follows.

A dynamic function able to call any function in the universe

dynamic-function() { 
  { local function_name ; function_name="${1}" ; }
  ${function_name}
}

A dynamic function able to call any descendent function in the universe

dynamic-function() { 
  { local function_name ; function_name="${1}" ; }
  ${FUNCNAME}-${function_name}
}

A dynamic function able to call any function of a class in the universe

dynamic-function() { 
  { local function_name ; function_name="${1}" ; }
  class-name-${function_name}
}

If you are looking for dynamic function, see attr.sh. get_ and set_ functions are generated on the fly whenever attr is called.

. ${SH2}/attr.sh
attr x
attr y
attr z
set_x 1
set_y $(( $( get_x ) + 1 ))
set_z $(( $( get_y ) + 1 ))
get_x # 1
get_y # 2 
get_z # 3

the second way: write a function to file and source

This method to implement dynamic functions is the same as employed in attr.sh (above). However, it may be viewed in general as writing a function to a file then sourcing. After all, that is what we do.

#!/bin/bash
## test-dynamic-function-second
## version 0.0.1 - initial
##################################################
test-dynamic-function-second() {
  local temp
  temp=$( mktemp )
  cat > ${temp} << EOF
${function_name}() {
  echo \${FUNCNAME}
}
EOF
  . ${temp}
  declare -pf ${function_name}
  ${function_name}
  rm -v ${temp}
}
##################################################
if [ ${#} -eq 1 ] 
then
 function_name="${1}"
else
 exit 1 # wrong args
fi
##################################################
test-dynamic-function-second
##################################################
## generated by create-stub2.sh v0.1.2
## on Sun, 07 Jul 2019 10:56:38 +0900
## see &lt;https://github.com/temptemp3/sh2&gt;
##################################################

Source: test-dynamic-function-second.sh

the third way: use function as a router

In this way, we use the function as a router such that the first argument indicates a subcommand and the rest its arguments. You can see how dynamic things can get considering the number of possible subcommands is infinite, or at least until you run out of disk space.

setup functions

One thing you may want to do in bash is set a variable from a function. The variable may have global scope or nearest local scope. The following example shows how a setup function would work using local scope to declare a variable.

shopt -s expand_aliases
alias bind='
{
  local account_name
  local account_type
  local account_balance
}
'
setup-acount() {
  acount_name="Joe Doe" # fictitious name
  acount_acount=1 # buck
  acount_type=savings # account
}
bank() {
  bind-account
  setup-account
}

wrapper functions

In case you would like to add context to function within the scope of your bash script, wrapper function eliminates unnecessary repetition in your code and put all the trivial things inside the wrapper so that you can focus on getting things done when developing a program.

cli wrapper functions

CLIs often come with an extensive list of options and subcommands, which make them a good use case for wrapper functions. Once you know what you need, put it in a wrapper function.

aws cli wrapper functions

CLIs come with a hoard of options and subcommands. The AWS-CLI is no different. Here is an example wrapper function for enabling metrics collection in auto scaling groups.

lazyload function

There may be a time when you want to lazyload functions in bash. That means to only source function code when relevant.

Suppose that your big script with hundreds of lines called foo.sh. It happens.

foo.sh

. ${SH2}/aliases/commands.sh
foo-bar-3() {
   true
}
foo-bar-2() {
   true
}
foo-bar-1() {
   true
}
foo-bar() {
  commands
}
foo() {
  commands
}
if [ ${#} -eq 0 ] 
then
 true
else
 exit 1 # wrong args
fi
foo

We want to make foo-bar and its dependents lazy load. That is unless the first argument provided by the user happens to be bar, functions beginning with foo-bar are not sourced.

First, put foo-bar functions in another file like so.

foo-bar.sh

foo-bar-3() {
   true
}
foo-bar-2() {
   true
}
foo-bar-1() {
   true
}
foo-bar() {
  commands
}

As you see I just made a copy of foo.sh and filtered out everything that isn't foo-bar.

Next, replace foo-bar functions with code to lazyload foo-bar.

foo.sh

. ${SH2}/aliases/commands.sh
foo-bar() { . foo-bar.sh ; ${FUNCNAME} ${@} ; } 
foo() {
  commands
}
if [ ${#} -eq 0 ] 
then
 true
else
 exit 1 # wrong args
fi
foo

export function

No life exists outside the shell. In the beginning, there was nothing but an abyss. Lines were read from beyond the tilde in the background. Then, after 4 billion cycles glyphic light filled the foreground. And then there was bash.

Export functions provide a means to include functional code, functions, within another shell that is not a subshell.

Meet true.sh

bash -v true.sh
declare -f true 1>/dev/null || {
  true() {
   command true
  }
}
if [ ${#} -eq 0 ] 
then
 command true
else
 exit 1 # wrong args
fi
true
( . true.sh )
echo ${?} # 0
bash true.sh
echo ${?} # 0

true.sh is a simple bash script that may be changed a export function.

In our shell, we declare a function also called true.

true() {
  echo true
}
( . true.sh ) # true
bash true.sh  # 
declare -fx true
bash -v true.sh 1>/dev/null 
true () {  echo true
}
declare -f true 1>/dev/null || {
  true() {
   command true
  }
}
if [ ${#} -eq 0 ]
then
 command true
else
 exit 1 # wrong args
fi
true
( . true.sh ) # true
bash true.sh # true

For our true to be used in true.sh as a separate shell, the export attribute needs to be added to true. This is done using declare -xf NAME or another bash builtins. See also bash declare

true() {
  echo true
}
( . true.sh ) # true
bash true.sh  # 
declare -fx true
( . true.sh ) # true
bash true.sh  # true

oop functions

One thing that may be missing in bash is builtin support for object-oriented programming concepts such as parent/child relationships. However, in practice, you can get away with such relationships, at least I have.

A parent is any function that can call another function, one of its children, using the ${FUNCNAME} special variable.

parent() { # parent function
  ${FUNCNAME}-child
}
parent-child() { # child function
  true
}

nested functions

There are times when you need nested functions in bash such as when implementing once functions. Simply nest a function definition within another. It's that simple. Nesting functions come in handy when implementing devices such as classes for object-oriented programming.

example: nested function

bash function in function example

outer() {
  inner() { # nested function
    true
  }
  inner
}

once functions

Suppose that we need a function that only runs the first time it is called, i.e. we need a once function. See how it is done in bash.

Commands

once() { 
  once() { 
    true
  }
  echo ${FUNCNAME}
}
test-once() {
  once
  once
  once
}
test-once

Source: test-once.sh

Output

once

echo function

Let an echo function be any function containing 1 or more echo commands. In case no output is produced by the function, the result may be interpreted as failure. Additionally, an echo function may be a function or an alias named echo.

Pseudocommands

echo-function() {
  # some code ...
  echo ${some_variable}
}
echo-function ${some_arguments}

Pseudo-output

some output

anonymous functions

In bash, the closest you are going to get to anonymous is good enough. Here is what I mean. If that is not good enough, then I don't know what is.

#!/bin/bash
## test-anonymous-function
## version 0.0.1 - initial
##################################################
anonymous() {
  _() {
    true
  }
}
test-anonymous-function() {
  _() { { anonymous ; }
    echo anonymous functions are:
    echo - may be nested
    echo - short
    echo - may use positional parameters
    echo - may be once
  }
  _
  _
}
##################################################
if [ ${#} -eq 0 ] 
then
 true
else
 exit 1 # wrong args
fi
##################################################
test-anonymous-function
##################################################
## generated by create-stub2.sh v0.1.2
## on Sun, 07 Jul 2019 11:32:14 +0900
## see <https://github.com/temptemp3/sh2>
##################################################

Source: test-anonymous-function.sh

exit functions

Exit functions extend the builtin exit command. In a script, it may be used to execute cleanup and logging routines. An example of how to override the exit command follows.

Script

#!/bin/bash
## test-function-exit
## version 0.0.1 - initial
##################################################
set -e # errexit
trap exit EXIT
exit() {
  builtin exit ${?}
}
test-function-exit() {
  echo noise
  ( return 0 )
  echo silence
}
##################################################
if [ ${#} -eq 0 ] 
then
 true
else
 exit 1 # wrong args
fi
##################################################
test-function-exit
##################################################
## generated by create-stub2.sh v0.1.2
## on Mon, 08 Jul 2019 19:50:33 +0900
## see <https://github.com/temptemp3/sh2>
##################################################

Source: test-function-exit.sh

Command line

bash test-function-exit.sh

Output

noise

read functions

read functions:

  • use read builtin command

    • read from standard input
  • may be recursive
  • may use positional parameters

recursive functions

Recursive functions call themselves.

Here is an example of a simple recursive function in bash called fun.

fun() { # recursive function
  test ! "${once}" || { true ; return ; }
  once="true"
  fun
}

Note that we can also use the special variable ${FUNCNAME} in place of the function name on recursive function call lines to (in my opinion) to improve readability as follows.

fun() { # recursive function
  test ! "${once}" || { true ; return ; }
  once="true"
  ${FUNCNAME}
}

local variables in recursive functions

In general, you should limit the scope of a variable to the function it is intended to be used in. This is especially, true in recursive functions. Neglecting variable scope in recursive functions could produce unexpected behavior at runtime due to side effects.

#!/bin/bash
## test-function-recursive
## version 0.0.1 - initial
##################################################
fun() { # recursive function
  test "${max_fun}"	|| local -i max_fun_level=255
  test "${fun_level}"	|| local -i fun_level=1
  test ! ${fun_level} -gt ${max_fun_level} || { echo "" ; return ; }
  echo -n "${fun_level} "
  fun_level+=1
  ${FUNCNAME}
}
test-function-recursive() {
  fun
}
##################################################
if [ ${#} -eq 0 ] 
then
 true
else
 exit 1 # wrong args
fi
##################################################
test-function-recursive
##################################################
## generated by create-stub2.sh v0.1.2
## on Sun, 28 Jul 2019 21:32:36 +0900
## see <https://github.com/temptemp3/sh2>
##################################################

map functions

Script

#!/bin/bash
## test-map-functions
## version 0.0.1 - initial
##################################################
square() { { local -i n ; n=${1} ; }
  echo $(( n * n ))
}
map() { { local function_name ; function_name="${1}" ;  }
  local car cdr
  read -t 1 car cdr
  test "${car}" || { true ; return ; }
  ${function_name} ${car} 
  echo ${cdr} | ${FUNCNAME} ${function_name}
}
test-map-functions() {
  seq 10 | xargs | map square
}
##################################################
if [ ${#} -eq 0 ] 
then
 true
else
 exit 1 # wrong args
fi
##################################################
time test-map-functions
##################################################
## generated by create-stub2.sh v0.1.2
## on Sat, 27 Jul 2019 21:13:45 +0900
## see <https://github.com/temptemp3/sh2>
##################################################

Source: test-map-functions.sh

Commands

bash test-map-functions.sh

Output

1
4
9
16
25
36
49
64
81
100

function variables

  • local variables *
  • global variables *
  • positional parameters *
  • special variables

local variables

In bash, all variables defined within functions are global by default. The bash builtin local may be used within a function to declare a NAME as a variable with local scope.

Variable scope is relative to location declared in the script and presence of keywords used such as declare, local, or global. That is, not all variables declared outside a variable are necessarily global. A variable may be local to caller function and global to the function being called but not global to the script.

If NAME is a global or has otherwise been declared outside of the function, the value assigned to NAME is effectively unset if local is present.

If NAME is not a global and has not been declared outside the function, NAME inherits the local attribute.

example: local and global variables in functions

##
## case NAME is global and unset in function by local
##
_() { 
  echo ${return}
  local return
  echo ${return}
}
return=1
_
echo ${return}
# 1
#
#
unset return
unset -f _
##
## case NAME is local and set in function
##
_() {
  echo ${return}
  return=2
  echo ${return}
}
_
echo ${return}
# 
# 2
# 
unset return
unset -f _
##
## case NAME is global and set in function without local
##
_() {
  echo ${return}
  return=4
  echo ${return}
}
return=3
_
echo ${return}
# 3
# 4
# 4
unset return
unset -f _
##
## case NAME is global and set in function with local
##
_() {
  echo ${return}
  local return=5
  echo ${return}
}
return=6
_
echo ${return}
# 6
# 5
# 6

global variables

All variables in bash are global unless set inside a function with the local command. Global variables may be referenced when a local variable exists sharing the same NAME using declare -g to set a global variable from inside a function as follows.

Script

#!/bin/bash
## test-function-global-variables
## version 0.0.1 - initial
##################################################
bar=0
test-function-global-variables() {
  local bar=1
  declare -g bar=2
  echo ${bar}
}
##################################################
if [ ${#} -eq 0 ] 
then
 true
else
 exit 1 # wrong args
fi
##################################################
test-function-global-variables
echo ${bar}
##################################################
## generated by create-stub2.sh v0.1.2
## on Fri, 19 Jul 2019 20:52:09 +0900
## see <https://github.com/temptemp3/sh2>
##################################################

Source: test-function-global-variables.sh

Commands

bash test-function-global-variables.sh

Output

1
2

positional parameters

When a function is called its arguments are automatically assigned to positional parameters based on order appearing after the function name. ${1} ${2} ... are positional parameters. ${#} holds the number of positional parameters available. For example, if ${#} is 9, then the last assigned positional parameter is ${9}.

Script

#!/bin/bash
## test-function-positional-parameter-count
## version 0.0.1 - initial
##################################################
func() {
  echo There are ${#} positional parameter$( test ! ${#} -gt 1 || echo s )
  for i in $( seq ${#} )
  do
  echo \${${i}} is ${@:i:1}
  done
}
test-function-positional-parameter-count() {
  func $( seq $(( RANDOM % 100 )) | sort --random-sort )
}
##################################################
if [ ${#} -eq 0 ] 
then
 true
else
 exit 1 # wrong args
fi
##################################################
test-function-positional-parameter-count
##################################################
## generated by create-stub2.sh v0.1.2
## on Sun, 21 Jul 2019 20:37:44 +0900
## see <https://github.com/temptemp3/sh2>
##################################################

Source: test-function-positional-parameter-count.sh

Commands

bash test-function-positional-parameter-count.sh

Output

There are 3 positional parameters
${1} is 2
${2} is 1
${3} is 3

function operations

  • declare function *
  • get function name *
  • list functions *
  • function return *
  • function exit *
  • calling functions *

declare function

There are essentially two ways to create functions in bash that do not use the declare bash builtin.

1st method

In this method to make functions in bash, you can use the function keyword followed by the name and command list.

function NAME { COMMANDS ; }

2nd method

In this method to make bash functions, you would omit the function keyword. It is okay.

NAME { COMMANDS ; }

Coincidentally, we may declare a function outside of a script using bash history as follows.

{ COMMAND ; }
NAME () !!

However, if you that is too much, I recommend using 1 of the 2 methods prescribed above. By the way, I generally steer towards the 2nd method.

get function name

To get a function name inside a function, use the ${FUNCNAME} special variable.

list functions

There are two ways to list all bash function using the declare command.

To list function names in bash, use the declare -F bash builtin command.

To list function names along with the body, use the declare -f bash builtin command.

function return

At any point within a function, it may return control to its caller on the return command.

Function

fun() {
  return
  code never been touched
}

The return command takes an argument between 0 and 255 to be returned instead of the return value of the last command executed.

help return
return: return [n]
    Return from a shell function.

    Causes a function or sourced script to exit with the return value
    specified by N.  If N is omitted, the return status is that of the
    last command executed within the function or script.

    Exit Status:
    Returns N, or failure if the shell is not executing a function or script.

A function returns automatically after the last command returning the exit code of the last command.

Commands

more-fun() {
  false
}
more-fun
echo ${?}

Output

1

function exit

A function may return control to the caller of the function, exit the function, using the bash builtin return command. Conversely, the exit command will return control to the caller of the script.

calling functions

There are two ways to call functions in bash; the first being statically by the function name or dynamically using variables.

the first way: static function call

Suppose that we have a function called foo. In interactive mode, we call it by typing its name end hitting enter. In the script, we list its name as a command and it is called during execution of the script.

the second way: dynamic function call

Function calls need not be explicit in bash. That is, a variable or output to command substitution may call a function.

Remember in the first way, we have a function named foo. In the second way, we have a variable named bar. Suppose we set bar="foo". In the script ${bar} calls foo.

Now suppose that we have two other functions called foo2 and foo3 and bar=1. What does foo${bar} call? You get the picture.

See also dynamic functions


function return values

When it comes to functions in bash, one topic that often comes is returning values.There are many different things that you may return from a function, return values, exit codes, or nothing at all.

  • exit code *
  • string *
  • arrays *

    • associative array *
    • indexed array *
  • side effects
  • void
  • void with side effects

exit code

The exit code of the last command run inside the function is returned.

Function that returns with a non-zero exit code

fun() { false ; }
fun
echo ${?} # 1

Function that returns with an exit code of zero

fun() { true ; }
fun
echo ${?} # 0

string

Output (standard output and error) returned by the function.

fun() { echo string ; }
var return_value=$( fun )
echo ${return_value} # string

arrays

It is possible to return an array from a function in bash. See the example showing how to return arrays from functions below.

associative arrays

It is possible to return an associative array from a function through standard output. Sanitizing the string representation of the array before instantiation is recommended. An example showing how to return associative arrays from functions follows.

#!/bin/bash
## test-associative-arrays
## version 0.0.1 - initial
##################################################
. ${SH2}/cecho.sh
sanitize-assoc-array() { 
  grep -e '^declare\s-A\s[a-z_]*=(\(\[[^]]*.="[^"]*.\s\)\+)$' -o |
  head -1
}
create-test-assoc-array() {
  {
    declare -A assoc_arr
    assoc_arr['a']=1
    assoc_arr['b']=2
    assoc_arr['c']=3
    declare -p assoc_arr
  } | sanitize-assoc-array
}
modify-test-assoc-array() {
  assoc_arr['a']=1
  assoc_arr['b']=2
  assoc_arr['c']=3
}
test-associative-arrays() {
  cecho green "testing associative arrays ..."
  cecho yellow "test i"
  cecho yellow "receive associative array returned by another function"
  cecho yellow "and set name to hash"
  cecho yellow "expect declare -A hash=([a]=\"1\" [b]=\"2\" [c]=\"3\" )"
  arr=$( create-test-assoc-array )
  eval ${arr/assoc_arr/hash} # instantiate within function
  declare -p hash
  unset hash
  cecho yellow "test ii"
  cecho yellow "receive associative array returned by another function"
  cecho yellow "and use as is"
  cecho yellow "expect declare -A hash=([a]=\"1\" [b]=\"2\" [c]=\"3\" )"
  eval $( create-test-assoc-array ) # instantiate within function
  declare -p assoc_arr
  unset assoc_arry
  cecho yellow "test iii"
  cecho yellow "modify associative array in another function"
  cecho yellow "expect declare -A hash=([a]=\"1\" [b]=\"2\" [c]=\"3\" )"
  declare -A assoc_arr
  modify-test-assoc-array
  declare -p assoc_arr
  cecho green "done testing associative arrays"
}
##################################################
if [ ${#} -eq 0 ] 
then
 true
else
 exit 1 # wrong args
fi
##################################################
test-associative-arrays
##################################################
## generated by create-stub2.sh v0.1.2
## on Sat, 06 Jul 2019 13:53:40 +0900
## see <https://github.com/temptemp3/sh2>
##################################################

Source: test-associative-arrays.sh

indexed arrays

It is possible to return an indexed array from a shell function. Sanitizing the string representation of the array before instantiation is recommended. See the example showing how to return from functions above.


© Nicholas Shellabarger