Bash scripting guide · part 17 of 32
Functions
Defining and calling bash functions, arguments, local variables, return status versus output, namerefs, and sourcing a function library.
A bash function is a named compound command. You call it like any other command, it gets its own positional parameters, and it finishes with an exit status. That last part is the source of most confusion: a function does not "return a value" the way it does in other languages. It returns a status (0 to 255) and, separately, it can write to standard output, which the caller can capture.
Defining and calling
There are two syntaxes. name() { ...; } is POSIX and works in every shell; function name { ...; }
is a bash/ksh form that buys you nothing in bash and breaks under sh. Use the first. The body
is usually a { } group, so it needs a space after { and a ; or newline before }.
Define a function and call it
greet() {
echo "Hello, $1"
}
greet world
greet "Ada Lovelace"
Hello, world Hello, Ada Lovelace
The function is defined once and then called twice. Inside it, $1 is the function's first
argument, not the script's, so the quoted "Ada Lovelace" arrives as one argument.
A function must be defined before the line that calls it runs. Bash reads a script top to
bottom, so the usual layout is: functions first, then a main "$@" call at the very end.
That also means the whole file has been parsed before anything executes, which matters if
someone edits the script while it is running.
Arguments
Inside a function, $1, $2, $#, "$@" and shift all refer to the function's own
arguments. The caller's are hidden until the function returns. $0 is the exception: it stays
the name of the script (or of the shell). The function's own name is in ${FUNCNAME[0]}.
A function's own positional parameters
#!/usr/bin/env bash
show() {
echo "function ${FUNCNAME[0]} got $# args: $*"
for a in "$@"; do printf ' [%s]\n' "$a"; done
}
echo "script got $# args"
show "one two" three
echo "after the call, \$1 is still: $1"
bash args.sh alpha beta
script got 2 args function show got 2 args: one two three [one two] [three] after the call, $1 is still: alpha
The script was called with alpha beta, the function with "one two" three. Each sees only
its own arguments, and "$@" keeps one two together as a single word. Back in the script,
$1 is alpha again.
Local variables
Variables in bash are global by default, including ones first assigned inside a function.
local makes a variable belong to the current function call. Use it for every variable a
function assigns unless you mean to change global state.
Without local, assignments leak out of the function
count=10
bump() {
local count=99
total=5
}
bump
echo "count=$count total=$total"
count=10 total=5
count was declared local, so the global count is untouched and still 10. total was
not, so the function created a global that is still there after it returns.
Bash scoping is dynamic, not lexical: a local variable is visible to every function
called from the function that declared it, for as long as that call is running. It is a common
surprise for people coming from other languages, and occasionally useful.
Locals are visible to the functions you call
inner() { echo "inner sees level=$level"; }
outer() { local level=outer; inner; }
level=global
outer
inner
inner sees level=outer inner sees level=global
When outer calls inner, the lookup of level finds outer's local first. Called
directly, inner sees the global. The value depends on who called, not where inner was written.
Return status versus output
return N sets the function's exit status, the same number $? and if look at. It is
meant for success or failure, not for data. The status is taken modulo 256, so a function
cannot return 300 or -1 meaningfully.
return sets the status, which wraps at 256
is_even() { return $(( $1 % 2 )); }
if is_even 4; then echo "4 is even"; fi
is_even 7; echo "is_even 7 -> $?"
big() { return 300; }
big; echo "return 300 -> $?"
4 is even is_even 7 -> 1 return 300 -> 44
is_even returns 0 (success) for even numbers, so it reads naturally in an if. return 300
comes back as 44, because only the low 8 bits survive (300 - 256 = 44).
To get data out, print it and let the caller capture it with $(...). This is the idiomatic
"return value". The catch is that $(...) runs the function in a subshell, so any global it
sets is lost (see Subshells and command grouping). If a function must both produce a value and change
state, a nameref avoids the subshell.
Return data on stdout, and what a subshell loses
calls=0
to_upper() {
calls=$((calls + 1))
printf '%s\n' "${1^^}"
}
name=$(to_upper "ada")
echo "name=$name calls=$calls"
to_upper "grace" >/dev/null
echo "calls=$calls"
name=ADA calls=0 calls=1
The captured call returned ADA, but its calls increment happened in a subshell and was
thrown away, so calls is still 0. The direct call ran in the current shell and the counter
reached 1.
Write into a caller's variable with a nameref
split_host() {
local -n _host=$2 _port=$3
_host=${1%:*}
_port=${1##*:}
}
split_host db01:5432 h p
echo "host=$h port=$p"
host=db01 port=5432
local -n (bash 4.3+) makes _host an alias for whatever variable name was passed in, here
h and p. No subshell is involved. Give nameref variables unusual names: if the caller
passed a variable that is itself called _host, the reference would point at itself.
Inspecting and removing functions
declare -F lists function names, declare -f name prints a function's definition as bash
reformatted it, and type name tells you whether a word is a function, builtin, alias or file.
unset -f name removes one. Functions can also shadow commands: a function named ls wins
over /usr/bin/ls, and command ls bypasses it.
See how bash stored a function
ls() { command ls -1 "$@"; }
type ls
ls data | head -n 3
ls is a function
ls ()
{
command ls -1 "$@"
}
config.json
fruits.txt
hosts.txt type says ls is now a function and prints the body as bash stored it, with the braces on
their own lines and its own indentation. Inside, command ls calls the real binary; without command the function
would call itself forever.
Libraries with source
Shared helpers go in a file that defines functions and does nothing else, which scripts load
with source file (or the POSIX spelling . file). Sourcing runs the file in the current
shell, so its functions and variables become part of the caller. The fixture's src/util.sh
is such a file: a log that writes to stderr with a timestamp, and a die that logs and exits.
Load helper functions from another file
source src/util.sh
log "starting"
die "config not found"
echo "never printed"
20:22:20 starting 20:22:20 error: config not found
log prints the time and message on stderr. die calls log and then exit 1, and because
a sourced function runs in the current shell, that exit ends the whole script: the last
echo never runs. Use return in library code if the caller should decide.
Recursion and the call stack
Functions can call themselves. Each call gets fresh locals, and FUNCNAME, BASH_LINENO and
BASH_SOURCE hold the current call stack, which is what the caller builtin reads. Bash has
no tail-call optimization and recursion is slow; it is fine for walking a small tree, not for
number crunching.
A recursive function and its stack
fact() {
local n=$1
if (( n <= 1 )); then
echo "depth ${#FUNCNAME[@]}: ${FUNCNAME[*]}" >&2
echo 1
return
fi
echo $(( n * $(fact $((n - 1))) ))
}
fact 5
depth 5: fact fact fact fact fact 120
fact 5 prints 120. The deepest call reports the stack on stderr: five nested fact calls.
FUNCNAME has no main entry here because the code runs from bash -c, not from a script file.
Style that holds up
- Declare every variable a function assigns with
local, and assign the result of a command substitution on a separate line (local out; out=$(cmd)) so the status is not lost (see Strict mode and its caveats). - Return status for success or failure, print data on stdout, send messages to stderr.
- Quote
"$@"when passing arguments through.$*and unquoted$@re-split them. - Put the script body in
main() { ...; }and finish withmain "$@", so every helper is defined before anything runs.
Related
declareCreate variables with attributes (array, associative array, integer, read-only, exported), or show how variables are defined.localDeclare variables inside a function that disappear when it returns, leaving any global of the same name untouched.sourceRun the commands in a file inside the current shell, so its variables, functions and directory changes stay in effect.typeShow how the shell would interpret a name, as an alias, keyword, function, builtin or file on PATH.commandRun a command while skipping functions and aliases of the same name, or check whether a command exists with -v.- Exit codesWhat exit statuses mean, how $? works, the reserved values 126, 127 and 128+N, PIPESTATUS, and how to choose codes for your own scripts.
- Script argumentsPositional parameters $1 to ${10}, $# and $0, why "$@" is almost always right and "$*" rarely, shift, getopts, and ending options with --.
- Subshells and command groupingThe difference between ( ... ) and { ...; }, what a subshell inherits and what it cannot change, which constructs create one, and what that costs.
- Strict mode and its caveatsWhat set -euo pipefail actually does, the cases where -e silently does nothing, the traps in -u and pipefail, and a balanced way to use it.