Bash from the ground up

Functions and their arguments

Naming a block of lines, passing values in as $1 and $2, collecting them with "$@", and the two ways a function can hand an answer back.

Part 7 of 12

Intermediate20 min readBack to Courses

A block with a name

A function is a group of lines with a name, so the same work can be done twice without being written twice. The name is followed by two brackets and a block, and everything after that is like any other script: the value the function prints is printed. The name is the command a caller writes, and the brackets are only a marker that this is a definition.

Inside a function, the values it was handed arrive as $1, $2 and so on, counting from one. "$@" is all of them, and it is written inside quotes so that a value containing a space stays one value. These are the same names a script uses for its own arguments, which is a small confusion worth knowing about early: inside a function they belong to the function. The same names are reused for every call, which is why a function cannot read the arguments of the call before it.

Two ways to answer, and keeping names tidy

A function can answer in two ways. The first is to print its answer and let the caller capture it. The second is to finish with an exit code, which asks a yes-or-no question rather than producing a value. A function that checks something is easiest to use when it answers with a code: then a line such as if is-big 12; then reads almost like English. A function that prints can produce several lines, and the caller captures all of them.

Names created inside a function are visible outside it unless you mark them local. In a short script that rarely matters; in a long one, local is what keeps a count in one function from quietly changing a name in another. A local name is restored when the function returns, so the caller’s value is untouched.

Arguments in, answers out

# "$@" is every argument, and $1 is the first one.
total() {
  sum=0
  for n in "$@"; do sum=$((sum + n)); done
  echo "$sum"
}
echo "total of 3 4 5 is $(total 3 4 5)"

# A name marked local stays inside its function.
double() {
  local n=$1
  echo $((n * 2))
}
echo "double 21 is $(double 21)"

# An answer as an exit code, used straight in a test.
is-big() {
  [ "$1" -gt 10 ]
}
if is-big 12; then echo "12 is big"; fi

Positional values and their count

A function receives its arguments exactly as a command does: $1 is the first, $2 the second, and $# is how many there are. ${1:-guest} supplies a default, and shift drops the first value so the rest can be handled in a loop. The same names inside a function refer to the function's arguments, not the script's. $0 is the script name, not the function name, which is a useful distinction when a message needs to say where it came from.

"$@" is every argument as a separate word, spaces intact. "$*" joins them into one word separated by the first character of IFS, usually a space. Loop over "$@" when the values are separate things; use "$*" only when a single joined string is genuinely what you want.

Scope, and the two exits

Names created in a function are global unless declared local. A local count keeps that name inside the function and restores the caller's value on return, which is what makes two functions safe to combine. The keyword only works inside a function; at the top level it is an error.

return ends a function with a status, while exit ends the whole script. A function that produces text prints it, and the caller captures it with $(...); a function that answers a question returns a status and is used directly in if. Mixing the two is how a function becomes hard to call.

Arguments are positional, not named

A function receives its arguments by position. $1 is the first, $2 the second, and there is no name attached to either. That is why a call with two similar values is easy to get backwards, and why a comment or a wrapper with named variables is worth the line. The function cannot tell what you meant, only what you passed.

Defaults are written in the function body, not in the signature: ${1:-guest} reads the first argument and substitutes a value when it is empty or missing. The pattern is the same expansion from the values chapter. A function with many defaults is easier to call, but each one is a branch the reader has to know about.

A total, and a yes-or-no function

Run it and read the last three lines. Then change the numbers handed to the total, and add a small function of your own that prints a greeting with an argument in it.

Output
Click "Run" to execute your code

The brackets in a function definition are decoration: values do not go between them. They are passed the way a command is called — after the name, separated by spaces.

Three mistakes: calling a function before its definition, because the shell reads top to bottom and the name does not exist yet; expecting return 5 to hand back the number five as text, when it only sets a status; and forgetting local, so a loop counter in one function overwrites a name used in another.

Try these next

  • Write a function that takes a name and a greeting, with a default for the greeting.
  • Print $# inside a function called with two values, then with none.
  • Write a function that returns 1 when a value is empty, and call it from an if.

Defaults, counts and a local name

Run it, call `label` with no second argument to see the default, and add a call with three words.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Inside a function, what is $1?