BASH: using functions, with examples

By | 01/15/2023
 

terminalThis a translation of a post from 2013 with some edits, but still relevant for learning BASH.

In fact, a function in bash is a regular variable, but with more features.

The main use is when the same code needs to be used several times and/or in different related scripts.

Declaring and calling a function

The function is declared like this:

function function_name ()
{
  function body
}

Or:

function one {
  echo "One"
}

two () {
  echo "Two"
}

function three () {
  echo "Three"
}

However, the most correct option, in order to make the script compatible with different shell versions, would be the second one:

two () {
  echo "Two"
}

And try to never use the third option:

function three () {
  echo "Three"
}

You can call a function simply by specifying its name in the body of the script:

#!/bin/bash
function one {
  echo "One"
}

one

[simterm]

$ ./example.sh
One

[/simterm]

It is important that the function declaration be exist before it is called, otherwise, an error will be received:

#!/bin/bash

function one {
  echo "One"
}

one

two

function two {
  echo "Two"
}

[simterm]

$ ./example.sh
One
./example.sh: line 7: two: command not found

[/simterm]

Calling a function with arguments

Let’s move on to a more complex function, and consider calling a function with arguments.

For example, let’s take a function that is called at the place in the code where you need to get a response from the user:

#!/bin/bash

answer () {
  while read response; do
    echo
      case $response in
        [yY][eE][sS]|[yY])
          printf "$1"
          $2
          break
          ;;
        [nN][oO]|[nN])
          printf "$3"
          $4
          break
          ;;
        *)
          printf "Please, enter Y(yes) or N(no)! "
      esac
  done
}

echo "Run application? (Yes/No) "
answer "Run" "" "Not run" ""

In this case, the function answer() expects a response from the user in the style Yesor No(or any variation given in the expression [yY][eE][sS]|[yY]or [nN][oO]|[nN]), and depending on the response, performs a certain action.

In case of a response Yes, the action specified in the first argument $1 with which the function was called will be performed.

Let’s check:

[simterm]

$ bash test.sh 
Run application? (Yes/No) 
y

Run

[/simterm]

With answer No:

[simterm]

$ ./example.sh

Run application? (Yes/No)
no

Not run

[/simterm]

Calling commands directly from arguments, and even more so from variables, is considered not the best solution, so let’s rewrite it and call it with the operators && (that is in case of success, when receiving code 0) and || – in case of an error and receiving response code 1:

#!/bin/bash

answer () {
  while read response; do
    echo
      case $response in
        [yY][eE][sS]|[yY])
          printf "$1\n"
          return 0
          break
          ;;
        [nN][oO]|[nN])
          printf "$2\n"
          return 1
          break
          ;;
        *)
          printf "Please, enter Y(yes) or N(no)! "
      esac
  done
}

echo -e "\nRun application? (Yes/No) "
answer "Run" "Will not run" && echo "I'm script" || echo "Doing nothing"

Now we pass the answer “Run” as the first argument to the function, and in the case of the user’s answer “Yes“, we’ll execute the printf "Run" and echo "I'm script". If the answer Nois selected, then we print the second argument Will not run, and perform the action echo "Doing nothing":

[simterm]

$ bash test.sh 

Run application? (Yes/No) 
y

Run
I'm script

$ bash test.sh 

Run application? (Yes/No) 
no

Will not run
Doing nothing

[/simterm]

Accordingly, instead of the echo you can run any other command:

#!/bin/bash

answer () {
  while read response; do
    echo
      case $response in
        [yY][eE][sS]|[yY])
          printf "$1\n"
          return 0
          break
          ;;
        [nN][oO]|[nN])
          printf "$2\n"
          return 1
          break
          ;;
        *)
          printf "Please, enter Y(yes) or N(no)! "
      esac
  done
}

echo -e "\nKill TOP application? (Yes/No) "
answer "Killing TOP" "Left it alive" && pkill top || echo "Doing nothing"

[simterm]

$ ./example.sh

Kill TOP application? (Yes/No)
y

Killing TOP

[/simterm]

It is important to keep in mind that if the first command fails (in this example, pkill does not find the specified process), then the function will return code 1, and the second part will be executed:

[simterm]

$ ./example.sh

Kill TOP application? (Yes/No)
y

Killing TOP
Doing nothing

[/simterm]

Variables in functions

Variables can also be used in arguments.

For example, you can define several answers in different variables, and use the right one in different cases:

#!/bin/bash

answer () {
  while read response; do
    echo
      case $response in
        [yY][eE][sS]|[yY])
          printf "$1\n"
          return 0
          break
          ;;
        [nN][oO]|[nN])
          printf "$2\n"
          return 1
          break
          ;;
        *)
          printf "Please, enter Y(yes) or N(no)! "
     esac
  done
}

replay1="Killing TOP"
replay2="Left it alive"

echo -e "\nKill TOP application? (Yes/No) "
answer "$replay1" "$replay2" && echo "I'm script" || echo "Doing nothing"

[simterm]

$ ./example.sh

Kill TOP application? (Yes/No)
y

Killing TOP
I'm script
$ ./example.sh

Kill TOP application? (Yes/No)
n

Left it alive
Doing nothing

[/simterm]

As with regular variables, functions use “positional arguments”, i.e.:

  • $# – display the number of passed arguments
  • $* – display a list of all passed arguments
  • $@ – the same as $*– but each argument is considered as a simple word (string)
  • $1 - $9 – are numbered arguments, depending on the position in the list

For example, let’s create a script with a function that should display the number of arguments passed:

#!/bin/bash

example () {
  echo $#
  shift
}

example $*

[simterm]

$ ./example.sh 1 2 3 4
4

[/simterm]

Or just display all the arguments passed to it:

#!/bin/bash

example () {
  echo $*
  shift
}

example $*

[simterm]

$ ./example.sh 1 2 3 4
1 2 3 4

[/simterm]

Or you can pass arguments directly when calling a function, and not when calling a script, as in the example above:

#!/bin/bash

example () {
  echo $*
  shift
}

example 1 2 3 4

[simterm]

$ ./example.sh
1 2 3 4

[/simterm]

Local variables

By default, all given variables in bash scripts are considered global within the script itself, but in a function, you can declare a local variable that will be available only during its (function) execution.

Example:

#!/bin/bash

ex0=0

example () {
  local ex1=1

  echo "$ex1"
}

example

[[ $ex0 ]] && echo "Variable found" || echo "Can't find variable!"
[[ $ex1 ]] && echo "Variable found" || echo "Can't find variable!"

Check it:

[simterm]

$ bash test.sh
1
Variable found
Can't find variable!

[/simterm]

Math operations in functions

As with variables, functions can use mathematical operations.

For example this function:

#!/bin/bash

mat () {
  a=1
  (( a++ ))
  echo $a
}

mat

As a result, we get the value of the variable $a + 1:

[simterm]

$ ./mat.sh
2

[/simterm]

A more complex example – using several variables and calculating their value:

#!/bin/bash

mat () {
  a=1
  b=2
  c=$(( a + b ))
  echo $c
}

mat

Result:

[simterm]

$ ./mat.sh
3

[/simterm]

Another option is to use arguments:

#!/bin/bash

mat () {
  a=$1
  b=$2
  c=$(( a + b ))
  echo $c
}

mat $1 $2

Run it:

[simterm]

$ ./mat.sh 1 1
2

[/simterm]

Recursive functions

A recursive function is a function that, when called, calls itself.

For example:

#!/bin/bash

recursion () {
  count=$(( $count + 1 ))
  echo $count
  recursion
}

recursion

Such a function will call itself endlessly until its execution is manually interrupted:

[simterm]

$ ./example.sh
...
913
914
915

[/simterm]

For better clarity, let’s add a loop that checks the condition: if the variable $count exceeds the value of the variable $recursions, then the function will stop its execution:

#!/bin/bash

count=0
recursions=4

recursion () {
  count=$(( $count + 1 ))
  echo $count

  while [ $count -le $recursions ]; do
    recursion
  done
}

recursion

Run the script:

[simterm]

$ ./example.sh
1
2
3
4
5

[/simterm]

To simplify the script, you can replace the expression count=$(( $count + 1 )) with (( count++ )):

#!/bin/bash

count=0
recursions=4

recursion () {
  (( count++ ))
  echo $count

  while [ $count -le $recursions ]; do
    recursion
  done
}

recursion

Check it:

[simterm]

$ ./example.sh
1
2
3
4
5

[/simterm]

Export functions

To pass a function to the next script called in a new (child) instance of shell, it must be exported.

For example, let’s take two files – in the file 1.sh we will declare a function and call the script 2.sh:

#!/bin/bash

one () {
  echo "one"
}

bash 2.sh

And in the file 2.sh will try to use this function:

#!/bin/bash

one

Run it:

[simterm]

$ ./1.sh 
2.sh: line 3: one: command not found

[/simterm]

Now, export the function using the option export with the key -f:

#!/bin/bash

one () {
  echo "one"
}

export -f one

bash 2.sh

Run:

[simterm]

$ ./1.sh 
one

[/simterm]

Another option is to call the second script in the same instance of the shell by using the source:

#!/bin/bash

one () {
  echo "one"
}

source 2.sh

Or so:

#!/bin/bash

one () {
  echo "one"
}

. 2.sh

Both options are equivalent and will give the same result:

[simterm]

$ ./1.sh
one

[/simterm]

Checking for a function availability

Sometimes it is necessary to check if a function exists before executing it. For this, we can use the declare command.

Called with a key -f and no arguments declare will display a bodies of all available functions:

#!/bin/bash

one () {
  echo "one"
}

two () {
  echo "two"
}

declare -f

Result:

[simterm]

$ ./test.sh 
one () 
{ 
    echo "one"
}
two () 
{ 
    echo "two"
}

[/simterm]

With the key -F – only names:

#!/bin/bash

one () {
  echo "one"
}

two () {
  echo "two"
}

declare -F

And:

[simterm]

$ ./test.sh
declare -f one
declare -f two

[/simterm]

If you specify function names as arguments, declare will simply display their names:

#!/bin/bash

one () {
  echo "one"
}

two () {
  echo "two"
}

declare -F one two

Check it:

[simterm]

$ ./test.sh
one
two

[/simterm]

You can set the key -f and a name of the function, then only the body of the specified function will be displayed:

#!/bin/bash

one () {
  echo "one"
}

two () {
  echo "two"
}

declare -f one

Run:

[simterm]

$ ./test.sh
one () {
echo "one"
}

[/simterm]

You can check the presence of functions before executing them using an additional function, and passing names of functions to be checked:

#!/bin/bash

one () {
  echo "one"
}

two () {
  echo "two"
}

isDefined() {
  declare -f "$@" > /dev/null && echo "Functions exist" || echo "There is no some functions!"
}

isDefined one two 

Pay attention to the use of “$@” – as it was written above, it is a parameter that displays the argument “as is”, without any interpretation by bash.

Let’s run the script to check:

[simterm]

$ ./test.sh
Functions exist

[/simterm]

And now, let’s try to add one “extra” function:

#!/bin/bash

one () {
  echo "one"
}

two () {
  echo "two"
}

isDefined() {
  declare -f "$@" > /dev/null && echo "Functions exist" || echo "There is no some functions!"
}

isDefined one two three

Result:

[simterm]

$ ./test.sh
There is no some functions!

[/simterm]

declare detected the absence of the function three, and returned code 1, which caused the ||.