funwithlinux guide

A Deep Dive into Bash Functions and Variables

Bash (Bourne Again Shell) is the backbone of Unix-like systems, powering everything from simple command-line tasks to complex automation scripts. At the heart of writing efficient, maintainable Bash scripts lie two fundamental building blocks: **variables** and **functions**. Variables store data, while functions encapsulate reusable code—together, they transform messy one-liners into structured, scalable scripts. In this blog, we’ll explore Bash variables (types, scoping, and best practices) and functions (definition, parameters, recursion, and more) in depth. Whether you’re a beginner looking to level up your scripting skills or an experienced developer refining your Bash workflow, this guide will equip you with the knowledge to write cleaner, more robust scripts.

Table of Contents

  1. Bash Variables: The Basics
  2. Bash Functions: Reusable Code Blocks
  3. Advanced Topics
  4. Best Practices
  5. Conclusion
  6. Reference

Bash Variables: The Basics

What Are Variables?

In Bash, a variable is a named container for data (text, numbers, or paths). Variables let you store values, pass data between parts of a script, and make scripts dynamic. For example, instead of hardcoding a file path like /tmp/logs, you can store it in a variable LOG_DIR="/tmp/logs" and reuse it throughout the script.

Types of Variables

Bash variables are loosely typed (no need to declare data types), but they fall into three main categories:

1. Local Variables

Visible only within the current shell or script. They are not inherited by child processes.
Example:

name="Alice"  # Local variable (only in this shell/script)
echo "Hello, $name"  # Output: Hello, Alice

2. Environment Variables

Inherited by child processes (e.g., scripts, subshells). Use export to make a local variable an environment variable.
Example:

export PATH="$PATH:/usr/local/bin"  # Add a directory to the system PATH (environment variable)
echo $HOME  # Built-in environment variable (user's home directory)

3. Positional Parameters

Special variables that hold arguments passed to a script or function. Denoted by $1, $2, …, $n (where n is the argument position).
Example:
If you run ./script.sh apple banana, then:

  • $1 = apple
  • $2 = banana

Declaring and Assigning Variables

To declare a variable, use variable_name=value (no spaces around =!). To access its value, prefix the name with $ (e.g., $variable_name).

Key Rules:

  • No spaces: name="Bob" is valid; name = "Bob" is not (Bash interprets name as a command).
  • Quoting: Use quotes to handle values with spaces or special characters:
    message="Hello, world!"  # Double quotes preserve spaces and expand variables
    echo $message  # Output: Hello, world!
    
    path='/home/user/My Documents'  # Single quotes prevent variable expansion
    echo $path  # Output: /home/user/My Documents
  • Case sensitivity: Name and name are distinct variables.

Special Variables

Bash provides built-in special variables for common tasks like handling arguments, exit statuses, and script metadata. Here are the most useful ones:

VariableDescriptionExample
$0Name of the script/functionecho "Script name: $0"Script name: ./script.sh
$1, $2, ...Positional parameters (arguments)./script.sh a b$1=a, $2=b
$#Number of arguments passed./script.sh a b c$#=3
$@All arguments as separate stringsfor arg in "$@"; do echo $arg; done (loops over each arg)
$*All arguments as a single stringecho "$*"a b c (if args are a, b, c)
$?Exit status of the last commandls non_existent_file; echo $?2 (error)
$$Process ID (PID) of the current shellecho "My PID: $$"
$!PID of the last background processsleep 10 &; echo "Background PID: $!"

Example: Using Special Variables

#!/bin/bash
echo "Script name: $0"
echo "Number of arguments: $#"
echo "Arguments: $@"
echo "First argument: $1"

Run with ./script.sh apple banana:

Script name: ./script.sh
Number of arguments: 2
Arguments: apple banana
First argument: apple

Variable Scope: Global vs. Local

  • Global Variables: Visible everywhere in the script (default for variables declared outside functions).
  • Local Variables: Visible only within the function or block where they’re declared (use the local keyword).

Example: Global vs. Local

#!/bin/bash

global_var="I'm global"  # Global variable

my_function() {
  local local_var="I'm local"  # Local variable (only in my_function)
  global_var="Updated global"  # Modifies the global variable
  echo "Inside function: $local_var, $global_var"
}

my_function
echo "Outside function: $global_var"  # Global var was updated
echo "Outside function: $local_var"  # Error: local_var: unbound variable

Output:

Inside function: I'm local, Updated global
Outside function: Updated global
./script.sh: line 9: local_var: unbound variable

Bash Functions: Reusable Code Blocks

Functions let you group commands into named, reusable blocks. They eliminate repetition, improve readability, and make scripts easier to debug.

Function Definition Syntax

Bash supports two syntaxes for defining functions:

1. Standard Syntax

function_name() {
  # Commands here
}

2. function Keyword Syntax

function function_name {
  # Commands here
}

Both are equivalent, but the first syntax is more portable (works in POSIX shells).

Example: Simple Function

greet() {
  echo "Hello, $1!"  # $1 is the first argument to the function
}

greet "Alice"  # Output: Hello, Alice!
greet "Bob"    # Output: Hello, Bob!

Parameters and Arguments

Like scripts, functions accept positional parameters ($1, $2, …) and special variables like $@ (all arguments) and $# (number of arguments).

Example: Function with Parameters

sum() {
  local a=$1  # First argument
  local b=$2  # Second argument
  echo $((a + b))  # Arithmetic expansion: $(( ... ))
}

result=$(sum 5 3)  # Capture output with command substitution
echo "5 + 3 = $result"  # Output: 5 + 3 = 8

Handling Variable Arguments with $@
Use $@ to loop over all arguments dynamically:

print_args() {
  echo "Number of args: $#"
  for arg in "$@"; do  # "$@" preserves spaces in arguments
    echo "- $arg"
  done
}

print_args "apple" "banana pie" "cherry"

Output:

Number of args: 3
- apple
- banana pie
- cherry

Return Values and Exit Status

Bash functions don’t return values like other programming languages. Instead, they:

  1. Output text (via echo, printf, etc.), which can be captured with $(function_name).
  2. Set an exit status (via return N), where N is a number (0 = success, 1-255 = error).

Example: Exit Status with return

is_positive() {
  local num=$1
  if (( num > 0 )); then
    return 0  # Success (true)
  else
    return 1  # Failure (false)
  fi
}

if is_positive 5; then  # Check exit status (0 = true)
  echo "5 is positive"
else
  echo "5 is not positive"
fi

Output: 5 is positive

Example: Capturing Output with Command Substitution

get_greeting() {
  local time=$(date +%H)  # Hour of the day (00-23)
  if (( time < 12 )); then
    echo "Good morning"
  elif (( time < 18 )); then
    echo "Good afternoon"
  else
    echo "Good evening"
  fi
}

greeting=$(get_greeting)
echo "$greeting, Alice!"  # Output: Good morning, Alice! (if run at 9 AM)

Function Scope

Variables inside a function are global by default (they modify the script’s global state). To limit a variable to the function, use the local keyword.

Example: Using local to Avoid Global Side Effects

count=0

increment_bad() {
  count=$((count + 1))  # Modifies the global count
}

increment_good() {
  local count=0  # Local variable (shadows global count)
  count=$((count + 1))
  echo $count  # Output local count
}

increment_bad
echo "Global count after bad: $count"  # Output: 1

result=$(increment_good)
echo "Global count after good: $count"  # Output: 1 (unchanged)
echo "Local result: $result"  # Output: 1

Advanced Topics

Recursive Functions

A function that calls itself is recursive. Useful for tasks like traversing directories, calculating factorials, or solving mathematical problems.

Example: Factorial Function

factorial() {
  local n=$1
  if (( n == 0 )); then
    echo 1  # Base case: 0! = 1
  else
    echo $(( n * $(factorial $((n - 1))) ))  # Recursive call
  fi
}

echo "5! = $(factorial 5)"  # Output: 5! = 120 (5*4*3*2*1)

Example: Directory Traversal (Recursive)

list_files() {
  local dir=$1
  for item in "$dir"/*; do
    if [ -d "$item" ]; then  # If item is a directory
      echo "DIR: $item"
      list_files "$item"  # Recurse into subdirectory
    elif [ -f "$item" ]; then  # If item is a file
      echo "FILE: $item"
    fi
  done
}

list_files "./my_project"  # List all files/subdirs in ./my_project

Arrays in Functions

Bash arrays can be passed to functions, but they require special handling (Bash functions don’t support array return values natively).

Passing Arrays to Functions
Use "${array[@]}" to pass array elements as separate arguments, then reconstruct the array inside the function with local args=("$@").

print_array() {
  local arr=("$@")  # Reconstruct array from arguments
  echo "Array elements: ${arr[*]}"  # ${arr[*]} joins elements with spaces
}

fruits=("apple" "banana" "cherry")
print_array "${fruits[@]}"  # Pass array as separate arguments

Output: Array elements: apple banana cherry

“Returning” Arrays
Since functions can’t return arrays, use a global array or command substitution with a delimiter (e.g., :) to simulate returns:

get_even_numbers() {
  local start=$1
  local end=$2
  local evens=()
  for ((i=start; i<=end; i++)); do
    if ((i % 2 == 0)); then
      evens+=($i)  # Add to array
    fi
  done
  echo "${evens[*]}"  # Output elements separated by spaces
}

# Capture output and split into array
result_str=$(get_even_numbers 1 10)
evens=($result_str)  # Split string into array

echo "Even numbers: ${evens[@]}"  # Output: 2 4 6 8 10

Global vs. Local Variables: Pitfalls to Avoid

Accidentally modifying global variables is a common Bash scripting mistake.

Pitfall: Unintended Global Modification

config="default"

set_config_bad() {
  config="custom"  # Modifies global config!
}

set_config_bad
echo "Config: $config"  # Output: custom (unintended change)

Fix: Use local

config="default"

set_config_good() {
  local config="custom"  # Local variable (no global change)
  echo "Local config: $config"
}

set_config_good
echo "Global config: $config"  # Output: default (unchanged)

Best Practices

  1. Naming Conventions

    • Use UPPERCASE for environment variables (e.g., PATH, LOG_DIR).
    • Use lowercase for local variables and functions (e.g., user_name, process_file).
    • Avoid reserved words (e.g., if, for, function).
  2. Quote Variables
    Always quote variables ("$var") to prevent word splitting and globbing:

    filename="my file.txt"
    cat "$filename"  # Works (quotes preserve spaces)
    # cat $filename → Error: cat: my: No such file or directory
  3. Use local in Functions
    Prevent global side effects by declaring variables as local inside functions.

  4. Avoid Global Variables
    Pass data to functions via parameters and return data via output/exit status instead of relying on globals.

  5. Comment Functions
    Document what a function does, its parameters, and return values:

    # Calculate the sum of two numbers
    # Args: $1 (int), $2 (int)
    # Output: Sum of $1 and $2
    sum() {
      echo $(( $1 + $2 ))
    }
  6. Test with set -euo pipefail
    Add set -euo pipefail at the top of scripts to catch errors early:

    • -e: Exit on error.
    • -u: Treat undefined variables as errors.
    • -o pipefail: Exit if any command in a pipeline fails.

Conclusion

Bash variables and functions are the building blocks of clean, maintainable scripts. Variables let you store and manipulate data, while functions encapsulate logic for reuse. By mastering scoping, parameters, recursion, and best practices like local variables and quoting, you’ll write scripts that are robust, readable, and easy to debug.

The key to mastery is practice: experiment with recursive functions, refactor messy scripts into modular functions, and always test edge cases. With these tools, you’ll transform from a Bash novice to a scripting pro.

Reference