1. Overview

When writing complex logic in Bash scripts, it makes sense to group it in reusable functions.

In this quick tutorial, we’re going to take a look at how to define and use Bash functions.

2. Basics

We can define Bash functions in two ways:

name () compound-command [redirections] 

function name [()] compound-command [redirections]

The function keyword can be omitted only if parentheses are present.

Alternatively, we can also omit the parentheses if we use the function keyword.

The body can be any compound command, while redirections are also optional and performed when the function is executed.

2.1. Defining the Function

We mentioned before that we could define functions in two ways. Let’s look at a quick example:

simple_function() {
    for ((i=0;i<5;++i)) do
        echo -n " "$i" ";
    done
}

simple_function

We call our function by merely invoking its name, but we must define it before executing it.

This trivial example just prints some numbers:

0  1  2  3  4

As we said in the beginning, we can use the function keyword and omit the parentheses:

function simple_function {
    # same body as before
}

Of course, the result is the same.

Notice that we also used the same name for our function. In this case, Bash uses the last function definition found in our script.

Since we said that the body could be any compound command, we can even omit the curly braces:

function simple_for_loop()
    for ((i=0;i<5;++i)) do
        echo -n " "$i" ";
    done

In this example, the output is the same as before.

However, this only works when we’re executing instructions inside the for loop. That’s because the looping construct acts as a compound command.

We can additionally define the body of a function using conditional constructs and command groups.

2.2. Passing Input Arguments

Passing inputs to a function is no different from passing arguments to a Bash script:

function simple_inputs() {
    echo "This is the first argument [$1]"
    echo "This is the second argument [$2]"
    echo "Calling function with $# arguments"
}

simple_inputs one 'two three'

Let’s take a closer look at this example. First, we print the two inputs from positional parameters.

After that, we also print the total number of arguments using a special parameter.

We also escape the second input with quotes so that we avoid word splitting.

Let’s look at the output:

This is the first argument [one]
This is the second argument [two three]
Calling function with 2 arguments

2.3. Getting Outputs

When we execute a function, Bash considers it similar to a command.

This means that a return statement can only signal a numerical exit status with values between 0 and 255.

If we do not return an exit code, then Bash will return the exit status of the last command in our function.

Let’s calculate the sum of two numbers:

sum=0
function simple_outputs() {
    sum=$(($1+$2)) 
}

simple_outputs 1 2
echo "Sum is $sum"

In this snippet, we use a global variable to store the actual result.

As an alternative to this approach, we can rely on command substitution:

function simple_outputs() {
    sum=$(($1+$2)) 
    echo $sum
}

sum=$(simple_outputs 1 2)
echo "Sum is $sum"

Notice that now we’re executing our function in a sub-shell. We’ll explore this a bit later.

Because functions are similar to commands, we can capture their standard output where we have our actual result:

Sum is 3

2.4. Using Argument References

As of Bash 4.3+, we can pass an input argument by reference and then modify its state inside the function:

function ref_outputs() {
    declare -n sum_ref=$3
    sum_ref=$(($1+$2)) 
}

Let’s drill down into this example to understand it better.

First, we declare a nameref variable that stores the third argument’s name.

As a second step, we use this variable as a left-hand-side operand on the assignment operation. We can do this because this variable is a reference to the third argument.

Finally, we call our function by specifying as positional arguments both the inputs and the output:

ref_outputs 1 2 sum
echo "Sum is $sum"

And we’ll see the same result:

Sum is 3

3. Advanced Concepts

Now that we’ve seen the basics, let’s take a look at more advanced concepts and usage scenarios with functions.

3.1. Variables and Scopes

We looked at global variables in our previous examples. We can also define local variables:

variable="baeldung"
function variable_scope() {
    local variable="lorem"
    echo "Variable inside function variable_scope : [$variable]"
}

variable_scope
echo "Variable outside function variable_scope : [$variable]"

Local variables shadow variables with the same name from the calling scope:

Variable inside function variable_scope : [lorem]
Variable outside function variable_scope : [baeldung]

Let’s complicate things further and call another function from our previous function:

variable="baeldung"
function variable_scope2() {
    echo "Variable inside function variable_scope2 : [$variable]"
}
function variable_scope() {
    local variable="lorem"
    echo "Variable inside function variable_scope : [$variable]"
    variable_scope2
}

variable_scope

The output is a bit surprising:

Variable inside function variable_scope : [lorem]
Variable inside function variable_scope2 : [lorem]

Even though we declared a local variable in our variable_scope function, it is still visible in our second function.

This is called dynamic scoping and affects how variables can be seen in nested child scopes.

3.2. Sub-Shells

Remember that we mentioned sub-shells above. A sub-shell is a special type of command group that allows us to spawn a new execution environment from the current shell.

Since the body of the function can be delimited with any command group, we can execute our logic directly in a sub-shell:

sum=0
function simple_subshell() (
    sum_ref=$(($1+$2))
)

simple_subshell
echo "Sum is $sum"

Notice that instead of curly braces, we’re now using parentheses to delimit the function body.

When we run this example, we notice our global variable has not changed:

Sum is 0

Let’s try now with argument references:

sum=0
function simple_subshell() (
    declare -n sum_ref=$3
    sum_ref=$(($1+$2))
)

simple_subshell 1 2 sum
echo "Sum is $sum"

We get the same result. That’s because variable assignments are dismissed when our spawned execution environment finishes.

We can, however, retrieve the standard output stream of a sub-shell using command substitution, as we saw in the previous snippets.

In general, the purpose of a sub-shell is to allow parallel processing of tasks.

3.3. Redirection

In the beginning, we saw that the function definition syntax also allows redirections.

Let’s consider a simple example where we read a file line by line and print its contents:

function redirection_in() {
    while read input;
        do
            echo "$input"
        done
} < infile

redirection_in

In this snippet, we redirect the contents of our test file directly to the standard input of the function.

The read command fetches each line from the standard input.

When we run the function, the output contains a list of car manufacturers along with model and production year:

Honda  Insight  2010
Honda  Element  2006
Chevrolet  Avalanche  2002

We can also redirect the standard output of a function to a file:

function redirection_out() {
    declared -a output=("baeldung" "lorem" "ipsum")
    for element in "${output[@]}"
        do
            echo "$element"
        done
} > outfile

redirection_out

In this case, the output file outfile contains our three elements on separate lines:

baeldung
lorem
ipsum

But what about redirecting to and from other commands? For this purpose, we can use process substitution:

function redirection_in_ps() {
    read
    while read -a input;
        do
            echo "${input[2]} ${input[8]}"
        done
} < <(ls -ll /)

redirection_in_ps

This example reads the folders and their owner from root (/). Let’s take a closer look at what happens:

root bin
root boot
root dev
root etc
# some more folders

The output of the ls command is interpreted as a file through process substitution.

Then, this output is redirected to the standard input of the function, which processes it further.

When we want to redirect the standard output of a function to a command, we just reverse the process substitution operator at line 7:

function redirection_out_ps() {
    declare -a output=("baeldung" "lorem" "ipsum" "caracg")
    for element in "${output[@]}"
        do
            echo "$element"
        done
} > >(grep "g")

redirection_out_ps

This way, we can consider the standard input of grep as a file and redirect our function output to it.

This snippet prints only the lines that contain the letter g:

baeldung
caracg

3.4. Recursion

We can also use recursion with Bash functions. Let’s explore calculating the n-th Fibonacci number:

function fibonnaci_recursion() {
    argument=$1
    if [[ "$argument" -eq 0 ]] || [[ "$argument" -eq 1 ]]; then
        echo $argument
    else
        first=$(fibonnaci_recursion $(($argument-1)))
        second=$(fibonnaci_recursion $(($argument-2)))
        echo $(( $first + $second ))
    fi 
}

Let’s take a closer look to understand it better. The first part of the function handles the first and second Fibonacci numbers.

For all other numbers, we call our function recursively to calculate the two preceding numbers.

We use arithmetic expansion to subtract 1 and 2 from our input argument and, once again, command substitution to retain the result.

Let’s see how this works for calculating the 7th and 15th Fibonacci numbers:

echo $(fibonnaci_recursion 7)
echo $(fibonnaci_recursion 15)
13
610

While for the first call, things run smoothly, we can quickly observe execution becomes quite slow for the second call.

Although recursion is possible with Bash functions, we’re usually better off avoiding it.

We can also limit the number of nested calls of a function by setting the FUNCNEST built-in variable:

FUNCNEST=5
echo $(fibonnaci_recursion 7)
echo $(fibonnaci_recursion 15)
fibonnaci_recursion: maximum function nesting level exceeded (5)
fibonnaci_recursion: maximum function nesting level exceeded (5)
# some more errors

4. Summary

In this article, we explored the practical aspects of using Bash functions. We can use different constructs to define the body of a function and multiple ways to retrieve outputs.

Declaring local variables is possible, but dynamic scoping affects how functions view variables. We can also redirect files and other commands in and from functions and even use recursion.

Overall, Bash functions offer great flexibility and provide a powerful way to organize complex scripts.

As always, the full source code of the tutorial is available over on GitHub.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments