Section 1 of 12

1 Before you start

Before you begin, take a few minutes to check what you already know and how confident you feel. You will see the same questions again at the end of the module — this helps both you and us measure what you have learned. Click an option for every question and confidence rating, then click Next to continue.

Pre-test

Inside a function, what does $1 refer to?

Pre-test

How do you capture the output of a function called my_func into a variable?

Pre-test

What does return 1 do inside a bash function?

Pre-test

Why use local inside a function?

Pre-test

What does "$@" expand to inside a function?

Pre-test

Inside a function or script, what does $# hold?

Pre-test

What does $0 refer to?

Pre-test

Which is the correct way to define a function in bash?

Pre-confidence

I can define a bash function and call it with arguments.

Not at all confident
Fully confident
Pre-confidence

I can capture a function's output into a variable using $(...).

Not at all confident
Fully confident
Pre-confidence

I know why local matters and use it for every variable inside a function.

Not at all confident
Fully confident
Section 2 of 12

2 Introduction

As your bash scripts grow beyond a dozen lines, reading them from top to bottom can start to feel overwhelming. To keep things organized, you will want to group chunks of code into named blocks so your script reads more like a clean outline.

These blocks are called functions. Interestingly, functions and scripts handle inputs in the exact same way. Both topics use the same mental model.

In this lesson we will cover:

  • Defining a function and calling it.
  • Reading arguments inside a function or script: $1, $2, $@, $#.
  • Return values - the difference between echo and return.
  • Local variables and why you want them.
A Bash function receives its inputs through the same positional parameters as a script ($1, $2, $@, $#), so the two share one input model.
A Bash function receives its inputs through the same positional parameters as a script ($1, $2, $@, $#), so the two share one input model.
Section 3 of 12

3 Defining and Calling a Function

Think of a function as a mini-script living inside your main script. To create one, you write the name, add empty parentheses (), and put the commands inside curly braces {}.

greet() {
    echo "Hello, cohort"
}

greet

If you have programmed in languages like Python, you are used to calling functions with parentheses, like greet(). Bash is different. To run a function, you just type its name like any other terminal command. No parentheses!

In Bash you define a function with () and {}, but you run it by typing its name on its own, with no parentheses.
In Bash you define a function with () and {}, but you run it by typing its name on its own, with no parentheses.

You can write a function on one line like greet() { echo "Hi"; }, but you must include that final semicolon before the closing brace. Usually, it is best to stick to the multi-line format above for readability.

greet() { echo "Hello, cohort"; }
Section 4 of 12

4 Arguments - $1, $2, $@, $#

Because you don't use parentheses to call functions, you don't put data inside them either. Instead, you just add spaces, exactly like you do when running terminal commands.

Inside the function, bash automatically assigns these inputs to numbered variables starting with a dollar sign ($1, $2, etc.).

introduce() {
    echo "Hi, patient $1 was seen for $2"
}
introduce P-001 hypertension
A bash function receives its space-separated arguments as positional parameters, so the first value becomes $1 and the second becomes $2 in the order they are passed.
A bash function receives its space-separated arguments as positional parameters, so the first value becomes $1 and the second becomes $2 in the order they are passed.

Whether you are reading inputs passed to a function, or inputs passed to the whole script from the terminal, bash provides a few special variables to help you manage them:

  • $1, $2, $3 ... - the first, second, third argument.
  • $0 - the name of the script (or the function) itself.
  • "$@" - all arguments as a list. Quotes matter here.
  • $# - the count of arguments.
  • "$*" - all arguments joined into one string. Almost always you want "$@" instead.
Bash splits a command's words into numbered parameters (0 the name, $1 onward the arguments), with $# counting only the arguments, " @" exposing them as a separate list, and "$*" joining them into a single string.
Bash splits a command's words into numbered parameters (0 the name, $1 onward the arguments), with $# counting only the arguments, " @" exposing them as a separate list, and "$*" joining them into a single string.
summarise_args() {
    echo "I got $# arguments"
    for arg in "$@"
do
        echo "  - $arg"
    done
}

summarise_args P-001 P-002 P-003
Try it out

Try this snippet in the Bash Scratchpad on the right.

Try this snippet
summarise() {
    echo "got $# arguments"
    for arg in "$@"
do
        echo "  - $arg"
    done
}
summarise P-001 P-002 P-003
Section 4.1 of 12

4.1 shift - drop the first argument

Sometimes you want to treat the very first argument differently than the rest. The shift command acts like a conveyor belt. It drops $1 entirely, and shifts everything else down the line (what was $2 becomes $1, what was $3 becomes $2).

process_first_then_rest() {
    echo "first: $1"
    shift
    echo "the rest: $@"
}

process_first_then_rest a b c d
In bash, shift discards the first positional parameter and renumbers the rest, so $2 becomes $1, $3 becomes $2, and the argument count $# decreases by one.
In bash, shift discards the first positional parameter and renumbers the rest, so $2 becomes $1, $3 becomes $2, and the argument count $# decreases by one.
Section 5 of 12

5 Returning Values - echo vs return

If you have programmed in Python or R, you are used to writing functions that calculate a value and then return that value.

Bash functions do not return data. Instead, bash has two completely separate output channels: one for passing data, and one for passing a status.

Section 5.1 of 12

5.1 echo (or printf) - the actual "return value"

If you want your function to give you back a string, a number, or any actual data, you must use echo (or printf). Think of echo as the function shouting its answer out loud.

To actually "catch" that shouted answer and save it to a variable, you use command substitution $(...), exactly like you would with a regular command.

bmi_category() {
    # crude integer category given an integer BMI in $1
    local bmi=$1
    if [[ $bmi -lt 19 ]]
then echo "underweight"
    elif [[ $bmi -lt 25 ]]
then echo "normal"
    elif [[ $bmi -lt 30 ]]
then echo "overweight"
    else echo "obese"
    fi
}

cat=$(bmi_category 28)
echo "category: $cat"
A Bash function returns data by echoing it to standard output, which the caller captures into a variable using command substitution $(...).
A Bash function returns data by echoing it to standard output, which the caller captures into a variable using command substitution $(...).
Section 5.2 of 12

5.2 return - sets the EXIT CODE only

In bash, the return keyword is strictly used to set an exit code (a number between 0 and 255). It is used to answer the question: "Did this function succeed or fail?" It acts exactly like the exit command does for a whole script, but just for the function.

  • return 0 = Success
  • return 1 (or any other number) = Failure
is_empty_file() {
    if [[ ! -s "$1" ]]
then
        return 0 # success = yes, empty
    else
        return 1
    fi
}

if is_empty_file cohort_2026.csv
then
    echo "cohort file is empty"
fi
In bash, return sets an exit code that signals success or failure rather than handing back a value, where 0 means success and any non-zero code means failure.
In bash, return sets an exit code that signals success or failure rather than handing back a value, where 0 means success and any non-zero code means failure.

In summary, choosing whether to use echo or return depends entirely on the specific goal of your function.

You should use echo when you need your function to pass back actual data, such as strings, mathematical results, or text.

Conversely, you should use return when your only goal is to signal whether the function succeeded or failed, essentially acting as a basic true or false flag.

Try it out

Try this snippet in the Bash Scratchpad on the right.

Try this snippet
bmi_category() {
    local bmi=$1
    if [[ $bmi -lt 25 ]]
then
        echo "normal"
    else
        echo "high"
    fi
}
category=$(bmi_category 28)
echo "category: $category"
In bash a function uses two separate channels, echo writes real data to stdout for the caller to capture with $(...), while return sets only a 0 to 255 exit status read from $? that signals success or failure.
In bash a function uses two separate channels, echo writes real data to stdout for the caller to capture with $(...), while return sets only a 0 to 255 exit status read from $? that signals success or failure.
Section 6 of 12

6 Local Variables - Why You Want Them

By default, every variable you create in a bash function is global. That means it "leaks out" of the function and can overwrite (shadow) variables with the same name in the rest of your script. It is incredibly easy to do this by accident.

count_visits() {
    i=0 # uh oh - global
    while [[ $i -lt 3 ]]
do
        i=$((i + 1))
    done
}

i=999
count_visits
echo $i # prints 3, not 999

Because the function used the global variable i, it accidentally destroyed the 999 we set earlier.

To prevent this, you simply add the word local in front of any new variable you introduce inside a function. This locks the variable inside the function, so it disappears as soon as the function finishes running.

count_visits() {
    local i=0
    while [[ $i -lt 3 ]]
do
        i=$((i + 1))
    done
}

i=999
count_visits
echo $i # prints 999

Make this a hard habit. Every variable introduced inside a function should be local unless you specifically need it to leak out.

In Bash a variable assigned inside a function is global by default and overwrites any same-named variable in the enclosing scope, so declaring it with local confines it to the function and leaves the outer value untouched.
In Bash a variable assigned inside a function is global by default and overwrites any same-named variable in the enclosing scope, so declaring it with local confines it to the function and leaves the outer value untouched.
Section 7 of 12

7 Putting It Together

Now that we have covered permissions, find, function outputs (echo vs. return), and local variables, let's look at what a professional, well-structured bash script actually looks like.

Here is a complete script that counts the number of rows in every patient vitals file and saves the results to a new document.

#!/usr/bin/env bash
# count rows in every patient vitals file and print a tsv

count_rows() {
    local f="$1"
    # subtract 1 for the header
    echo $(( $(wc -l < "$f") - 1 ))
}

report_patient() {
    local f="$1"
    local base
    base=$(basename "$f" .csv)
    local n
    n=$(count_rows "$f")
    echo -e "$base\\t$n"
}

for f in vitals/patient_*.csv
do
    report_patient "$f"
done > visit_counts.tsv

Walk through it slowly: We loop over the patient vitals files, use basename to strip away the messy folder paths and .csv extensions, use wc -l inside a custom function to count the lines (subtracting one to safely ignore the header), and then spit out a clean, tab-separated row. By redirecting the output of the entire loop using > visit_counts.tsv, we generate a complete, perfectly formatted data report using just a handful of organized code.

A redirection placed after done attaches to the entire loop, so the file is opened a single time and every line the loop prints is collected into that one file rather than the file being reopened on each pass.
A redirection placed after done attaches to the entire loop, so the file is opened a single time and every line the loop prints is collected into that one file rather than the file being reopened on each pass.

The top of a real-world script looks exactly like this—your tools and functions are defined at the top, and the main logic sits cleanly at the bottom. This modular approach is infinitely easier to read, debug, and maintain than writing the same logic as one giant, continuous block of code.

Try it out

Try this snippet in the Bash Scratchpad on the right.

Try this snippet
count_rows() {
    local f="$1"
    echo $(( $(wc -l < "$f") - 1 ))
}
count_rows vitals/patient_001.csv
Section 8 of 12

8 Check your understanding

You have reached the end of the module. Try the same questions again — your answers here, paired with your pre-test answers, are how we measure what the module taught you. Answer every question and confidence rating, then click Submit and see results to view your score.

Post-test

Inside a function, what does $1 refer to?

Post-test

How do you capture the output of a function called my_func into a variable?

Post-test

What does return 1 do inside a bash function?

Post-test

Why use local inside a function?

Post-test

What does "$@" expand to inside a function?

Post-test

Inside a function or script, what does $# hold?

Post-test

What does $0 refer to?

Post-test

Which is the correct way to define a function in bash?

Post-confidence

I can define a bash function and call it with arguments.

Not at all confident
Fully confident
Post-confidence

I can capture a function's output into a variable using $(...).

Not at all confident
Fully confident
Post-confidence

I know why local matters and use it for every variable inside a function.

Not at all confident
Fully confident
Section 9 of 12

9 Your results

Here is how your post-test answers compare with your pre-test answers. The pre/post pairing is the most reliable way to see what this module actually taught you.

Your score

Submit the post-test to see your results.

Muddiest point

What is the one thing from this module that is still unclear to you?

Rate this module

Overall, how would you rate this module?

How likely are you to recommend this module to a peer? (0 = not at all, 10 = extremely likely)