Functions
Arguments, return status, and keeping variables local
A function groups commands under a name so you can call them more than once. Two spellings exist; the first is the portable one and the one to use:
greet() {
echo "Hello, $1"
}
greet deb1
greet "the whole team"
Hello, deb1
Hello, the whole team
Inside a function, $1 and $2 are the function's arguments, not the script's, and $# counts
them. $0 is the exception: it stays the script's own name, because a function is not a separate
program. This is the same set of rules as
Script arguments, applied one level in.
A function must be defined before it is called. Bash reads a script top to bottom, so a call placed above its definition fails with "command not found", which is why helper definitions conventionally sit at the top and the work happens at the bottom.
return gives back a status, not a value
This is the part that surprises anyone arriving from another language. return sets an exit
status: an integer from 0 to 255, where 0 means success. It cannot hand back a string, and a
function's status is the status of its last command unless you say otherwise.
That makes functions usable directly as conditions:
is_big() { [ "$1" -gt 3 ]; }
if is_big 5; then echo "5 is big"; fi
if is_big 1; then echo "unreachable"; else echo "1 is not big"; fi
5 is big
1 is not big
is_big contains no return at all. The status of [ "$1" -gt 3 ] is the function's status, and
if branches on it exactly as it would for any command.
return and exit are not interchangeable. return leaves the function; exit ends the whole
script, wherever it was called from:
check() {
if [ -z "$1" ]; then
echo "no argument given"
return 1
fi
echo "got $1"
}
check || echo "check failed, but the script continues"
check hello
no argument given
check failed, but the script continues
got hello
Using exit 1 there would have stopped the script at the first call, and the two lines after it
would never have printed. A function that might be reused wants return, so its caller decides
what a failure means.
Handing back a value
Since return only carries a number, a function that computes something prints it and the caller
captures that with command substitution:
newest_file() {
ls -t "$1" | head -1
}
latest=$(newest_file /etc)
echo "captured: ${latest:+something}"
captured: something
The trade is that anything else the function prints ends up in the captured value too, so a
function used this way should send progress messages to stderr (>&2) and keep stdout for the
result alone.
local, and what happens without it
Every variable in bash is global by default, including ones first assigned inside a function.
A helper that forgets local reaches out and overwrites its caller's state:
name="outer"
clobber() { name="inner"; }
safe() { local name="inner"; }
clobber
echo "after clobber: $name"
name="outer"
safe
echo "after safe: $name"
after clobber: inner
after safe: outer
Both functions look identical from the outside. One of them quietly changed a variable belonging
to code it knows nothing about, and in a longer script that is a bug you find by accident, days
later, in a completely different function. Declare every variable a function assigns as local,
including loop counters.
masked() { local out=$(false); echo "with local: $?"; }
checked() { local out; out=$(false); echo "declared first: $?"; }
masked
checked
with local: 0
declared first: 1
The first reports success for a command that failed. This one is genuinely hard to spot by reading, and it is worth knowing before you rely on error handling inside a function.
Exercises
-
Write a function
requirethat prints a message to stderr and returns 1 when its argument is empty, and use it without stopping the script.Answer
require() { if [ -z "$1" ]; then echo "required value missing" >&2 return 1 fi } require "" || echo "carrying on anyway"return 1rather thanexit 1is what leaves that decision to the caller. -
Why does this print
0, and how would you fix it?count_lines() { local n=$(wc -l < missing.txt); }Answer
The exit status belongs to
local, not towc, so the failure to openmissing.txtis discarded. Split the declaration from the assignment:count_lines() { local n n=$(wc -l < missing.txt) || return 1 } -
A function needs to return both a count and a name. How?
Answer
Print both and let the caller split them, or assign to variables the caller names. The simplest reliable form is printing one per line and reading them:
stats() { echo "12"; echo "deb1"; } { read -r count; read -r host; } < <(stats)returncannot do it: it carries a single number between 0 and 255, which is a status and not a value.
What's next
That is the core of the language: variables, conditions, loops, arguments and functions. The two
lessons after this one are about the values themselves rather than the control flow around them,
Arrays and Parameter expansion. For a
script that has to run unattended, the pieces to add are trap for cleanup, set -euo pipefail,
and the error-handling patterns in
Exit codes and error handling.