05 · Functions & Arguments¶
🎥 Video walkthrough¶
Defining and calling a function¶
#!/usr/bin/env bash
greet() {
echo "Hello, $1!"
}
greet "Ada" # Hello, Ada!
greet "Grace" # Hello, Grace!
Both greet() { ... } and function greet { ... } work in Bash; the first
form is more portable (POSIX sh doesn't understand the function keyword),
so prefer it.
Positional parameters inside a function¶
Functions get their own set of positional parameters ($1, $2, ...) — they
don't inherit the script's, they receive whatever was passed at the call site.
show_args() {
echo "first: $1"
echo "second: $2"
echo "all args: $@"
echo "count: $#"
}
show_args one two three
# first: one
# second: two
# all args: one two three
# count: 3
$@ vs $* — always quote "$@"¶
print_each() {
for arg in "$@"; do
echo "arg: [$arg]"
done
}
print_each "hello world" foo
# arg: [hello world] <- preserved as ONE argument
# arg: [foo]
print_each_star() {
for arg in "$*"; do
echo "arg: [$arg]"
done
}
print_each_star "hello world" foo
# arg: [hello world foo] <- collapsed into a SINGLE string
"$@" expands to each positional parameter as its own separately-quoted
word — this is what you want almost every time you forward arguments. "$*"
joins everything into one string. Unquoted $@ and $* both word-split and
lose this distinction, so always quote.
Return values: return vs echo¶
return only sets a numeric exit status (0–255) — it is not a way to
hand back arbitrary data like a string or number result.
is_even() {
local n=$1
if (( n % 2 == 0 )); then
return 0 # success/true
else
return 1 # failure/false
fi
}
if is_even 4; then
echo "4 is even"
fi
To return an actual value (like a computed string or number), print it
with echo/printf and capture it with command substitution:
square() {
local n=$1
echo $(( n * n ))
}
result=$(square 5)
echo "5 squared is $result" # 5 squared is 25
local — keep function variables from leaking¶
counter=0
bump() {
local counter=100 # shadows the outer variable, only inside this function
((counter++))
echo "inside: $counter"
}
bump # inside: 101
echo "outside: $counter" # outside: 0 — untouched
Without local, a variable assigned inside a function is global by
default — a common source of bugs in longer scripts. Always declare
function-local state with local.
Default values for missing arguments¶
backup() {
local source=$1
local dest=${2:-/tmp/backups} # default if $2 wasn't passed
echo "backing up $source to $dest"
}
backup "/home/user/docs" # uses the default dest
backup "/home/user/docs" "/mnt/backup" # uses the explicit dest
Checking argument count¶
require_args() {
if [[ $# -lt 2 ]]; then
echo "Usage: require_args <name> <age>" >&2
return 1
fi
echo "$1 is $2 years old"
}
require_args "Ada" # prints usage to stderr, returns 1
require_args "Ada" 30 # Ada is 30 years old
How It Actually Works¶
A shell function is not a subprocess — calling greet "Ada" does not
fork. Bash just pushes a new entry onto its internal call stack, temporarily
rebinds the positional parameters ($1, $2, $#, $@) to the function's
own arguments, executes the function body's commands directly in the
current shell process, and then restores the caller's positional
parameters when the function returns. This is why a function can cd or set
a variable that persists after it returns (unless you explicitly local
it), unlike a script you call as a separate command, which runs in its own
forked/exec'd process and can never affect the parent shell's variables.
local allocates the variable in a scope frame tied to that specific
function-call stack entry; when the function returns, bash pops the frame
and any local bindings simply disappear, while unmarked assignments write
directly into the shell's single global variable table.
return sets the function's exit status ($?) the same way a process exit
does, but it's purely an internal stack-unwind signal understood by the bash
interpreter — no wait() syscall is involved because no child process was
ever created.
Cheat sheet¶
| Symbol | Meaning inside a function |
|---|---|
$1, $2, ... |
positional arguments passed to the function |
$# |
number of arguments passed |
"$@" |
all arguments, each preserved as a separate word |
"$*" |
all arguments joined into one string |
local var |
scope a variable to this function only |
return N |
set the function's exit status (0-255, not a value) |
echo value + $(func) |
the idiom for "returning" an actual value |
🔀 See this in another language¶
- PowerShell — Working with Objects & the Pipeline
- C++ — Arrays & std::vector Basics
- Swift — Optionals Basics
Exercise¶
Write a function max_of that takes two numbers as arguments and echoes the
larger one (capture it with $( ) when calling). Then write a function
validate_args that checks exactly 2 arguments were passed to the script,
printing a usage message to stderr and returning 1 if not.