| name | bash-safety |
| description | Enforce safe bash scripting practices when writing, reviewing, or fixing shell scripts. Covers quoting, arrays, conditionals, arithmetic, redirections, strict mode, and static analysis. Use when editing .sh/.bash files, reviewing shell scripts, fixing shellcheck warnings, or writing new bash code. |
Bash Safety & Best Practices
When writing or reviewing bash scripts, apply the rules below. For the
complete catalog of rules with detailed examples, see reference.md.
Sources: BashPitfalls,
Shellharden.
Rule 0: Run ShellCheck
Before any manual review, run static analysis:
shellcheck -o all script.sh
-o all enables every optional check (including those not on by default).
Fix all errors and warnings before proceeding. ShellCheck catches the
majority of the pitfalls listed here automatically.
Rule Categories (quick reference)
1. Quoting
The single most important rule. An unquoted variable undergoes word
splitting and pathname expansion (globbing). Always quote "$var" and
"$(cmd)".
cp $file $target
echo $msg
cp -- "$file" "$target"
printf '%s\n' "$msg"
Exceptions (quoting optional but never harmful): $?, $$, $!, $#,
${#array[@]}, right side of assignments (a=$b), inside [[ ]],
and inside case … in.
2. Arrays
Use real arrays when you need a list. Never use whitespace-delimited strings.
files="a b c"
for f in $files; do …; done
files=(a b c)
for f in "${files[@]}"; do …; done
Always iterate positional parameters with "$@", never $* or $@.
3. Conditionals & Tests
[ is a command (alias for test). Spaces around every argument are mandatory.
[[ ]] is a bash keyword with safer parsing.
[bar="$foo"]
[ bar == "$foo" ]
[ "$foo" = bar && "$bar" = foo ]
[ "$bar" = "$foo" ]
[[ $foo = "$bar" && $bar = "$baz" ]]
Unquoted RHS in [[ ]] is treated as a glob pattern. Quote it for literal
comparison: [[ $foo = "$bar" ]].
4. Loops & Iteration
Never parse ls or unquoted command substitutions in for.
for f in $(ls *.mp3); do …; done
for f in ./*.mp3; do
[ -e "$f" ] || continue
…
done
while IFS= read -r line; do
…
done < <(some_command)
5. Arithmetic
Use (( )) for integer math. Never use [[ $x > 7 ]] (string comparison).
[[ $foo > 7 ]]
[ $foo > 7 ]
(( foo > 7 ))
[ "$foo" -gt 7 ]
Validate user-supplied values before using them in arithmetic contexts to
prevent code injection.
6. Command Substitution
Prefer "$(cmd)" over backticks. Always quote the result.
dir=`dirname $f`
cd $(dirname "$f")
dir="$(dirname -- "$f")"
cd -P -- "$(dirname -- "$f")"
local var=$(cmd) masks the exit status of cmd. Separate declaration
from assignment:
local var
var=$(cmd)
rc=$?
7. Redirections & Pipes
Redirections are evaluated left to right. 2>&1 must come after the
stdout redirect:
somecmd 2>&1 >>logfile
somecmd >>logfile 2>&1
Each command in a pipeline runs in a subshell; variable changes are lost
after the loop:
grep foo bar | while read -r; do ((count++)); done
while read -r; do
((count++))
done < <(grep foo bar)
Never read from and write to the same file in one pipeline:
sed 's/foo/bar/' file > file
sed -i 's/foo/bar/' file
sed 's/foo/bar/' file > tmp && mv tmp file
8. Filenames & Paths
Filenames can contain spaces, dashes, newlines, and glob characters.
- Prefix relative paths with
./ to prevent dash-as-option: cp "./$f" dest/
- Use
-- to end option parsing: rm -- "$file"
- Use
-print0 / read -d '' with find:
while IFS= read -r -d '' file; do
…
done < <(find . -type f -name '*.mp3' -print0)
9. Output
echo cannot safely print arbitrary data (leading -n, -e interpreted
as options; no -- support in GNU echo).
echo "$var"
printf '%s\n' "$var"
Never use the variable as the format string:
printf "$var"
printf '%s' "$var"
10. Script Structure
Hashbang
#!/usr/bin/env bash
Don't put options (-euo pipefail) in the hashbang; they can be overridden.
Strict mode
set -euo pipefail
Caveats:
set -u treats empty arrays as unset in bash < 4.4. Use a feature check
or omit -u for older bash.
set -e is ignored inside functions called as conditions (f && …), and
inside command substitutions. Always add explicit error checks.
pipefail can cause false failures when earlier pipeline stages exit
due to SIGPIPE (e.g. cmd | head).
Safer globbing
shopt -s nullglob globstar
nullglob prevents unmatched globs from being passed as literal strings.
globstar enables ** recursive globbing.
Dependency assertion
require() { hash "$@" || exit 127; }
require curl jq
11. Dangerous Patterns
| Pattern | Risk | Fix |
|---|
eval "$var" | Code injection | Avoid eval; use arrays |
$(( array[$x] )) | Injection via index | Validate $x first |
find -exec sh -c 'echo {}' | Code injection | Use sh -c '…' _ {} with "$1" |
export CDPATH=… | Breaks cd in child scripts | Don't export CDPATH |
echo "Hello!" (interactive) | History expansion | Use printf or set +H |
Remediation Workflow
When fixing an existing script:
- Run
shellcheck -o all script.sh and fix all findings
- Quote every unquoted variable and command substitution
- Replace
echo "$var" with printf '%s\n' "$var"
- Replace
for x in $(cmd) with while read loops
- Replace string-based lists with arrays
- Add
set -euo pipefail (with appropriate caveats)
- Add
shopt -s nullglob if globs are used
- Add
-- after commands that accept options before variable args
- Prefix relative paths with
./ where needed
- Re-run
shellcheck -o all and verify clean
For the complete rule reference with 40+ detailed rules and examples,
see reference.md.