Code of the Day
IntermediateScripting patterns

Functions

Define reusable shell functions, manage local variables, handle arguments with $@/$#, return values, and share logic across scripts.

BashIntermediate10 min read
Recommended first
By the end of this lesson you will be able to:
  • Define and call Bash functions
  • Use local to keep variables from leaking out of functions
  • Access positional arguments via $1-$9, $@, and $#
  • Return values using echo and exit codes
  • Source a function library from multiple scripts

Scripts grow. A 50-line script that does everything in one block becomes hard to debug, test, and reason about. Functions solve this the same way they do in every language: name a piece of logic, isolate its state, and call it by name. The difference in Bash is that functions live in the same shell process and share the global environment — which makes scoping discipline essential.

Defining and calling functions

Bash has two equivalent syntaxes; the second is more portable:

# Style 1 — function keyword
function greet {
  echo "Hello, $1"
}

# Style 2 — POSIX compatible (preferred)
greet() {
  echo "Hello, $1"
}

greet "world"      # Hello, world
greet "Alice"      # Hello, Alice

Functions must be defined before they are called — Bash reads scripts top to bottom. Define utility functions at the top of the file (or in a sourced library), and call them from the bottom.

Positional arguments: $1, $@, $#

Inside a function, $1$9 refer to the function's own arguments, not the script's. $@ expands to all arguments as separate words; $# is the count:

list_args() {
  echo "Received $# arguments"
  for arg in "$@"; do
    echo "  - $arg"
  done
}

list_args alpha beta "gamma delta"
# Received 3 arguments
#   - alpha
#   - beta
#   - gamma delta

Always quote "$@" when passing it through — it preserves per-argument boundaries even when arguments contain spaces.

local variables

Without local, any variable you set inside a function contaminates the global environment:

bad_counter() {
  count=0              # sets the global $count
  count=$((count + 1))
}

good_counter() {
  local count=0        # visible only inside this function
  count=$((count + 1))
  echo "$count"
}

Forgetting local is a common, silent bug. If a function and the script body both use a variable named tmp, result, or i, the function will clobber the caller's value unless local is used. Make local a reflex for every variable you declare inside a function.

Returning values

Bash return only passes a numeric (0–255) — not a string. Return data by printing it and capturing with $():

get_timestamp() {
  date +"%Y-%m-%d %H:%M:%S"
}

stamp=$(get_timestamp)
echo "Script started at $stamp"

Use the exit code for true/false checks — keeping the function name as a boolean predicate makes the calling code read naturally:

is_even() {
  local n=$1
  (( n % 2 == 0 ))    # arithmetic evaluates to 0 (true) if non-zero result... wait:
                       # (( expr )) returns 0 if expr != 0, 1 if expr == 0
}

# Actually: (( n % 2 == 0 )) is 0 (true) when n is even
if is_even 4; then echo "even"; fi
if ! is_even 7; then echo "odd"; fi

(( expr )) as an exit code: The arithmetic compound (( n % 2 == 0 )) exits 0 (success/true) when the expression is non-zero in the C sense — i.e., when n % 2 == 0 evaluates to 1 (true). This is consistent with [[ ]]: exit 0 means success/true, exit 1 means failure/false.

Function libraries

Large projects put shared functions in a library file and source it at the top of each script:

# lib/utils.sh
log_info()  { echo "[INFO]  $(date +%T) $*"; }
log_error() { echo "[ERROR] $(date +%T) $*" >&2; }

die() {
  log_error "$1"
  exit "${2:-1}"
}
#!/usr/bin/env bash
source "$(dirname "$0")/lib/utils.sh"

[[ -f "$1" ]] || die "File not found: $1"
log_info "Processing $1"

source (or its alias .) runs the library in the current shell — functions and variables become available immediately. The $(dirname "$0") idiom finds the library relative to the script's own location, regardless of where the script is called from.

Check your understanding

  1. 1.
    Inside a Bash function, you set result="done" without using local. What happens?
  2. 2.
    A function needs to hand a filename string back to its caller. What is the correct approach?
  3. 3.
    Inside a function, $1 refers to the script's first argument, not the function's first argument.

Do it yourself

# Define and call a function
greet() {
  local name=${1:-"stranger"}
  echo "Hello, $name!"
}
greet "Alice"
greet

# Return a value via echo
get_extension() {
  local file=$1
  echo "${file##*.}"   # parameter expansion strips up to last dot
}
ext=$(get_extension "report.csv")
echo "Extension: $ext"

# Source a small library inline
source /dev/stdin <<'EOF'
log() { echo "[$(date +%T)] $*"; }
EOF
log "Library sourced"

Where to go next

Functions give you structure. The next lesson — Exit codes — shows how to make scripts fail loudly and correctly with set -e, set -u, trap, and explicit exit codes, turning functions into reliable building blocks.

Finished reading? Mark it complete to track your progress.

On this page