Shell Function Examples

Functions in Bash let you group reusable commands, accept arguments, return values, and build modular scripts. They are the key to writing maintainable shell scripts that don't repeat code. This page covers defining functions, argument handling, local variables, and practical real-world patterns.

ShellBashFunctionsScripting
Basics
Define and call a function
greet() {
  echo "Hello, $1!"
}

# Call the function
greet "World"     # Output: Hello, World!
greet "Alice"     # Output: Hello, Alice!
Define a function with name() { ... }. Positional parameters $1, $2, etc. hold the arguments passed when calling. Functions must be defined before they are called in the script. No need to declare parameter types or count.
Use local variables
calculate() {
  local result=$(( $1 + $2 ))
  echo "Sum: $result"
}

result="global value"
calculate 3 7   # Output: Sum: 10
echo $result    # Still: global value
Use the local keyword to declare variables that are scoped to the function. Without local, variables are global and can overwrite variables in the calling scope. Always use local for function-internal variables to prevent bugs.
Return a value via echo
get_timestamp() {
  echo $(date +%Y%m%d_%H%M%S)
}

ts=$(get_timestamp)
echo "Backup file: backup_${ts}.tar.gz"
Bash functions can't return values like other languages — return only sets an exit code (0-255). The idiomatic way to return a string value is to echo it and capture the output with command substitution $().
Set a default parameter value
deploy() {
  local env="${1:-production}"
  local branch="${2:-main}"
  echo "Deploying $branch to $env"
}

deploy              # production / main
deploy staging      # staging / main
deploy staging dev  # staging / dev
The ${var:-default} syntax uses default if var is unset or empty. This is the standard pattern for optional function arguments with sensible defaults. It works for positional parameters too.
Argument Handling
Validate required arguments
backup_db() {
  if [ $# -lt 2 ]; then
    echo "Usage: backup_db <host> <dbname>" >&2
    return 1
  fi
  local host="$1"
  local db="$2"
  echo "Backing up $db from $host"
}
$# is the count of arguments passed. Check it at the start of functions that require specific arguments. Write usage messages to stderr (>&2) and use return 1 to signal an error without exiting the script.
Pass all arguments to another command
log_run() {
  echo "Running: $*"
  "$@"
  echo "Exit code: $?"
}

log_run ls -la /tmp
"$@" expands to all positional parameters as separate quoted words — the safe way to forward all arguments to another command. $* expands to a single string. Always use "$@" when forwarding arguments.
Practical Patterns
Check if a command exists
require_command() {
  if ! command -v "$1" >/dev/null 2>&1; then
    echo "Error: '$1' is required but not installed." >&2
    exit 1
  fi
}

require_command docker
require_command jq
command -v checks if a command is available in PATH without running it. This is more portable than which. Redirecting to /dev/null suppresses all output. Use this at the start of scripts to fail fast with clear error messages.
Logging function with levels
log() {
  local level="$1"
  shift
  echo "[$(date +%H:%M:%S)] [$level] $*"
}

log INFO "Starting deployment"
log WARN "Config file not found, using defaults"
log ERROR "Database connection failed"
A reusable logging function adds timestamps and log levels to all messages. shift removes the first argument (level), leaving $@ as the message. This pattern makes scripts much easier to debug and monitor.
Retry a command N times
retry() {
  local max="${1}"; shift
  local count=0
  until "$@"; do
    count=$((count + 1))
    [ $count -ge $max ] && return 1
    echo "Attempt $count failed, retrying..."
    sleep 2
  done
}

retry 3 curl -sf https://api.example.com/health
A retry wrapper runs a command and retries up to N times on failure. This pattern is essential in deployment scripts where transient network errors can cause intermittent failures. The command and all its arguments are forwarded via "$@".
Recursive function (factorial)
factorial() {
  local n=$1
  if [ $n -le 1 ]; then
    echo 1
  else
    local prev=$(factorial $((n - 1)))
    echo $((n * prev))
  fi
}

factorial 5   # Output: 120
Bash functions can call themselves recursively. Capture the recursive result with command substitution. Be aware that each recursive call spawns a subshell, which is slow for deep recursion. Bash has a default stack limit of ~1000 levels.

How to Use

  1. Define functions at the top of your script before any code that calls them.
  2. Use local for all variables inside functions to avoid polluting the global scope.
  3. Validate $# at the start and print a usage message if required args are missing.
  4. Use "$@" to forward all arguments; capture return values with $(function_name).

Frequently Asked Questions

How do I return a value from a bash function?

Bash's return statement only sets an integer exit code (0 = success, non-zero = error). To return a string, echo it inside the function and capture it with command substitution: result=$(my_function). For multiple values, echo them separated by a delimiter and use IFS to split.

What is the difference between $@ and $* in functions?

"$@" expands each argument as a separate quoted word: "arg1" "arg2" "arg3". "$*" expands all arguments as a single word joined by IFS (default space): "arg1 arg2 arg3". Always use "$@" when forwarding arguments to preserve spaces within individual arguments.

Can I export functions to subshells?

Yes, use export -f function_name to make a function available in child processes and subshells. This is useful when you need a function to be available in scripts called from your script. However, note that exported functions are passed via environment variables and can have security implications.

Related Tools