Reusing Code With Functions in Bash

20 min read·Jan 1, 2025

In programming, a function is a reusable code structure that allows you to regroup and encapsulate multiple instructions that are part of the same logic.

It allows you to re-run these instructions multiple times from different parts of a script, without manually repeating them.

In short, you can think of a function as a mini-script within the script.

Defining functions

To define a function in Bash, you can use the following syntax:

function_name () {
  instructions
}

Where:

  • function_name is the name of the function.
  • instructions are the set of instructions executed by the function.

Note: The instructions specified between the curly braces {} are called the body of the function.

Example

Let's consider this script, that declares a function with a single instruction:

#!/bin/bash

hello () {
  echo "Hello, World!"
}

When executed, it will:

  1. Define a function named hello.
  2. Declare in the body of the function a single instruction that outputs the string "Hello, World!".

Invoking functions

Just like any other available Bash commands, functions can be invoked (executed) by their name anywhere in the script, including within other functions:

function_name () {
  instructions
}

function_name

Note: In Bash, functions must be declared before they are used.

Example

Let's consider this script, that executes a function:

#!/bin/bash

hello () {
  echo "Hello, World!"
}

hello

When executed, it will:

  1. Define a function named hello.
  2. Declare in the body of the function a single instruction that outputs the string "Hello, World!".
  3. Invoke the hello function.

Which will produce this output:

$ ./script.sh
Hello, World!

Passing arguments to functions

Similar to a script's positional arguments, function arguments are values passed to a function when invoked, such as strings, numbers, or arrays, allowing it to perform specific tasks or computations based on these values.

function_name value1 ... valueN

These arguments are then made accessible within the function's body using the same syntax as for positional arguments $N:

function_name () {
  instruction $1 ... $N
}

Note: While positional arguments and function arguments share the same syntax $N, positional arguments are not accessible within the body of a function unless they are explicitly passed as function arguments.

Example

Let's consider this script, that takes two numbers and output their sum:

#!/bin/bash

add () {
  result=$(($1 + $2))
  echo $result
}

add $1 $2

When executed, it will:

  1. Define a function named add.
  2. Store the sum of its parameters in a variable named result using an arithmetic expansion.
  3. Output the value of the result variable.
  4. Execute the add function using as function arguments the first two positional arguments of the script.

Which will produce this output:

$ ./script.sh 1 2
3
$ ./script.sh 6 3
9

Setting a custom exit status

In Bash, the exit status of a function (or a command) is an integer value that represents the success or failure of the function's execution.

It is used to indicate whether the function completed its task as expected or encountered an error.

It allows the calling code to make decisions based on the outcome of the function and is accessible through the $? variable.

By convention, an exit status of 0 indicates that the function successfully completed without errors, and a non-zero exit status indicates that something went wrong.

Note: By default, a function's exit status is set to the value of the exit status of the last instruction executed by the function.

To set a custom exit status, you can use the return statement:

fname () {
  instructions
  return status
}

Whenever encountered, the return statement will immediately terminate the execution of the function.

In some cases, this behaviour can be leveraged to simplify the logic of a function by performing an early return instead of setting up an overly complex if-else structure.

Example

Let's consider this script, that uses the is_even function to determine whether an integer is even or odd:

#!/bin/bash

is_even () {
  if [[ $(($1 % 2)) -eq 0 ]]; then
    echo "$1 is even"
    return 0
  else
    echo "$1 is odd"
    return 1
  fi
}

is_even 1
echo "Exit status is $?"

is_even 2
echo "Exit status is $?"

When executed, it will:

  1. Define a function named is_even.
  2. Check if the remainder of the division of the function parameter by 2 equals 0.
  3. If true, output the value of the function parameter concatenated to the string "is even" and set the function's exit status to 0.
  4. If false, output the value of the function parameter concatenated to the string "is odd" and set the function's exit status to 1.
  5. Invoke the is_even function by passing it 1 as parameter.
  6. Output the exit status of the function.
  7. Invoke the is_even function by passing it 2 as parameter.
  8. Output the exit status of the function.

Which will produce this output:

$ ./script.sh
1 is odd
Exit status is 1
2 is even
Exit status is 0

Note:

Since the return statement immediately terminates the execution of the function, we can simplify the is_even function by removing the unnecessary else statement, which will produce the same result:

is_even () {
  if [[ $(($1 % 2)) -eq 0 ]]; then
    echo "$1 is even"
    return 0
  fi
  echo "$1 is odd"
  return 1
}

Capturing the output of a function

Unlike in other programming languages, Bash functions cannot directly return data, such as strings, numbers, or arrays.

However, it is possible to capture the data written by function or command to the standard output into a variable using the command substitution expansion $().

output=$(command)

Example

Let's consider this script, that outputs the sum of two integers:

#!/bin/bash

add () {
  echo $(($1 + $2))
}

sum=$(add 1 2)
echo $sum

When executed, it will:

  1. Define a function named add that outputs the sum of its first two function parameters.
  2. Store the output of the add function's execution into a variable named sum using the command substitution expansion.
  3. Output the value of the sum variable.

Which will produce this output:

$ ./script.sh
3

Variable scopes

In programming, the scope of a variable refers to the area of the code where a variable can be accessed and used.

In Bash, there are two main types of variable scope: global and local.

Global variables

Global variables are variables that are accessible and modifiable from anywhere in the script, including within functions.

They are usually declared and assigned values outside of any function or structure, at the top of the script.

They are generally used to store values that need to be shared across functions or for data that needs to maintain its state throughout the entire script, referred to as constants.

Example

Let's consider this script, that increments a global variable in different ways:

#!/bin/bash

count=0

increment () {
  count=$((count + 1))
}

increment

count=$((count + 1))

echo "\$count = $count"

When executed, it will:

  1. Declare a global variable named count and initialize it to 0.
  2. Declare a function named increment that when invoked increases the value of the variable count by 1.
  3. Invoke the increment function.
  4. Increase the value of the count variable by 1.
  5. Output the value of the count variable.

Which will produce this output:

$ ./script.sh
$count = 2

Local variables

Local variables are variables that are only accessible and modifiable from within the function they are defined in.

They are used to store temporary values that should be isolated from the rest of the script, and only exist for the function's execution duration.

They are declared using the local keyword.

function () {
  local variable=value
}

Note: Local variables do not affect or interact with variables of the same name in the global scope or other functions. If a local variable shares the same name as a global variable, the local variable takes precedence within the function, which is known as variable shadowing.

Example

Let's consider this script, that illustrates variable shadowing using a global and a local variable with the same name:

#!/bin/bash

result=42

multiply () {
  local result=$(($1 * $2))
  echo "(local) \$result = $result"
}

multiply 3 2

echo "(global) \$result = $result"

When executed, it will:

  1. Declare a global variable named result and initialize it to 42.
  2. Define a function named multiply that multiplies its first two function parameters, stores their product in a local variable named result, and outputs the value of the variable result.
  3. Invoke the multiply function with the values 3 and 2 as parameters.
  4. Output the value of the global result variable.

Which will produce this output:

$ ./script.sh
(local) $result = 6
(global) $result = 42

Overriding existing commands

In Bash, you can override existing commands by creating a custom function with the same name but a different implementation.

When executed, this function will take precedence over the original command and be executed instead of it.

This mechanism is often referred to as command hijacking or function overloading.

shell_command () {
  # new implementation
}

Note:

To invoke a shell command within a shell function of the same name, you have to invoke it using the command command, as otherwise, your script will end up in an infinite call loop.

shell_command () {
  command shell_command ...
}

Example

Let's consider this script, that overrides the shell's ls command:

#!/bin/bash

ls () {
  echo "Executing the ls function..."
  command ls -l $@
}

ls ~

When executed, it will:

  1. Define a function named ls.
  2. Invoke the shell's ls command with the -l flag and all the function parameters supplied to the function.
  3. Invoke the ls function with the tilde expansion ~.

Which will produce this output:

$ ./script.sh
Executing the ls function...
total 400
drwx------@   7 razvan  staff     224 Oct 26 21:21 Desktop
drwx------@  14 razvan  staff     448 May 27 10:38 Documents
drwx------@  29 razvan  staff     928 Oct 26 15:15 Downloads
drwx------@ 105 razvan  staff    3360 May 29 07:02 Library
drwx------   20 razvan  staff     640 Aug 30 16:20 Projects
drwxr-xr-x+   4 razvan  staff     128 Jun 14  2018 Public

Summary

Here's a summary of what you've learned in this lesson:

  • A function is a reusable block of code that performs a specific task or set of tasks.
  • A function is defined using the syntax function_name () { instructions }.
  • A function is invoked by its name like any other command.
  • A function can take zero or more arguments like any other command.
  • The exit status of a command is set using the return statement.
  • The output of a command can be captured using a command expansion $(command).
  • Global variables are accessible and modifiable from anywhere in the script.
  • Local variables are declared using the readonly keyword and are only accessible and modifiable from within the function they are defined in
  • A function can override another function or command by being declared under the same name.

Enjoying the courses?

I've made these courses completely free so anyone can learn from them. If they've helped you and you'd like to actively support the work behind BackendBrewery, you can leave a tip:

Support BackendBrewery
Reusing Code With Functions in Bash | Backend Brewery