Shell scripting has more pitfalls per line than any other widely used language. The good news: a small set of habits and one excellent linter prevent most of them.
ShellCheck
ShellCheck is a static analyser for shell scripts. It catches missing quotes, unset variables, deprecated syntax, common confusions — and explains each warning with a wiki link.
shellcheck script.sh
# Or in CI:
find . -name "*.sh" -exec shellcheck {} +
Editor integrations (VS Code, vim, Emacs) flag issues as you type. Use them.
Common warnings to memorise
- SC2086 — "Double quote to prevent globbing and word splitting." The most common bug.
- SC2046 — "Quote this to prevent word splitting." Same problem in
$(). - SC2155 — "Declare and assign separately."
local x=$(cmd)hides the exit code; split it. - SC2164 — "Use
cd ... || exit." A failed cd that you ignored sent yourrm -rfto the wrong place. - SC2034 — "Variable appears unused." Often a typo elsewhere.
Treat ShellCheck warnings as errors in CI. Fix or explicitly disable with a comment:
# shellcheck disable=SC2034
some_var="intentionally unused"
shfmt
shfmt formats shell scripts consistently:
shfmt -i 2 -ci -w script.sh
shfmt -d script.sh # diff mode for CI
Pick indentation once and forget about it.
The Production Checklist
- Shebang:
#!/usr/bin/env bash(not/bin/sh). set -euo pipefailat the top.IFS=$'\n\t'if you handle filenames.- Quote every variable expansion:
"$var". - Use
$(...), never backticks. - Use
[[ ... ]], never[ ... ]. - Prefer arrays over space-separated strings.
- Always
localinside functions. - Validate inputs at the top; fail with a clear message.
trapfor cleanup of temp files and resources.- Send errors to stderr (
>&2); use stdout only for results. - Document with a usage function and
--help. - Run ShellCheck in CI; fail the build on warnings.
Bash vs sh
If your script lives in a Bash environment (most servers, CI, dev machines), write Bash and enjoy [[, arrays, local, read -r -d '', $(...), etc.
Write strict POSIX sh only when you must — minimal containers, BSD targets, or Alpine without bash installed. Then ShellCheck with -s sh to enforce it.
Don't Reach for Shell When You Should Reach for Python
Shell is great for:
- Gluing programs with pipes.
- Short orchestration (under ~100 lines).
- Bootstrapping environments where Python may not be installed.
Shell is bad for:
- Anything with structured data (JSON, YAML beyond a few fields).
- Math beyond integers.
- Networking beyond curl.
- Complex logic with many branches.
When a script grows past ~150 lines, has nested logic, or processes JSON, rewrite in Python. The maintenance cost crosses the line.
Testing
Yes, you can test shell scripts.
- bats-core — Bash Automated Testing System. Each test is a function; assertions via standard helpers.
- shunit2 — older but still solid.
#!/usr/bin/env bats
@test "greet outputs hello" {
source ./mylib.sh
result=$(greet alice)
[ "$result" = "Hello, alice" ]
}
Worth it for shared libraries and any script that controls money or production data.
Library Patterns
For larger projects, factor common helpers into lib.sh and source it:
#!/usr/bin/env bash
set -euo pipefail
# locate the directory of this script reliably
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./lib.sh
source "$SCRIPT_DIR/lib.sh"
main() {
log "starting"
do_thing
log "done"
}
main "$@"
Anti-Patterns
- Parsing
lsoutput. Usefind, globs, or arrays. - Using
evalon user input. Almost always a security bug. cat file | grep ...— useless cat; justgrep ... file.- Reading a file with
for line in $(cat file)— splits on whitespace, not lines. - Using
whichin scripts (non-portable). Prefercommand -v. - Hardcoding paths to tools without checking them with
command -vat startup.
Security Hygiene
- Never run untrusted scripts as root.
- Never echo secrets — they end up in logs and process tables.
- Don't use
curl | bashfor installs in production. - Validate any user input that becomes part of a path or command.
- Set restrictive permissions on scripts holding credentials (
chmod 600).
Cert Mapping
| Cert | Scope |
|---|---|
| RHCSA / LFCS | Writing reliable, idempotent admin scripts |
| AWS / Azure / GCP | Bootstrap and lifecycle scripts that must not silently fail |
| CKA | Helper scripts around kubectl |
Closing
You now have the toolkit to use the shell as a productivity multiplier and to write scripts that survive contact with production. Pair this with the Linux Basics, Cloud Security, and CI/CD courses for the deployment context, and with the Python for DevOps course for everything shell shouldn't do. Shell scripting is small but rewards discipline; the patterns here will serve you for decades.