Advanced Bash Scripting
Go beyond the basics. In this lesson you'll write real-world bash: functions that return data and status, arrays, modern [[ ]] tests and case branching, "strict mode" safety with set -euo pipefail , cleanup with trap , debugging traces, and powerful parameter-expansion tricks.
Learn Advanced Bash Scripting in our free Command Line course — an interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Cli course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
1. Functions: Arguments, Scope & Return
Define a function with name() { ... } and call it like any command. Inside, the arguments are $1 , $2 , and $@ for all of them — exactly like a script. Use local so a function's variables don't leak out. A function doesn't return data with return ; instead it echoes its result (you capture it with result=$(myfunc arg) ), and uses return N only to set an exit status ( 0 – 255 ).
2. Arrays
An array holds many values under one name: arr=(apple banana cherry) . Read one element by index with ${ arr[0] } (arrays are 0-based), all elements with ${ arr[@] } , and the count with ${ #arr[@] } . Append with arr+=(date) , and loop safely with for x in " ${ arr[@] } "; do ... done (the quotes keep elements with spaces intact).
3. Modern Tests with [[ ]]
[[ ]] is bash's modern conditional — safer than [ ] with strings, and it understands == (with glob pattern matching), =~ (regex), and && / || inside the brackets. Handy string tests: -z (empty), -n (non-empty); file tests: -f (regular file), -d (directory). The older [ ] still works and is more portable, but in bash scripts prefer [[ ]] .
4. The case Statement
When you'd write a long if/elif/elif chain comparing one value against many options, a case statement is cleaner: case "$x" in pattern) ... ;; *) ... ;; esac . Each clause ends with ;; , patterns can use globs (and | to list several), and *) is the catch-all default . Close the whole thing with esac (that's "case" backwards).
5. Safety: set -euo pipefail
Add this near the top of any serious script — it's often called "bash strict mode." set -e exits the moment any command fails (so the script can't blunder on with bad state). set -u turns the use of an unset variable into an error (catching typos in names). set -o pipefail makes a pipeline fail if any stage fails, not just the last one — so a broken command in the middle can't hide.
6. Cleanup with trap
trap registers a handler to run when a signal or special event fires. The classic use is guaranteed cleanup: trap 'rm -f "$tmpfile"' EXIT deletes a temp file whenever the script ends — success, error, or Ctrl-C. You can also catch interrupts explicitly with trap ... INT TERM . This is far more reliable than a delete line at the bottom, which is skipped if the script exits early.
7. Exit Codes & Debugging
Every script ends with an exit code : exit 0 means success, any non-zero means failure. $? holds the exit code of the last command, and functions report status with return . To debug , turn on a trace with set -x — bash prints each command (with its values expanded) as it runs — and set +x turns it back off. You can also trace a whole file with bash -x script.sh .
8. Parameter Expansion Tricks
Parameter expansion edits a value as you read it , with no external tools. The big ones: ${ VAR:-default } uses a default if VAR is unset/empty; ${ VAR:=default } also assigns it; ${ #VAR } is the string length; ${ VAR%.txt } strips a suffix; ${ VAR#prefix } strips a prefix; and ${ VAR/old/new } replaces text. These replace whole pipelines of sed / cut calls.
🎯 Your Turn: finish the function
The function is almost done — fill in the blanks marked ___ using the # 👉 hints, then run it and check the Output panel matches.
🎯 Your Turn: work with an array
Two blanks: get the element count, then expand every element in the loop. Then run it.
Pro Tips
- 💡 Always quote " ${ arr[@] } " in loops. Unquoted, elements containing spaces get split into extra words — quoting keeps each element whole.
- 💡 Capture output, not status: a function's result comes from $(myfunc) ; return only sets $? . Don't try to read data from return .
- 💡 Adopt set -euo pipefail early in real scripts. It turns silent, late-night bugs into loud, immediate failures you can actually find.
- 💡 Trace with set -x when a script misbehaves — seeing each command with its expanded values is usually faster than scattering echo statements.
Common Errors (and the fix)
- Function "returns the wrong number" — you used return 100 expecting to read 100. return only sets an exit status (0–255). Echo the value and capture it with $(...) instead.
- Loop only sees one giant item — you wrote ${ arr[*] } or forgot the quotes. Loop over " ${ arr[@] } " to get each element separately.
- "unbound variable" after adding set -u — you referenced a variable that may be unset. Give it a default: " ${ VAR:- } " , or set it before use.
- case "syntax error: unexpected end of file" — a clause is missing its ;; , or you forgot to close with esac . Every clause ends with ;; .
- Temp file left behind on Ctrl-C — your cleanup was a plain line at the end, which got skipped. Move it into trap 'rm -f "$tmpfile"' EXIT so it always runs.
📋 Quick Reference
Concept
Syntax
Notes
Function
f() { … }
$1 = first arg
Local var
local v=…
Stays inside the function
Capture output
r=$(f arg)
Reads what f echoes
Array all
${ arr[@] }
Quote it in loops
Array count
${ #arr[@] }
Number of elements
Append
arr+=(x)
Adds to the end
Modern test
[[ -f f && -n s ]]
==, =~, -z/-n, -f/-d
Case
case x in p) … ;; esac
;; per clause, *) default
Strict mode
set -euo pipefail
-e exit, -u unset, pipefail
Cleanup
trap '…' EXIT
Runs on any exit
Debug trace
set -x / set +x
Prints each command
Default value
${ VAR:-def }
Use def if unset/empty
Frequently Asked Questions
Mini-Challenge: safe backup-namer
No blanks this time — just a brief and an outline. Combine everything from this lesson: strict mode, an argument guard, a trap cleanup, parameter expansion to build the backup name, and a case for a friendly message. Check your output against the example in the comments.
🎉 Lesson Complete!
- ✅ Functions take args ( $1 …), keep vars private with local , echo results (capture with $(...) ), and set status via return
- ✅ Arrays: ${ arr[0] } one, ${ arr[@] } all, ${ #arr[@] } count, arr+=(x) append
- ✅ Prefer [[ ]] (==, =~, -z/-n, -f/-d) and branch many ways with case … esac
- ✅ Harden scripts with set -euo pipefail and guarantee cleanup with trap '…' EXIT
- ✅ exit 0 / $? track success; debug with set -x ; reshape values with parameter expansion
- 🏆 You've finished the Command Line course! You can now read files, navigate, pipe, and write robust bash scripts — go automate something.
Practice quiz
How do you declare a variable that stays private to a function?
- local var=value
- let var=value
- scope var=value
- private var=value
Answer: local var=value. Inside a function, 'local var=value' keeps the variable from leaking into the rest of the script.
Inside a function, how do you refer to its first argument?
- $first
- ${1st}
- $1
- $arg1
Answer: $1. A function receives positional arguments just like a script: $1 is the first, $2 the second, and so on.
How do you expand ALL elements of an array named arr?
- ${arr}
${arr[@]} expands to every element; quoted as "${arr[@]}" each element stays a separate word.
How do you get the number of elements in array arr?
- ${arr.length}
- ${#arr}
- $arr.count
${#arr[@]} gives the element count. Note ${#arr} would give the length of the FIRST element instead.
What does set -e do?
- Erase the environment
- Exit the script immediately if a command fails
- Echo every command
- Enable extended globbing
Answer: Exit the script immediately if a command fails. set -e (errexit) stops the script the moment any command returns a non-zero status.
What does set -u do?
- Update the PATH
- Undo the last command
- Unset all variables
- Treat unset variables as an error
Answer: Treat unset variables as an error. set -u (nounset) makes referencing an undefined variable an error instead of silently using an empty value.
What does pipefail do?
- Make a pipeline fail if any command in it fails
- Print the pipeline
- Buffer the pipe output
- Disable all pipes
Answer: Make a pipeline fail if any command in it fails. Normally a pipeline's status is that of the LAST command; set -o pipefail makes it fail if ANY stage fails.
What does 'trap cleanup EXIT' do?
- Trap network exits only
- Run the handler when the script exits
- Delete the cleanup function
- Ignore the EXIT signal
Answer: Run the handler when the script exits. trap registers a handler that runs on the named event; EXIT fires whenever the script terminates, perfect for cleanup.
What does ${VAR:-default} do?
- Always assign 'default' to VAR
- Delete VAR
- Compare VAR to 'default'
- Use 'default' if VAR is unset or empty
Answer: Use 'default' if VAR is unset or empty. ${VAR:-default} substitutes 'default' when VAR is unset or empty, WITHOUT changing VAR. Use := to also assign it.
What ends each clause in a case statement?
- done
- esac
- ;;
- fi
Answer: ;;. Each case clause ends with ;; and the whole statement is closed with esac.
Continue this course
- Previous: Networking Commands