Functions in Bash Scripting

In this article, we will cover the basics of how to define, call, pass arguments to, and return values from bash functions. We will also look at recursive functions.
Functions in Bash Scripting

On this page

Functions are an essential part of bash scripting. Just like functions in traditional programming languages, Bash functions allow you to modularize your code by encapsulating sections of code with related functionality into reusable units, thereby reducing the amount of code and increasing readability.

In this article, we will cover the basics of how to define, call, pass arguments to, and return values from bash functions. We will also look at recursive functions.

Displaying Existing Functions

Before we dive into creating our own functions, it can be useful to see what functions already exist in your shell environment or current Bash session. The declare -F command is invaluable for this purpose. Executing this command in your terminal displays a list of all functions currently defined, providing a snapshot of the available functionality:

declare -F

This command outputs both the names functions you have defined in your scripts as well as any builtin or sourced functions, allowing you to quickly ascertain the functions at your disposal.

Defining and Calling Functions

In Bash, there are two main ways to define a function. The first is by using the function_name () syntax, followed by a block of commands enclosed in braces {}. Here's a simple example:

greet() {
	name="Bob"
  echo "Hello, $name!"
}

This defines a function named greet .

You can call your function from anywhere in your script by using its name followed by any required arguments. In this case, our function doesn't require any arguments. We will discuss function arguments later.

greet

This would output Hello, Bob!.

You call bash functions just like scripts or commands to execute the code they contain on demand.

The second method is by specifying the function keyword before the function name:

function greet() {
	name="Bob"
  echo "Hello, $name!"
}
💡
The method without function keyword is more common in bash scripts.

Passing Values to Functions

Functions in Bash can accept arguments, which are passed by including them after the function name, separated by spaces. Inside the function, these arguments are accessible as $1, $2, and so forth, corresponding to their position:

add_numbers() {
  echo "$(($1 + $2))"
}

add_numbers 3 4  # Outputs 7

Returning Values from Functions

Bash functions don't support return values like other standard programming languages. They typically return a status code (0 for success and non-zero for failure), but you can use them to return arbitrary values by echoing the desired value and capturing it when the function is called:

get_name() {
  echo "Alice"
}

name=$(get_name)
echo $name  # Outputs "Alice"

Notice that we used command substitution when calling the function. As previously mentioned, this is because functions can be called just like commands.

For returning status codes, use the return statement:

is_even() {
  if [ $(($1 % 2)) -eq 0 ]; then
    return 0  # Success
  else
    return 1  # Failure
  fi
}

is_even 4 && echo "Even" || echo "Odd"  # Outputs "Even"

Recursive Functions

Recursion in programming is a technique where a function calls itself to solve a problem. This method breaks down a problem into smaller instances of the same problem, eventually reaching a condition where the problem can be solved without further recursion.

In Bash scripting, recursive functions can be particularly useful for tasks that involve iterative processes or traversing hierarchical structures, such as file directories.

To illustrate recursion in Bash, let's consider a classic example: calculating the factorial of a number. The factorial of a number n (denoted as n!) is the product of all positive integers less than or equal to n. For instance, 5!=5×4×3×2×1=120.

The recursive definition of a factorial is:

  • 0!=10!=1 (base case)
  • For n > 0, n! = n * (n-1)!

This definition directly translates into a recursive function in Bash:

factorial() {
    if [ $1 -le 1 ]; then
        echo 1
    else
        # Recursive step: n * factorial(n-1)
        echo $(( $1 * $(factorial $(($1 - 1))) ))
    fi
}

# By the way, I have got a distinction in Advanced Pure 
# Mathematics.

Here is a more verbose code:

factorial() {
  if [[ $1 == 1 ]]; then
    echo 1
  else
    local temp=$[ $1 - 1 ]
    local result=$(factorial $temp)
    echo $[ $result * $1 ]
  fi
}

Since the second code is easier to understand, I'll use it for a step-by-step explanation to help you fully grasp recursion.

1. Base Case

Every recursion needs a base case - a condition where it does NOT call itself again. This breaks the chain of recursion.

Our base case is n = 1, which just returns 1.

if [[ $n == 1 ]]; then
  echo 1

2. Recursive Case

This is where the function calls itself.

We decrease n by 1 each time using a variable called temp.

Then call factorial() again with the decreased temp, storing the result:

else
  local temp=$[$n - 1]
  local result=$(factorial $temp)

3. Result

After the recursive call returns, we echo the result multiplied by current n:

  echo $[ $result * $n ]

By continuing to call factorial and passing the decreasing number each time, we calculate the final factorial using the logic we defined.

Let's see the full code:

factorial() {
  if [[ $1 == 1 ]]; then
    echo 1
  else
    local temp=$[ $1 - 1 ]
    local result=$(factorial $temp)
    echo $[ $result * $1 ]
  fi
}

When we call factorial 5, the recursion executes until temp reaches 1, then unwinds the stack calculating 5 * 4 * 3 * 2 * 1 = 120. Powerful technique innit?

While it takes some visualization, recursion allows solving certain problems elegantly with functions that call themselves repeatedly. Get the base case and recursive reduction steps right, and you can leverage this technique for compact yet complex logic in your scripts.

⚠️
When writing recursive functions, it's crucial to ensure that the base case is well-defined and reachable; otherwise, the recursion could lead to infinite loops. Additionally, due to the nature of Bash and its limited arithmetic capabilities, recursion in Bash is generally less efficient and can be slower for complex tasks compared to other programming languages, especially for tasks requiring deep recursion levels.

The Bash Fork Bomb

While Bash functions are incredibly useful for structuring and reusing code, they can also be used to create malicious scripts, such as a fork bomb. A fork bomb is a type of denial-of-service (DoS) attack that exploits the system’s process creation mechanism to overwhelm and effectively crash the system.

Understanding the Fork Bomb

A fork bomb works by creating a function that calls itself recursively without a base case, continuously spawning new processes. This rapidly consumes all available process slots and CPU resources, leading to a complete system freeze where no new processes can be initiated, and existing processes fail to execute.

Here’s a classic example of a Bash fork bomb:

:(){ :|:& };:

Let's break it down:

  • :() defines a function named : (a single colon).
  • { :|:& } tells the function to call itself (:), pipe the output to another instance of itself (:), and run in the background (&).
  • ;: finally calls the function, triggering the loop.

Once executed, this script starts an exponential process creation loop, eventually consuming all system resources.

Mitigating a Fork Bomb

Mitigating the impact of a fork bomb involves limiting the number of processes a user can create. This can be done using the ulimit command, which restricts system resources available to the shell and its child processes.

To limit the maximum number of processes a user can spawn, you can set the following in your shell:

ulimit -u 100

This command restricts the user to a maximum of 100 processes. If the fork bomb tries to exceed this limit, the system will prevent it from doing so, protecting the rest of the system from being overwhelmed.

Example of a Fork Bomb in C

The concept of a fork bomb isn’t limited to Bash scripts. Here’s an example of how it can be implemented in C:

#include <unistd.h>

int main() {
    while(1) {
        fork();
    }
    return 0;
}

This C program continuously calls fork() in an infinite loop, creating child processes without end, which can overwhelm the system just like the Bash fork bomb.

Why Fork Bombs Happen

Fork bombs exploit the system’s ability to create processes. In a typical scenario, forking is a normal operation used to create child processes for multitasking. However, when done without restraint—especially in a recursive manner—it can lead to resource exhaustion, which is the core of the problem.

Conclusion

Bash functions are crucial for bringing structure and reusability to your scripts. Knowing how to define, call, and manipulate these functions can lead to more advanced and modular scripts. The key to mastering Bash functions lies in practice, so integrate them into your scripts. Whether you're writing simple utility scripts or complex automation tasks, Bash functions will undoubtedly be an invaluable tool in your scripting toolkit.

Subscribe to sysxplore newsletter and stay updated.

Don't miss anything. Get all the latest posts delivered straight to your inbox. It's free!
Great! Check your inbox and click the link to confirm your subscription.
Error! Please enter a valid email address!