Why this matters
As scripts grow past a screenful, the same few lines start appearing in several places: print an error and exit, check that a file exists, log a timestamped message. Copying them means fixing every copy when something changes. A function gives a block of code a name, so you write it once and call it like a command. Functions also make scripts readable: a main section that says check_args; backup_database; rotate_old_backups documents itself.
Defining a function
greet() {
echo "Hello, $1!"
}
That is the whole syntax: a name, (), and a body in braces. The parentheses are always empty; they are not where arguments go. There is an alternative form, function greet { ... }, which is bash-only; the name() { } form works in every POSIX shell, so prefer it.
Defining a function runs nothing. bash stores the body under that name, and from then on the name works like a command:
$ greet() { echo "Hello, $1!"; }
$ greet World
Hello, World!
$ type greet
greet is a function
greet ()
{
echo "Hello, $1!"
}
On one line, the body needs a ; (or newline) before the closing }, and a space after {: braces are reserved words, just like then and fi.
Remember the lookup order from module 1: aliases, then functions, then builtins, then PATH. A function can therefore shadow a real command, which is occasionally useful and often confusing. (Inside such a function, command ls calls the real ls and skips the function.)
Functions must be defined before they are called
bash executes a script top to bottom. A call runs whatever is stored under that name at that moment:
#!/bin/bash
hello # error: hello: command not found
hello() { echo hi; }
hello # works
So scripts put their functions near the top and the code that uses them at the bottom (lesson 3 shows the common main layout).
Arguments: $1, $2, "$@" inside the function
A function receives arguments exactly like a script does, and inside the function $1, $2, $#, "$@" and $* refer to the function's own arguments:
#!/bin/bash
show_args() {
echo "function got $# args: $*"
for a in "$@"; do echo " [$a]"; done
}
show_args one "two words" three
echo "script got $# args: $*"
$ bash args.sh x y
function got 3 args: one two words three
[one]
[two words]
[three]
script got 2 args: x y
bash saves the caller's positional parameters at the call and restores them on return. $0 stays the script's name; the function's name is in the variable FUNCNAME.
Exit status and return
A function call is a command, so it has an exit status: by default, the status of the last command run in the body. return N ends the function early and sets the status to N (0-255); plain return keeps the status of the last command.
has_user() {
grep -q "^$1:" /etc/passwd
}
if has_user learner; then echo "learner exists"; fi
has_user nobody123 || echo "no such user"
Functions whose status means yes/no make conditions read like English. Name them like questions: is_number, has_user, file_is_empty.
return is not how you return data. return "text" is an error (return: text: numeric argument required). To hand back a value, print it and let the caller capture it with $(...); that is the subject of the next lesson.
exit vs return
returnleaves the function, and the script continues after the call.exitleaves the whole script, from anywhere, including from inside a function.
That makes exit exactly right for a "fatal error" helper, the most common function in real scripts:
die() {
echo "$(basename "$0"): error: $*" >&2
exit 1
}
[[ -r $config ]] || die "cannot read config file $config"
>&2 sends the message to standard error, so it is not mixed into the script's real output and is still visible when stdout is redirected.
What happens when you call a function
A function call does not fork. bash runs the body in the current shell process, in the current directory, with the same variables. That is why a function can cd and the change sticks, and why a function can change global variables (more on scope next lesson). The only things bash saves and restores around the call are the positional parameters and a few internal details like FUNCNAME:
script: $1=x $2=y call show_args one two
├── push: save $1=x $2=y (a "call frame" on bash's internal stack)
│ function body runs with $1=one $2=two, same process, same variables
└── pop: restore $1=x $2=y status of the body becomes $?
Compare that with running a separate script (bash other.sh), which forks a new process with its own variables and directory: nothing it does to its variables can affect the caller.
Managing functions interactively
$ declare -F # list the names of defined functions
declare -f greet
$ declare -f greet # print a definition
$ unset -f greet # remove it
Interactive shells usually have many functions defined by bash completion; declare -F | wc -l in the sandbox shows how many.
Common mistakes
- Putting arguments in the parentheses.
greet(name) { ... }is a syntax error:syntax error near unexpected token 'name'. Arguments arrive as$1,$2. - Calling before defining.
command not found, even though the function is in the file. - No space after
{or no;before}.greet() {echo hi}fails withsyntax error near unexpected token '{echo': without the space,{echois one word, not the reserved word{. Andgreet() { echo hi }leaves the function open, because the}is just another argument to echo:syntax error: unexpected end of file. - Using
returnfor data.return "$result"fails for non-numbers and truncates numbers over 255 (return 300gives status 44). - Expecting the script's
$1inside a function. It is the function's$1. Pass what you need:process_file "$1"ormain "$@". - Naming a function like a command. A function called
testorlschanges how every later call behaves.
Exercises
die builds the two helpers almost every script needs; a test checks that errors go to stderr. is-even is a predicate function used directly as a condition, checked against invalid input, an empty argument and the exit status of the whole script.
Recap
name() { commands; }defines a function; calling it is like running a command.- Inside,
$1...$#"$@"are the function's own arguments; the caller's are restored after. - The status is the last command's, or
return N;returncannot return text. exitin a function ends the whole script: ideal fordie.- Calls run in the current shell: no fork, same variables and directory.
Going further: help return, help declare. Run declare -f | head -40 in the sandbox shell to read some of the completion functions bash has loaded; real-world function style in the wild.