Shell Scripting Basics with Safe, Fail-Fast Bash
I wrote reliable Bash scripts using a strict mode (set -euo pipefail), quoted variables, and conditionals, then linted with shellcheck. Fail-fast settings made scripts abort on the first error instead of silently continuing in a broken state.
Objective & Context
Most shell bugs come from unquoted variables and ignored errors. This lab establishes a safe scripting baseline and static analysis, the foundation the advanced scripting and sysadmin labs build on.
Learning Objectives
- Write a strict-mode Bash skeleton with set -euo pipefail and a safe IFS.
- Quote variables correctly to survive spaces and empty values.
- Use positional parameters with default and required-value patterns.
- Write conditionals and loops that handle errors explicitly.
- Lint scripts with shellcheck and interpret exit statuses.
Environment & Prerequisites
Prerequisites
- Bash 5.x (check with
bash --version) and shellcheck:sudo apt install -y shellcheckorsudo dnf install -y ShellCheck. - A small task to automate, such as archiving a directory.
- A scratch directory for test scripts:
mkdir -p ~/lab/scripts. - Comfort with basic commands and redirection from earlier labs.
Step-by-Step Execution
1. Strict-mode script skeleton
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
target="${1:?usage: backup.sh DIR}"
tar -czf "backup-$(date +%F).tgz" "$target"
Expected output (the script itself is the artifact):
(script saved as backup.sh and made executable)
$ ls -l backup.sh
-rwxr-xr-x 1 taki taki 168 Jun 17 09:50 backup.sh
What this teaches: set -euo pipefail makes the script exit on any error, undefined variable, or failed pipe stage; ${1:?...} turns a missing argument into a usage error instead of silent misbehavior.
2. Lint before running
shellcheck backup.shWhat this teaches: shellcheck performs static analysis without running the code, catching quoting bugs, unreachable code, and Bashisms before they become 2 AM incidents.
3. Run and check exit status
chmod +x backup.sh && ./backup.sh ~/lab/data; echo "exit=$?"Expected output:
exit=0
What this teaches: echo $? (here exit=$?) inspects the exit status immediately; 0 means success. Use a directory you own for the first run: archiving /etc as a non-root user fills the output with permission-denied warnings and a nonzero exit. Checking statuses explicitly is what separates scripts from hopeful typing.
4. Guard with a conditional and loop over arguments
#!/usr/bin/env bash
set -euo pipefail
for target in "$@"; do
if [[ -d "$target" ]]; then
tar -czf "backup-$(basename "$target")-$(date +%F).tgz" "$target"
else
echo "skip: $target is not a directory" >&2
fi
done
Expected output:
$ ./backup-multi.sh ~/lab/data /nope
skip: /nope is not a directory
What this teaches: [[ -d ... ]] tests explicitly instead of letting tar fail obscurely, and "$@" loops over every argument safely quoted. Sending the skip message to stderr (>&2) keeps diagnostics out of any piped stdout. This conditional-plus-loop pattern is the backbone of most batch admin scripts.
How It Works
Most shell bugs come from two defaults: unquoted variables that word-split on spaces, and ignored failures that let a script continue after a command broke. Strict mode attacks both at the source. set -e aborts on the first failing command, -u turns unset variables into errors, and -o pipefail propagates failures through pipelines that would otherwise mask them with the last command's status.
Quoting is the other half of safety. "$var" preserves the variable as exactly one word even when it contains spaces; $var without quotes splits and globs. The rule is simple: quote every expansion unless you specifically want splitting, and shellcheck will remind you whenever you forget.
shellcheck exists because Bash fails silently in creative ways. Running it before every execution builds a lint-first habit: the analyzer knows hundreds of pitfalls (from useless cat to unquoted $1) and explains each with a wiki link. Combined with testing exit codes, it turns scripting from folklore into engineering.
Validation & Testing
Call the script with a missing argument and an invalid path and confirm it aborts with a clear non-zero exit. Pass criteria: shellcheck reports clean, undefined variables fail under -u, and errors abort under -e.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Script fails on an unset variable | set -u treats it as an error, which is the point | Use ${VAR:-default} for optional values or export the variable first |
| set -e did not stop on a failing command | The command was in a conditional or after && / || | Check the status explicitly with if ! cmd, or restructure the logic |
| Pipeline hid a failure | Without pipefail only the last stage's status counts | Keep set -o pipefail in every script header |
| shellcheck SC2086: quote this | An unquoted expansion that will word-split | Wrap in double quotes: "$var" |
| ./script.sh: permission denied | The file lacks the execute bit | Run chmod +x script.sh, or invoke with bash script.sh |
| Bad interpreter error | The shebang points to a missing path or has CRLF line endings | Use #!/usr/bin/env bash and convert line endings with dos2unix |
Advanced: Troubleshooting
- Word splitting: always quote
"$var"; set IFS deliberately. - Silent failures:
set -epluspipefailsurfaces errors in pipelines. - Unset variable crashes: provide defaults with
${var:-default}.
Key Results
- Adopted fail-fast strict mode as the script default.
- Caught common bugs pre-run with shellcheck.
- Eliminated word-splitting issues through consistent quoting.
- Returned meaningful exit codes for pipeline integration.
Key Takeaways
- Start every script with set -euo pipefail; it converts silent failures into loud ones.
- Quote every variable expansion unless you explicitly want word splitting.
- Use ${1:?...} to demand required arguments with a usage message.
- Lint with shellcheck before running, not after debugging.
- Exit statuses are data: check $? or branch on commands directly.
Practice Challenges
Challenge 1: Fail-fast archiver
Goal: extend backup.sh to refuse archiving when the target directory does not exist, exiting with status 2 and a clear message.
Hint: test with [[ -d $target ]] before tar runs.
Challenge 2: Lint clean
Goal: write a 15-line script that passes shellcheck with zero warnings on the first run.
Hint: quote everything, prefer [[ ]] over [ ], and avoid backticks.