Functions turn a pile of working commands into a named, reusable step. In bash they are lighter than in most languages — no typed parameters, no real return values — and that lightness is exactly why learning their three honest mechanisms (arguments, exit codes, output) matters early.

Defining and calling #

Define once, call by name
log() {
  echo "[$(date +%H:%M:%S)] $*" >&2
}

deploy() {
  log "starting deploy to $1"
  rsync -a dist/ "server:/srv/$1/"
}

deploy aospix

Definition is name() { ... } — no function keyword needed, no parentheses on parameters. Calls look like commands, because they are: functions join PATH-found commands in the namespace and even appear in type deploy.

Parameters: positional, inside #

Inside a function, $1, $2, … are the function's own arguments — they do not leak from the script's. "$@" is all of them, $# the count:

Every argument, passed through safely
run_in_dir() {
  local dir="$1"; shift
  ( cd "$dir" && "$@" )
}
run_in_dir /var/log tail -n 5 messages

local keeps variables inside the function — without it, a loop variable in a helper silently overwrites the caller's. The shift idiom peels the first argument so "$@" becomes "the rest".

Returning: two channels #

Bash functions return one thing officially — a numeric status, like every command:

The return is an exit code, not a value
file_exists() {
  [[ -f "$1" ]]
}
if file_exists "$config"; then echo ok; fi

The function's return value is the exit code of its last command — so a bare [[ ]] is a perfectly good boolean. Data comes back the other channel: output. The caller captures it with command substitution:

Data flows out via stdout
current_branch() {
  git rev-parse --abbrev-ref HEAD
}
branch="$(current_branch)"

Mixing the channels is the classic bug: an echo "debug..." inside a function whose output is captured will corrupt the result — send diagnostics to stderr (>&2) and data to stdout, always.

You wantMechanism
Success/failurereturn 0 or last command's status
A value backEcho it; caller uses "$(fn)"
Progress messagesecho ... >&2
Private variableslocal

Arguments arrive via $1 and friends; structure grows from arrays. The scripting series continues.