Skip to content

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:

#!/usr/bin/env bash
set -euo pipefail

And every variable reference must be quoted:

"$variable"        # not $variable
"${array[@]}"      # not ${array[@]}
"$1" "${@}"        # not $1 $@

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 awk programming 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

  1. 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?

  1. 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.

  1. 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.


day05-part1-capstone | index