Skip to content
Lesson 8 of 14

Step 1 of 5 · Reading · ~3 min

Learn

Functions and Pipes

Functions

By the end of this lesson you can write a function that takes arguments, hands back either text or a yes/no answer, and keeps its variables from leaking into the rest of the script. Bash functions are miniature scripts inside your script: they take arguments the way a script does, report success or failure the way a command does, and "return" data the way commands do, by printing it.

Defining and calling

shout() {
    echo "${1^^}!"
}

shout "ready"        # READY!
shout "go"           # GO!

A call looks like any command, with no parentheses. Arguments arrive as positional parameters: $1, $2, and so on, plus $# (how many) and "$@" (all of them, each kept as one word, which is the only correct way to forward arguments). Nothing is declared in the parentheses after the name; they only mark "this is a function".

Two ways out: text and status

A function has two separate output channels, and they answer different questions.

Data leaves on stdout. return cannot carry a value, so a function prints its result and the caller captures it with command substitution (a later lesson covers it fully):

double() {
    echo "$(( $1 * 2 ))"
}

total=$(double 21)       # runs the function and captures what it printed
echo "$total"            # 42

Anything the function prints becomes part of the captured value, so a stray debug echo corrupts the result. Send diagnostics to stderr with echo "..." >&2.

Yes/no answers leave as the exit status. return N ends the function with status N, where 0 means success and anything else means failure. Because if branches on a command's status, a function can be used directly as a condition:

contains() {
    local needle=$1
    shift                       # drop $1, so "$@" is now just the list
    local item
    for item in "$@"; do
        if [[ "$item" == "$needle" ]]; then
            return 0            # found: stop right here, success
        fi
    done
    return 1                    # reached the end: not found
}

if contains "pear" apple pear plum; then
    echo "in stock"
fi

return leaves the function immediately, even from inside a loop. It leaves only the function; exit would end the whole script. A function that never says return reports the status of its last command.

local keeps your variables yours

Variables in bash are global by default, even when assigned inside a function:

tally() {
    i=100                # silently overwrites the caller's i
}

for i in 1 2 3; do
    tally
    echo "$i"            # prints 100 every time
done

local i creates a variable that exists only for the duration of the call. The rule has no exceptions worth learning: declare every variable inside a function with local, including loop counters. It is the cheapest bug insurance in the language, because a leaked name collides with the caller's variables in ways that depend on call order.

Common mistakes

  • return with data. return "$result" fails unless the value is a number from 0 to 255. Print the value instead.
  • Printing when you meant a status. A yes/no function that also echoes something pollutes every if and $( ) that uses it.
  • Forgetting local on the loop variable. for i in ... inside a function changes the caller's i.
  • Using $0 for the first argument. $0 is the name of the script. The first argument is $1.
  • Unquoted calls. check $name splits the value into several arguments if it contains spaces; write check "$name".

Your exercise

Finish a yes/no function that decides whether a number is prime, and let the script's own loop use it to list primes. Decide what the function reports and through which channel. The loop that calls it uses i and prints it right after each call, so a function that clobbers i gives wrong output.

Up nextPipes and RedirectionFunctions and Pipes

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…