Best Practices, ShellCheck & Portability¶
The difference between a script that works on your machine and one that works on a production server is attention to portability, safety flags, and linting.
Learning Objectives¶
- Apply the full set of bash best practices consistently
- Use ShellCheck to catch bugs before they reach production
- Write portable scripts that run on bash 4+ and POSIX sh
- Know when to reach for a different tool (Python, awk) instead of bash
The Non-Negotiables¶
Every production bash script must have these:
And every variable reference must be quoted:
ShellCheck¶
ShellCheck (shellcheck.net) is the single most impactful tool for writing better bash scripts.
# Install
apt install shellcheck # Debian/Ubuntu
brew install shellcheck # macOS
# Run
shellcheck script.sh
shellcheck *.sh
# Common warnings and fixes
| ShellCheck Code | Issue | Fix |
|---|---|---|
| SC2086 | $var not quoted |
Use "$var" |
| SC2046 | $(cmd) not quoted |
Use "$(cmd)" |
| SC2006 | Backtick substitution | Use $(cmd) |
| SC2045 | for f in $(ls) |
Use glob for f in * |
| SC2164 | cd without checking exit |
Use cd dir || exit 1 |
| SC2155 | local var=$(cmd) |
Separate: local var; var=$(cmd) |
local var=$(cmd) loses the exit code
local var=$(cmd) always succeeds because local returns 0 even if cmd fails. Use two lines: local var; var=$(cmd).
Portability Checklist¶
If your script must run on different systems (different Linux distros, macOS, old servers):
| Check | Portable | Bash-only |
|---|---|---|
| Shebang | #!/usr/bin/env bash |
#!/bin/bash |
| String comparison | = in [ ] |
== in [](<#>) |
| Arrays | No (bash 4+) | declare -a arr |
| Assoc arrays | No (bash 4+) | declare -A map |
[](<#>) |
No | Yes |
(( )) |
No | Yes |
${var,,} lowercase |
bash 4+ | Yes |
read -r |
Yes (POSIX) | Yes |
printf |
Yes (POSIX) | Yes |
echo -e |
No | Use printf |
For maximum portability, target /bin/sh and avoid all bash-specific features. For most scripting work, targeting bash 4.0+ is fine — just document the minimum version.
When Not to Use Bash¶
Bash is the right tool when: - You are gluing existing tools together with pipes - The script is under ~200 lines - File manipulation is the primary task
Reach for Python (or another language) when:
- You need complex data structures (dicts of lists, nested objects)
- You need real error handling with exceptions
- You are doing heavy string parsing or regex processing
- You need HTTP/JSON interaction beyond simple curl calls
- The logic is complex enough that testing matters
Quote
"Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface." — Doug McIlroy, Unix philosophy
A Checklist for Every Script¶
Before committing any script, verify:
□ #!/usr/bin/env bash at the top
□ set -euo pipefail on line 2
□ All variables quoted
□ All arrays iterated with "${arr[@]}"
□ Temp files cleaned up with trap EXIT
□ Errors go to stderr (>&2)
□ --help / -h flag implemented
□ Script works with no arguments (or errors clearly)
□ shellcheck passes with no warnings
□ Works with filenames containing spaces (test it)
□ Tested the happy path and at least two error paths
printf vs echo¶
Prefer printf over echo for output that contains variables — echo behavior varies across shells and with flags like -e or -n.
echo -e "line1\nline2" # works in bash, but -e is not POSIX
printf "line1\nline2\n" # always works, always safe
printf "Name: %s\n" "$name" # formatting
Recap: The Full Toolbox¶
After two weeks, you have:
| Category | Tools Covered |
|---|---|
| Navigation | cd, ls, pwd, find, locate |
| Files | cp, mv, rm, chmod, chown, tar, rsync |
| Text | cat, head, tail, grep, sed, awk, cut, sort, uniq, wc |
| Variables | assignment, quoting, parameter expansion, arrays |
| Control flow | if, case, for, while, until |
| Functions | definition, arguments, local, sourcing |
| Error handling | set -euo pipefail, trap, die() |
| Automation | cron, at, systemd timers |
| Networking | curl, wget, ssh, scp, jq |
| Linting | shellcheck |
Next Steps¶
- Practice: Build the remaining projects from the Projects section
- Deepen: Learn
awkprogramming thoroughly — it is a language unto itself - Explore: Look at
bats(Bash Automated Testing System) for unit-testing your scripts - Broaden: Learn Python for tasks where bash hits its limits — the two complement each other well
Interview Questions¶
- What is the first thing you check when a bash script works locally but fails in production?
Show answer
The environment. Production systems often have a different $PATH, different locale settings, different bash version, and stricter file permissions. Check: (1) are all commands being called with full paths or is $PATH set? (2) Are any bash 4+ features being used on a system with bash 3 (macOS)? (3) Does the script depend on environment variables that are set in your .bashrc but not in cron or CI?
- What is ShellCheck and why should every shell programmer use it?
Show answer
ShellCheck is a static analysis tool that reads bash (and sh) scripts and reports bugs, anti-patterns, and portability issues. It catches things like unquoted variables, for x in $(ls), lost exit codes from local var=$(cmd), and hundreds of other issues that even experienced programmers miss. Running shellcheck before committing a script is the single highest-leverage habit in shell scripting.
- When should you stop writing bash and switch to Python?
Show answer
When any of these apply: the script exceeds ~200 lines and is growing; you need complex data structures (nested dicts, objects); you need proper unit tests; you are doing significant JSON parsing or HTTP interaction; or a colleague will need to understand and modify the script. Bash is excellent for gluing tools together; Python is better for logic-heavy programs. The ability to recognize which tool fits is a key skill.