CodeRigor

LESSON

Write functions and finish a complete program

Name a step as a function, pass it arguments with $1, keep its variables local, capture its output with $( ), and combine everything into one complete interactive script.

As scripts grow, the same few steps turn up again and again, and a long list of statements becomes hard to read. A function is a block of statements with a name. You write the block once, and from then on its name works like a command of your own.

Define once, call many times

Bash
say_hello() {
  echo "Hello!"
  echo "Welcome to the shop."
}

say_hello
say_hello

This prints the two lines twice:

Text
Hello!
Welcome to the shop.
Hello!
Welcome to the shop.

The first four lines define the function: its name, a pair of round brackets, and a block between { and }, called the function's body. Defining prints nothing. It teaches Bash a new word.

Writing the name on a line of its own calls the function: Bash runs the body, then carries on with the line after the call. A call looks exactly like any other command. There are no brackets in the call.

In other people's scripts you may see function say_hello {. That is another way of writing the same definition.

Arguments: $1 and $2

Like echo, a function can be given arguments: write them after the name, separated by spaces. Inside the body, $1 is the first argument and $2 is the second.

Bash
say_hello() {
  echo "Hello, $1! Table $2 is ready."
}

say_hello "Ada" 4
say_hello "Grace Hopper" 12

This prints Hello, Ada! Table 4 is ready. and Hello, Grace Hopper! Table 12 is ready.

Arguments are separated by spaces, so an argument that contains a space needs quotation marks, as "Grace Hopper" has here. The same is true when the argument is a variable: say_hello "$name" "$table". This is the quoting habit from lesson 2 doing real work. Without the quotes, a name with a space in it would arrive as two arguments.

local: variables that belong to the function

$1 says nothing about what the value means. Give each argument a name on the first lines of the body, with the word local in front:

Bash
area() {
  local width="$1"
  local height="$2"
  echo "A $width by $height room has an area of $(( width * height ))."
}

area 3 4

This prints A 3 by 4 room has an area of 12.

local makes a variable that exists only while the function is running. That matters because every other variable in a script is shared by the whole script, functions included. Without local, a function can silently change a variable that the rest of the script is using:

Bash
add_tax() {
  total=$(( $1 + 2 ))
  echo "With tax: $total"
}

total=100
add_tax 5
echo "Total: $total"

This prints With tax: 7 and then Total: 7. The script's own total of 100 has been lost, because the function used the same name. With local total=$(( $1 + 2 )) in the function, the last line prints Total: 100. Declare every variable you create inside a function with local.

Getting a result back

Often you want a function to work something out and hand the result back to the line that called it. In Bash a function hands back a result by printing it, and the caller collects what was printed with $( ).

Bash
double() {
  local n="$1"
  echo "$(( n * 2 ))"
}

result=$(double 21)
echo "Twice 21 is $result"

This prints Twice 21 is 42.

$(double 21) runs the command between the brackets and is replaced by whatever that command printed. Nothing appears on the screen at that point: the output is captured and, here, given the name result.

A call, its argument, and the result that comes back
start / 4 ⇄ code
The argument 21 arrives in the function as $1. The function prints its answer, and $( ) captures that output so that the caller can name it.
A call, its argument, and the result that comes back
The argument 21 arrives in the function as $1. The function prints its answer, and $( ) captures that output so that the caller can name it.

Two pairs of brackets look alike and do different jobs:

  • $(( ... )), with two round brackets, calculates.
  • $( ... ), with one, runs a command and captures its output.

Bash also has a return statement, but it does not carry a result the way return does in other languages: it can only report success or failure as a small number. A later unit of this course covers it. Until then, a function's result is what it prints.

Functions that decide

A function body can hold anything a script can: decisions, loops, calls to other functions.

Bash
describe() {
  local temperature="$1"
  if [[ "$temperature" -lt 10 ]]; then
    echo "cold"
  elif [[ "$temperature" -lt 20 ]]; then
    echo "mild"
  else
    echo "warm"
  fi
}

for t in 4 15 27; do
  word=$(describe "$t")
  echo "$t degrees is $word."
done

This prints 4 degrees is cold., 15 degrees is mild. and 27 degrees is warm. The loop is short and readable because the three-way decision has a name.

A complete program

Here is a whole script that uses everything from this unit: variables, input, arithmetic, a loop, a decision and a function. It adds up prices typed in pence and prints the total in pounds.

Bash
#!/usr/bin/env bash
# Add up prices typed in pence and show the total in pounds.

# show_pounds PENCE prints an amount such as 12.05
show_pounds() {
  local pence="$1"
  printf '%d.%02d\n' "$(( pence / 100 ))" "$(( pence % 100 ))"
}

echo "How many items?"
read -r count

total=0
for (( i = 1; i <= count; i++ )); do
  echo "Price of item $i in pence?"
  read -r price
  total=$(( total + price ))
done

if [[ "$total" -ge 2000 ]]; then
  echo "Delivery is free."
else
  total=$(( total + 350 ))
  echo "Delivery costs 3.50."
fi

pounds=$(show_pounds "$total")
echo "To pay: $pounds pounds"

With 2, 450 and 405 typed, the script asks its three questions and then prints Delivery costs 3.50. and To pay: 12.05 pounds. Copy it into the Bash playground and try it with other prices.

The structure of the complete program
start / 5
Functions are defined first, so that they exist before anything calls them. The main part then follows the familiar shape: read, repeat, decide, print.
The structure of the complete program
Functions are defined first, so that they exist before anything calls them. The main part then follows the familiar shape: read, repeat, decide, print.

When you write a program of your own, work in this order too. Decide what the output should look like, write any small functions and try each one with a few values, then write the main part one step at a time, running the script after each step.

Mistakes you will meet

Calling a function before it is defined. Bash reads a script from the top, so a function exists only after its definition has been read.

Bash
say_hello "Ada"

say_hello() {
  echo "Hello, $1!"
}
Text
solution.sh: line 1: say_hello: command not found

This is the familiar message for an unknown command: on line 1, Bash has not yet met the word say_hello. Put function definitions at the top of the script.

Forgetting $( ) around a call.

Bash
double() {
  local n="$1"
  echo "$(( n * 2 ))"
}

result=double 21
echo "Twice 21 is $result"
Text
solution.sh: line 6: 21: command not found
Twice 21 is 

Without $( ), line 6 is not a call of double at all, and Bash ends up trying to run 21 as a command. When a message says a value of yours is a "command not found", look for missing $( ) or a stray space on that line.

Brackets or commas in a call. say_hello("Ada", 4) is how many other languages call a function. In Bash it is a syntax error. A call is the name, then the arguments separated by spaces: say_hello "Ada" 4.

Forgetting local. There is no message. A variable elsewhere in the script changes when you did not expect it to, as total did above.

What you can do now, and where to go next

You can define a function, pass it arguments, keep its variables local, capture its result, and build a complete interactive script from variables, input, decisions, loops and functions. That is the core of programming in any language. The capstone build asks you to write one more complete program: a number-guessing game.

When you have finished it, continue with the next unit of this course, which starts at arguments, quoting and expansion. It moves faster than this unit and assumes exactly what you now know. Its first lesson explains in full why every "$name" has been in double quotes, and from there the course goes on to conditions in depth, arrays, pipelines, files and writing your own command-line tools.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →