Skip to content
5 min read·Lesson 10 of 10

ShellCheck and Best Practices

A rigorous checklist for production-quality shell scripts, plus the tools that catch problems before they bite.

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 your rm -rf to 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

  1. Shebang: #!/usr/bin/env bash (not /bin/sh).
  2. set -euo pipefail at the top.
  3. IFS=$'\n\t' if you handle filenames.
  4. Quote every variable expansion: "$var".
  5. Use $(...), never backticks.
  6. Use [[ ... ]], never [ ... ].
  7. Prefer arrays over space-separated strings.
  8. Always local inside functions.
  9. Validate inputs at the top; fail with a clear message.
  10. trap for cleanup of temp files and resources.
  11. Send errors to stderr (>&2); use stdout only for results.
  12. Document with a usage function and --help.
  13. 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 ls output. Use find, globs, or arrays.
  • Using eval on user input. Almost always a security bug.
  • cat file | grep ... — useless cat; just grep ... file.
  • Reading a file with for line in $(cat file) — splits on whitespace, not lines.
  • Using which in scripts (non-portable). Prefer command -v.
  • Hardcoding paths to tools without checking them with command -v at startup.

Security Hygiene

  • Never run untrusted scripts as root.
  • Never echo secrets — they end up in logs and process tables.
  • Don't use curl | bash for 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

CertScope
RHCSA / LFCSWriting reliable, idempotent admin scripts
AWS / Azure / GCPBootstrap and lifecycle scripts that must not silently fail
CKAHelper 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.

Key Takeaways

  • Run ShellCheck on every script — it catches real bugs.
  • Strict mode, quoting, and IFS discipline prevent most footguns.
  • Prefer Bash over sh unless you need POSIX portability.
  • Use shfmt to keep formatting consistent.
  • Test scripts with bats; treat them as code.
🎉

Course Complete!

You've finished Bash and Shell Scripting. Now put your knowledge to the test with real exam-style practice questions.