Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 31

Bash Functions: Definition and Invocation

Recommended resources

Introduction

Hello! In our last lesson, you built a cleanup.sh script using for and while loops to automate archiving files. You saw how loops allow you to perform actions on multiple items, which is a big step in automation.

As your scripts grow, you'll notice that you might have distinct blocks of logic—one part to set up directories, another to archive logs, and another to archive temp files. If you need to perform the same setup or archiving task in multiple scripts, you'd have to copy and paste that code. This is where functions come in.

In this lesson, you will learn to define and call functions within a Bash script. This skill is fundamental to writing clean, reusable, and maintainable code, a principle that applies whether you're writing a system administration script in Bash or a web application in Python. By organizing your script's logic into functions, you make it more modular and easier to understand, just like organizing Python code into functions and classes.


1. Why Use Functions? The "Don't Repeat Yourself" Principle

In programming, a core principle is "Don't Repeat Yourself" (DRY). Functions are the primary tool for achieving this. Instead of writing the same sequence of commands multiple times, you can package them into a function and simply call it by name whenever you need it.

This not only saves typing but also makes your scripts much easier to maintain. If you need to change the logic, you only have to update it in one place—the function definition—rather than finding every place you copied the code.

Let's watch a video that demonstrates this concept by refactoring a script that has repetitive code.

How To Write Bash Scripts In Linux - Complete Guide (Part 12 - Functions)

This video from Learn Linux TV shows a practical example of a script with several identical if statements. The presenter then introduces a function to consolidate this repeated logic, making the script shorter and cleaner.

Watch from the beginning (00:06) to 07:28. Pay attention to how a repetitive block of code is identified and then moved into a function named check_exit_status. Notice how calling the function is much cleaner than rewriting the if statement multiple times.

As you saw, using a function resulted in:

  • Readability: The main part of the script now clearly shows its intent: run a command, then check the status.
  • Maintainability: To change how the exit status is checked, you only need to edit the function in one place.
  • Reusability: You can now use the check_exit_status function after any command in your script.

2. Defining and Calling Functions

There are two common ways to define a function in Bash. We'll focus on the most widely used and portable syntax.

Bash Functions

The article 'Bash Functions' from Linuxize provides a clear, concise overview of the syntax for defining functions. It's a great reference to have.

Read the sections 'Defining Bash Functions' and 'Example: Hello World Function'. Focus on the first syntax style: function_name () { ... }. Note the important rule that a function must be defined before it is called.

The basic syntax is:

function_name () {



    # A block of commands
    echo "This is inside the function."
    ls -l
}

To execute the commands inside the function, you simply "call" it by writing its name:




# This will execute the echo and ls commands defined in the function
function_name

3. Passing Arguments and Variable Scope

Functions become much more powerful when you can pass information to them. In Bash, functions receive arguments in the same way a script does: as positional parameters ($1, $2, $3, etc.).

This allows you to create flexible functions that behave differently based on the input you provide. For example, a function could take a filename as an argument and perform an action on that specific file.

Local vs. Global Variables

When working with variables and functions, it's crucial to understand variable scope.

  • Global variables are visible throughout the entire script, both inside and outside functions.
  • Local variables, declared with the local keyword, are visible only inside the function where they are created.

Using local is a best practice because it prevents a function from accidentally changing a variable used in another part of your script, which can lead to very confusing bugs.

Bash Script Demonstrating Global and Local Variables in Functions
This image illustrates variable scope. The `global_var` is accessible both inside and outside the function. The `local_var`, declared with `local`, exists only within the function; trying to access it outside results in nothing being printed.

4. "Returning" Values from a Function

If you're used to Python or Java, you might expect a function to return data. Bash functions work a bit differently and can be thought of as "mini-programs." They have two main ways of sending information back to the caller:

  1. Exit Status: Functions can return a numeric exit code (an integer between 0 and 255) using the return command. By convention, 0 means success and any non-zero value indicates an error. The main script can then check this value using the special variable $?. This is useful for signaling whether the function succeeded or failed.

  2. Standard Output: To "return" text or data, the function simply prints it to standard output using echo. The calling part of the script can then capture this output into a variable using command substitution $(...).

This next video does an excellent job of explaining these two concepts: passing arguments and the two methods of returning values.

Crash-Course! Functions in Bash quickly explained and demystified!

This crash course from the 'You Suck at Programming' channel provides a fantastic, quick explanation of how arguments and return values work in Bash functions. It clearly explains the 'mini-program' mental model.

Watch from 00:46 to 05:12. Focus on these key points: How arguments are passed to the greet function and accessed inside it using $1 and $2. How the function uses return 5 to set an exit code, which is then checked with $?. How the function's echo output is captured into a variable using s=$(...).


Do-It-Yourself: Refactor Your cleanup.sh Script

It's time to put these concepts into practice. You're going to take the cleanup.sh script you wrote in the previous lesson and refactor it to use functions. This will make it more modular and readable.

Your Current Script (or similar):




#!/usr/bin/env bash

shopt -s nullglob

TARGET_DIR="."
ARCHIVE_DIR="archive"

if [[ ! -d "$TARGET_DIR/$ARCHIVE_DIR" ]]; then
    echo "Creating archive directory..."
    mkdir "$TARGET_DIR/$ARCHIVE_DIR"
fi

echo "Searching for .log and .tmp files to archive..."

for file in "$TARGET_DIR"/*.log "$TARGET_DIR"/*.tmp; do
    echo "Archiving $file..."
    mv "$file" "$TARGET_DIR/$ARCHIVE_DIR/"
done

echo "Cleanup complete."

Your Task:

Create a new script named cleanup_functional.sh and modify it to use functions.

  1. Create a setup_archive_dir function:

    • This function should contain the logic for checking if the archive directory exists and creating it if it doesn't.
    • It should accept one argument: the path to the archive directory.
    • Inside the function, use a local variable for the directory path argument.
    • The if statement and mkdir command from your old script should go inside this function.
  2. Create an archive_files function:

    • This function should contain the for loop that archives files.
    • It should accept two arguments: the file pattern to search for (e.g., *.log) and the destination archive directory.
    • Use local variables for these arguments.
    • The for loop from your old script should go inside this function, but it should be modified to use the arguments you pass to it.
  3. Update the main script body:

    • The main part of your script (after the function definitions) should now be very simple.
    • It should first call setup_archive_dir with the name of your archive folder.
    • Then, it should call archive_files twice: once for .log files and once for .tmp files.

How to Test Your Script:

First, recreate your test environment if needed:




# remove the old practice directory if it exists
rm -rf ~/automation_practice




# Create a fresh directory to work in
mkdir ~/automation_practice
cd ~/automation_practice




# Create some files
touch app-2024-01.log
touch app-2024-02.log
touch tempfile_123.tmp
touch data.csv

Now, run your new cleanup_functional.sh script. The output and result should be exactly the same as before, but your code will be much better organized.

Click here for a hint on the new script's structure.



#!/usr/bin/env bash

shopt -s nullglob




# --- Function Definitions ---




# Creates the archive directory if it doesn't exist.
# Usage: setup_archive_dir <directory_path>
setup_archive_dir () {
    local archive_path="$1" # Use a local variable for the argument
    if [[ ! -d "$archive_path" ]]; then
        echo "Creating archive directory: $archive_path"
        mkdir -p "$archive_path"
    fi
}




# Archives files matching a pattern.
# Usage: archive_files <file_pattern> <destination_dir>
archive_files () {
    local pattern="$1"
    local dest_dir="$2"
    
    echo "Archiving files matching '$pattern'..."
    for file in $pattern; do
        echo "  -> Moving $file"
        mv "$file" "$dest_dir/"
    done
}





# --- Main Script Logic ---

TARGET_DIR="."
ARCHIVE_DIR="archive"
FULL_ARCHIVE_PATH="$TARGET_DIR/$ARCHIVE_DIR"




# Call the functions to do the work
setup_archive_dir "$FULL_ARCHIVE_PATH"
archive_files "$TARGET_DIR/*.log" "$FULL_ARCHIVE_PATH"
archive_files "$TARGET_DIR/*.tmp" "$FULL_ARCHIVE_PATH"

echo "Cleanup complete."

For more practice, you can explore the exercises on `w3resource`. The sections on "Creating and calling functions" and "Passing arguments to functions" are directly relevant.

Bash Scripting Exercises, Solutions, Practice

This page lists several exercises to help you solidify your understanding of Bash functions. No need to do them now, but it's a good resource to bookmark for further practice.

Navigate to the 'Functions and Modularization' section on the page. You will find links to exercises that cover what we've learned today.


Conclusion

Congratulations! You've taken a significant step forward in writing professional-quality shell scripts. By learning to define and call functions, you can now create code that is modular, reusable, and much easier to read and maintain.

Key Takeaways:

  • Functions group blocks of code under a name, promoting reusability and adhering to the DRY principle.
  • Functions are defined with the function_name () { ... } syntax and must be defined before they are called.
  • Arguments are passed as positional parameters ($1, $2, etc.).
  • Always use the local keyword for variables inside your functions to control their scope and prevent bugs.
  • Functions "return" information via two channels: a numeric exit status (return and $?) for success/failure, and standard output (echo and $(...)) for data.

In our next lesson, we will build on this by learning how to accept and parse command-line arguments in a script. This will allow you to make your scripts even more flexible, enabling you to run them like ./cleanup_functional.sh --target /var/log instead of having to edit variables inside the script itself.