Bash functions

Bash Functions: Make Your Scripts Reusable [With Examples]

If you are working with Linux at some point you will likely start writing Bash scripts. The more your code will grow the more you will realise how useful Bash functions can be.

What is a Bash function?

Functions are used in Bash scripts, as in other programming languages, to group code in a simpler and more reusable way. A function takes one or more input arguments and provides an exit code or value to the main script. Functions help reduce repetitive code and speed up your scripting.

In this tutorial we will go through the definition of a Bash function and you will see how to use functions in your scripts to improve the way they are written.

Let’s start coding!

How Do You Define a Function in a Shell Script

The syntax you use to define a function in a shell script is:

function <function_name> { 
    <function_code>
}

The code between brackets is called body of the function. The structure of the body depends on what you want to do with that function.

It might contain just a couple of commands or very complex logic mixing variable declarations, commands and statements.

There is also a second way to define functions in Bash:

<function_name>() {
    <function_code>
}

These are the changes compared to the first function definition syntax:

  • Remove the word function before the name of the function.
  • Add parentheses () after the name of the function.

You can choose the option you prefer.

As a general rule, if you see that your code starts becoming repetitive then it’s time to start using functions.

How to Call a Bash Function

Defining a function is not enough to execute it, you also have to call it in your script.

You can call a Bash function by simply using it’s name followed by zero or more parameters.

For example, I will define a function that prints the current date and then I will call it in our script:

function show_current_date {
    date
}

show_current_date 

In the code above I have defined a function called show_current_date that simply calls the date command. Then I call it by using the function name.

$ ./bash_function.sh 
Sat 30 Jan 2021 09:14:59 GMT 

The function definition needs to happen before you call that function in your script, otherwise your script won’t work. I will show what would happen in that scenario later on in this tutorial.

And now, let’s have a look at how we can make our functions a bit more useful…

Passing an Argument to a Function

In the same way you pass arguments to a Bash script, you can also pass arguments to a function.

And in the same way you can retrieve the value of the arguments passed to the function by using $1 for the first argument, $2 for the second argument, etc…

In the following example I will improve the show_current_date function to pass one argument that defines the date format.

function show_current_date {
    echo $1
    date $1
} 

show_current_date "+%Y-%m-%d"

In the function I use the echo command to show the value of the first argument received by the function (the format of the date).

Then I pass the first argument ($1) to the date command.

You can also see how the call to the function changes because here we are passing the date format to the function.

The output is:

$ ./bash_function.sh 
+%Y-%m-%d
2021-01-30 

Passing Multiple Arguments to a Function

In the next example we will look at how to pass multiple arguments to a Bash function.

Let’s create a function that calculates the sum of three numbers:

#!/bin/bash
   
function sum_numbers() {
    echo "The numbers received are: $1 $2 $3"
    sum=$(($1+$2+$3))
    echo "The sum of the numbers is $sum"
}

sum_numbers 3 6 22 

The last line of the script calls the function passing three parameters whose value is stored in the function arguments $1, $2 and $3 based on their order.

$ ./bash_function.sh 
The numbers received are: 3 6 22
The sum of the numbers is 31 

Get the Number of Function Arguments in Bash

We can improve our function by also printing the number of arguments received by the function.

The number of arguments received by a function is stored in the variable $#.

function sum_numbers() {
    echo "The number of arguments received is: $#"
    echo "The numbers received are: $1 $2 $3"
    sum=$(($1+$2+$3))
    echo "The sum of the numbers is $sum"
} 

The output is:

$ ./bash_function.sh 
The number of arguments received is: 3
The numbers received are: 3 6 22
The sum of the numbers is 31

How to Get the Last Function Argument in Bash

What if we want to get just the value of the last argument?

The variable ${@: -1} returns the value of the last argument passed to a function.

function sum_numbers() {
    echo "The number of arguments received is: $#"
    echo "The value of the last argument is: ${@: -1}"
    echo "The numbers received are: $1 $2 $3"
    sum=$(($1+$2+$3))
    echo "The sum of the numbers is $sum"
}

Run the script after updating the function, you will see the following:

$ ./bash_function.sh 
The number of arguments received is: 3
The value of the last argument is: 22
The numbers received are: 3 6 22
The sum of the numbers is 31

Makes sense?

How to Handle Arguments with Spaces

What happens if one or more function arguments contain spaces?

Let’s find out…

I want to pass the string “Bash shell” to the print_argument function that prints the value of the first argument $1.

#!/bin/bash
   
function print_argument {
    echo $1
} 

print_argument "Bash shell" 

To make sure the string we are passing is interpreted as a single argument I have surrounded it with double quotes.

$ ./bash_function.sh 
Bash shell 

The output is correct.

What do you think would happen if I remove the double quotes around the string?

The Bash interpreter would see Bash and shell as two separate parameters passed to the function. So, the function would only print the string Bash ($1).

Setting Arguments with a Default Value

If you want to have default values for function arguments in case their value is not set when calling the function, you can do the following…

${<argument_number>:-<default_value>}

${1:-test1} # For argument 1
${2:-test2} # For argument 2
...
${N:-testN} # For argument N

Let’s go through an example together…

Following the syntax above for default values we can define a new function:

#!/bin/bash
   
function print_arguments {
    ARG1=${1:-default_value1}
    ARG2=${2:-default_value2}
    ARG3=${3:-default_value3} 

    echo "The first argument is: $ARG1"
    echo "The second argument is: $ARG2"
    echo "The third argument is: $ARG3"
}

print_arguments $@ 

By using the variable $@ in the function call we are passing to the function all the command line arguments passed to our Bash script.

It’s time to test this function by passing a different number of arguments to the script:

$ ./default_arguments.sh 
The first argument is: default_value1
The second argument is: default_value2
The third argument is: default_value3 

You can see that default arguments are printed depending on the number of parameters passed to the function call:

$ ./default_arguments.sh
The first argument is: default_value1
The second argument is: default_value2
The third argument is: default_value3 

$ ./default_arguments.sh 1
The first argument is: 1
The second argument is: default_value2
The third argument is: default_value3 

$ ./default_arguments.sh 1 2
The first argument is: 1
The second argument is: 2
The third argument is: default_value3 

$ ./default_arguments.sh 1 2 3
The first argument is: 1
The second argument is: 2
The third argument is: 3

Now you also know how to use default values in your Bash functions 🙂

Passing Arguments to a Function Using a Bash Array

I was curious to find out how to pass arguments to a function using a Bash array.

I have to admit…

…it’s not one of the easiest things to do in Bash and I wouldn’t use it unless you don’t have any alternatives.

Have a look at the script below:

#!/bin/bash
   
function print_arguments {
    local arguments_array=("$@")
    echo "This is the array received by the function: ${arguments_array[@]}"

    ARG1=${arguments_array[0]}
    ARG2=${arguments_array[1]}
    ARG3=${arguments_array[2]}

    echo "The first argument is: $ARG1"
    echo "The second argument is: $ARG2"
    echo "The third argument is: $ARG3"
} 

arguments=("$@")
print_arguments "${arguments[@]}" 

I will explain this script step-by-step:

  • Generate an array from the arguments passed to the script using arguments=(“$@”).
  • Pass all the elements of the arguments array to the print_arguments function (print_arguments “${arguments[@]}”). To understand this better learn more about Bash arrays.
  • At the beginning of the print_arguments function we create a local array that contains all the values passed to the function.
  • Then we print all the values in the array arguments_array to confirm the arguments in the array have been received by the function (it’s very easy to make mistakes when passing array variables around).
  • Assign the first, second and third array elements to the variables ARG1, ARG2 and ARG3.

Try to write this script from scratch, it’s one of the best ways to remember the syntax we have used.

And if you have any questions please post them in the comments below.

Now that we have seen how input arguments work, let’s move to returning values from functions.

How to Use $? as Return Value

Bash doesn’t provide a basic way for a function to return a value in the same way most programming languages do.

By default a function “returns” the exit code of the last statement executed in the function.

The exit code is stored in the variable $? and its value can be verified after the function call, for example using a Bash if else statement.

To see this in practice we will start from code similar to the one in the previous section:

function show_current_date {
    date $1
} 

show_current_date "+%Y-%m-%d"
echo $?

Notice that we have added an echo command that prints the value of $? after the function call.

The output is:

$ ./bash_function.sh 
2021-01-30
0 

The status code coming from the function is zero because the last statement in the function is successful.

Let’s see what happens if we update the date command with an incorrect syntax (the rest of the code doesn’t change):

function show_current_date {
    date - $1
}

In the output you can see the error and the return code that this time is equal to 1 (in Bash 0 is a success and anything else represents a failure):

$ ./bash_function.sh 
date: illegal time format
usage: date [-jnRu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] ... 
             [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]
 1 

Using the Return Statement in a Bash Function

Bash also provides a return keyword.

The return keyword terminates the function and assigns the return value to the variable $?.

function show_current_date {
    date $1
    return 3
} 

show_current_date "+%Y-%m-%d"
echo $?

The output confirm that the value 3 is assigned to $?.

$ ./bash_function.sh 
2021-01-30
3 

Wondering what happens if we return a string instead?

function show_current_date {
    date $1
    return "value"
} 

We get back the following error and a 255 exit code.

2021-01-30
./bash_function.sh: line 5: return: value: numeric argument required
255 

Remember that non-zero return codes indicate a failure.

How to Return a String From a Bash Function

There is a way to return a string from a function, it’s not straightforward as in other programming languages though.

function get_operating_system {
    local result="Linux"
    echo "$result"
}

operating_system=$(get_operating_system)
echo $operating_system

Let’s go through this code. We have…

  • Defined the function get_operating_system that assigns a value to the local variable result and then outputs its value.
  • Used command substitution to call the function and store the value printed by the function into the variable operating_system.

Run this script on your machine and confirm that the last echo command of the script prints the string “Linux”.

Now you have several ways to get a value back from a function 🙂

Bash Functions and Global Variables

Understanding how the scope of variables works in Bash is important for you to avoid potential bugs in your scripts.

By default variables in Bash scripts are global.

Here is what I mean:

#!/bin/bash
   
MESSAGE="original message" 

update_message() {
    MESSAGE="updated message"
} 

echo "Message before function call: $MESSAGE"
update_message
echo "Message after function call: $MESSAGE"  

The output is:

Message before function call: original message
Message after function call: updated message 

You can see that we have:

  • Assigned a value to the global variable MESSAGE at the beginning of the script.
  • Defined a function that updates the value of the same variable MESSAGE.
  • Printed the value of the variable MESSAGE before and after calling the function.

Inside the function we were able to update the global variable MESSAGE, this confirms that variables are global by default in Bash.

In the next section we will see what other option we have when it comes to the scope of variables in Bash…

Local Variables in Bash Functions

Using global variables in functions can be quite risky because by doing that you might accidentally update a global variable in a function when that variable is also used somewhere else in the script…

…especially if you have hundreds of lines of code to keep track of.

Using the keyword local Bash allows to set variables that are local to a function.

Let’s make the variable MESSAGE local in the function we have defined in the previous script to see what happens:

#!/bin/bash
   
MESSAGE="original message" 

update_message() {
    local MESSAGE="updated message"
} 

echo "Message before function call: $MESSAGE"
update_message
echo "Message after function call: $MESSAGE"

This time the output doesn’t change because the local variable in the function is different from the global variable defined at the beginning of the script:

Message before function call: original message
Message after function call: original message 

You can also have a local variable with the same name in multiple functions. Each local variable is completely independent.

Command Not Found Error for Bash Functions

At the beginning of this tutorial I have mentioned that in Bash it’s important to define a function before you use it.

Notice the order of the function call and function definition in the script below:

show_current_date "+%Y-%m-%d"

function show_current_date {
    echo $1
    date $1
}

Here is what happens if you try to use a function in your script before defining it:

./bash_function_order.sh: line 3: show_current_date: command not found 

Move the function call at the end of the script and confirm that the script works as expected.

There is also another type of error you might see when you define functions using the function keyword (remember that at the beginning of this tutorial we have seen that you can define functions in two different ways).

Use Functions with Korn Shell and C Shell

The keyword function doesn’t apply to all types of shells.

Have a look at what happens if we try to execute the same script using Korn Shell (ksh) and C Shell (csh).

Using Korn Shell

$ ksh bash_function_order.sh 
+%Y-%m-%d
2021-01-31 

The script runs successfully.

Using C Shell

$ csh bash_function_order.sh 
function: Command not found. 

Sun 31 Jan 2021 13:41:02 GMT
}: Command not found.
show_current_date: Command not found. 

When we execute the script using the C Shell we get the following error back:

function: Command not found.

That’s because the C Shell doesn’t understand the keyword function.

We can try to use the function definition that doesn’t use the function keyword to see what happens:

show_curent_date() {
    echo $1
    date $1
} 

show_current_date "+%Y-%m-%d" 

This time we get a different error:

$ csh bash_function_order.sh 
Badly placed ()'s. 

The reason about these two errors using a function in a csh script is that C Shell doesn’t support functions.

Define a Function on a Single Line

Let’s take the following function we have defined in one of the sections above:

function show_current_date {
    echo $1
    date $1
} 

I want to see if we can write it in a single line. This is not necessarily what we would do in a real script, but it’s useful to understand as much as we can about the way Bash works.

First of all we simply write the two commands in the body of the function next to each other and separate them by a space:

show_current_date() { echo $1 date $1 }

The function doesn’t work anymore:

$ ./bash_function.sh 
./bash_function.sh: line 6: syntax error: unexpected end of file 

Why do we see the message “syntax error: unexpected end of file“?

To make this work we need to add a semicolon after each command inside the function (including the last one):

show_current_date() { echo $1; date $1; } 

Update the function on your machine and verify that it works fine.

Function Documentation

Adding documentation to functions helps clarify what a function does, what arguments it expects and how it returns a value.

This can help both someone who has written that function long time after writing it and also someone who is completely new to the code.

I haven’t found an official standard to document functions but there are few things that are important to understand a function:

  • What the function does.
  • The arguments it expects and their format (e.g. numbers, strings, arrays).
  • The value the function returns to the main script.

Let’s take as an example the following function:

function get_operating_system {
    local result="Linux"
    echo "$result"
}

operating_system=$(get_operating_system)
echo $operating_system

We can add three comments at the top of the function to explain the three points above.

# Description: Function that returns the current Operating System
# Arguments: No arguments
# Returns: Name of the Operating System
function get_operating_system {
....

You can see how our code becomes a lot easier to read by simply adding this documentation at the top of the function.

We could also add a fourth command that shows how to call it…

# Usage: operating_system=$(get_operating_system)

This is something you can customise depending on what you think it’s useful to explain about the functions you write.

Function Error Handling in Bash

Error handling helps you detect errors that occur during the execution of your scripts in order to stop their execution when an unexpected condition is encountered.

This concept applies to Bash scripts in general but in this section we want to focus on error handling applied to Bash functions.

Two of the approaches to handle errors in functions are:

  • Checking the number of arguments passed to a function to confirm it’s the expected one.
  • Verifying the value of the variable $? after the execution of statements in your function.

Let’s take the function we have created before to calculate the sum of three numbers:

function sum_numbers() {
    echo "The number of arguments received is: $#"
    echo "The numbers received are: $1 $2 $3"
    sum=$(($1+$2+$3))
    echo "The sum of the numbers is $sum"
}

Firstly, I want to make sure the function receives exactly three numbers. If it doesn’t the execution of our script stops.

To do that we can use a Bash if statement that stops the execution of the function, using the exit command, if the number of arguments is different than three.

#!/bin/bash
   
function sum_numbers() {
    if [ $# -ne 3 ]; then
        echo "The number of arguments received is $# instead of 3"
        exit 1
    fi

    echo "The numbers received are: $1 $2 $3"
    sum=$(($1+$2+$3))
    echo "The sum of the numbers is $sum"
}

sum_numbers "$@" 

See what happens when I run this script passing zero or three arguments:

$ ./function_error_handing.sh 
The number of arguments received is 0 instead of 3

$ ./function_error_handing.sh 2 7 24
The numbers received are: 2 7 24
The sum of the numbers is 33 

When we pass zero arguments to the script the condition of the if statement in the function is true and the execution of the script stops.

Using $? For Handling Errors in a Function

The following example shows how we can use the variable $? to handle errors in a function.

I have created a simple script that takes the name of a file as input and prints a message that contains the number of lines in the file.

#!/bin/bash
   
function count_lines {
    local count=$(wc -l $1 | awk '{print $1}')
    echo "$count"
} 

number_of_lines=$(count_lines $1)
echo "The number of lines is $number_of_lines" 

Here is the output of the script (I have created a file called testfile in the same directory of the script. This file contains three lines):

$ ./function_error_handing.sh testfile
The number of lines is 3 

Let’s see what happens if I pass the name of a file that doesn’t exist:

$ ./function_error_handing.sh testfile1
wc: testfile1: open: No such file or directory
The number of lines is  

The script doesn’t handle this very well. It shows the error from the wc command and then a partial message due to the fact that the function returns an empty value.

It’s a bit messy!

What can we do to improve the user experience?

Here is the update script, we will go through all the updates step by step:

#!/bin/bash
   
function count_lines {
    ls $1 > /dev/null 2>&1 

    if [ $? -ne 0 ]; then
        exit 1
    fi

    local count=$(wc -l $1 | awk '{print $1}')
    echo "$count"
} 

number_of_lines=$(count_lines $1)

if [ $? -eq 0 ]; then
    echo "The number of lines is $number_of_lines"
elif [ $? -eq 1 ]; then
    echo "Unable to detect the number of lines, the file $1 does not exist"
else
    echo "Unable to detect the number of lines"
fi 

There are many different options to detect errors in this scenario. Here is one option:

  • Use the ls command at the beginning of the function to check if the filename passed as first argument exists.
  • Hide the output of the ls command by redirecting standard output and standard error to /dev/null.
  • Verify if the ls command is successful using the variable $? (0 = success, 1 = failure).
  • In case of failure use exit 1 to return an exit code that we can use in the main script to give the user details about the type of error (so exit code 1 refers to a file that doesn’t exist and we could use other exit codes for other types of errors).
  • The main script prints the correct message depending on the value of $? that contains the status of the statement number_of_lines=$(count_lines $1).

This is just one way to do this, the important thing is for you to understand the thinking behind this code.

Can a Function Be Empty?

Let’s have a look at something that might happen if you define a function in a big script and for some reason you forget to implement the body of the function.

We will define an empty function…

…a function that doesn’t contain any code:

empty_function() {
   
} 

I’m not even calling this function in my script.

Here is the error we get when I execute the Bash script:

$ ./bash_function.sh 
./bash_function.sh: line 3: syntax error near unexpected token `}'
./bash_function.sh: line 3: `}' 

Just something to be aware of in case you see the message “syntax error near unexpected token” while working with functions.

Conclusion

Now you know how to define Bash functions, how to read arguments passed to them and how to return a value to your script from a function.

Have a look at your scripts and start thinking:”Where can you reduce duplication and complexity by using functions?”

Share knowledge with your friends!

Leave a Reply

Your email address will not be published. Required fields are marked *