6 min read

How to Write Reusable Code with Bash Functions

Table of Contents

Writing long, repetitive shell scripts is a pain. They are hard to read, difficult to debug, and a nightmare to maintain. The solution is to think like a programmer and write reusable, modular code. In Bash, this is done with functions.

Functions let you package blocks of code into named units that you can call whenever you need them. This is the key to writing clean, organized, and efficient scripts. Let’s walk through how to do it.

Step 1: The Basic Syntax

Defining a function is simple. You have two common formats. We’ll use the first one, as it’s the most compatible across different shells.

function_name() {
  # Your code goes here
  echo "Hello from my first function!"
}

To run the code inside the function, you just call it by its name, as if it were any other command.

#!/bin/bash

# Define the function
log_message() {
  echo "INFO: A message from my function."
}

# Call the function
echo "Starting the script..."
log_message
echo "Script finished."

Save this as test.sh, make it executable with chmod +x test.sh, and run it.

Output:

Starting the script...
INFO: A message from my function.
Script finished.

Step 2: Passing Arguments to Your Function

Functions become truly powerful when you can pass data to them. Just like a script, a function can access arguments using positional parameters: $1 for the first argument, $2 for the second, and so on. $@ refers to all arguments.

Let’s create a function that takes a username as an argument and prints a custom greeting.

#!/bin/bash

greet_user() {
  # $1 refers to the first argument passed to the function
  local username=$1
  echo "Welcome, ${username}!"
}

greet_user "Alice"
greet_user "Bob"

Output:

Welcome, Alice!
Welcome, Bob!

Notice the local keyword. This is crucial. It ensures the username variable exists only inside the function. Without it, you risk accidentally overwriting a variable with the same name elsewhere in your script, which can lead to confusing bugs. Always use local for variables inside your functions.

Step 3: Getting Data Out of a Function

There are two primary ways to get a result from a Bash function, and they serve different purposes.

Method 1: Use return for Exit Codes

The return command sets the function’s exit status, which is a number between 0 and 255. By convention, 0 means success and any other number indicates an error. This is perfect for functions that perform a check or an action that can either succeed or fail.

Here’s a function that checks if a file exists:

#!/bin/bash

# This function checks if a file exists.
# Returns 0 if it exists, 1 if it does not.
does_file_exist() {
  if [ -f "$1" ]; then
    return 0 # Success
  else
    return 1 # Failure
  fi
}

if does_file_exist "/etc/hosts"; then
  echo "/etc/hosts was found."
fi

if does_file_exist "/not/a/real/file"; then
  echo "This will not print."
else
  echo "/not/a/real/file was NOT found."
fi

Output:

/etc/hosts was found.
/not/a/real/file was NOT found.

The if statement automatically checks the exit status of the function call.

Method 2: Use echo to Return Data

What if you need to return a string, like a calculated filename or a line from a file? You can’t use return for that. The standard practice is to use echo to print the result to standard output and then capture it using command substitution ($(...)).

This function generates a timestamped filename.

#!/bin/bash

# This function creates a standard backup filename.
create_backup_filename() {
  local base_name=$1
  local timestamp=$(date +%Y-%m-%d_%H-%M-%S)
  # The last echo is the "return value"
  echo "${base_name}-${timestamp}.tar.gz"
}

# Capture the output of the function into a variable
archive_name=$(create_backup_filename "webapp-data")

echo "Preparing to create backup: ${archive_name}"
# Now you can use the $archive_name variable
# tar -czf "$archive_name" /var/www/webapp

Output:

Preparing to create backup: webapp-data-2025-11-14_22-33-06.tar.gz

Step 4: Putting It All Together

Let’s build a simple script that automates creating a backup of a directory. It combines everything we’ve learned: arguments, local variables, exit codes, and returning data.

#!/bin/bash

# A function to log messages with a timestamp.
log() {
  local message=$1
  echo "[$(date +'%Y-%m-%d %H:%M:%S')] - $message"
}

# A function to check if a directory exists.
# Returns 0 for success, 1 for failure.
check_directory() {
  local dir_path=$1
  if [ -d "$dir_path" ]; then
    log "Directory '${dir_path}' found."
    return 0
  else
    log "ERROR: Directory '${dir_path}' not found."
    return 1
  fi
}

# A function to create a tar.gz archive.
# Returns the archive name via echo.
create_archive() {
  local source_dir=$1
  local dest_dir=$2
  local base_name=$(basename "$source_dir")
  local timestamp=$(date +%Y%m%d-%H%M)
  local archive_name="${dest_dir}/${base_name}-${timestamp}.tar.gz"

  log "Creating archive: ${archive_name}"
  tar -czf "$archive_name" -C "$(dirname "$source_dir")" "$base_name"
  echo "$archive_name"
}

# --- Main Script Logic ---
SOURCE="/var/log"
DESTINATION="/tmp/backups"

# Create the destination directory if it doesn't exist
mkdir -p "$DESTINATION"

log "Starting backup process..."

# Check if the source directory exists before continuing
if ! check_directory "$SOURCE"; then
  log "Backup failed. Aborting."
  exit 1
fi

# Create the archive and capture its name
final_archive=$(create_archive "$SOURCE" "$DESTINATION")

log "Backup successful: ${final_archive}"
log "Backup process complete."

Conclusion

You now have the tools to write cleaner, more professional Bash scripts. By breaking your code into functions, you make it easier to read, debug, and reuse.

To summarize the key points:

  • Define Functions: Use my_func() { ... } for maximum compatibility.
  • Pass Arguments: Use $1, $2, etc., to read arguments passed to your function.
  • Use local: Always declare variables inside functions with local to avoid side effects.
  • Return Status: Use return 0 for success and return 1 (or another non-zero number) for failure.
  • Return Data: Use echo to print the data and capture it with my_var=$(my_func).

As a next step, look at one of your existing scripts. Can you identify a block of code that is repeated or could be logically grouped? Try refactoring it into a function. You’ll see the benefits immediately.