Linux Basic — Common Scripting Bugs
Common Scripting Bugs
After enough debugging sessions, the same handful of bugs come up over and over. Here are the ten you’ll encounter most — and the fix for each.
1. Unquoted Variables
# ❌ Breaks on any value with whitespace, globs, or empty string
file=$1
cp $file /tmp/
# ✅ Always quote
cp "$file" /tmp/
Single most common bug, single biggest source of weird behaviour. ShellCheck SC2086. Quote everything unless you specifically need word-splitting or glob expansion.
# Special exceptions where unquoted is right
[[ -n $var ]] # [[ ]] doesn't word-split — safe
(( count++ )) # arithmetic context — safe
local -a parts; parts=($string) # intentional word-splitting
2. Parsing ls
# ❌
for f in $(ls); do
rm "$f"
done
# ✅
for f in *; do
[[ -e "$f" ]] || continue
rm "$f"
done
ls output is for humans. Use globs or find -print0 for code. ShellCheck SC2010.
3. The while read Subshell
# ❌ count is lost — while is in a subshell
count=0
some_cmd | while read -r line; do
(( count++ ))
done
echo "$count"
# 0
# ✅ Process substitution avoids the subshell
count=0
while read -r line; do
(( count++ ))
done < <(some_cmd)
echo "$count"
The right side of a pipe runs in a subshell. Variable assignments don’t escape. Use process substitution or lastpipe.
4. cd Without Failure Check
# ❌ If /opt/myapp doesn't exist, you'll rm -rf the wrong dir
cd /opt/myapp
rm -rf *
# ✅ Verify before catastrophe
cd /opt/myapp || exit 1
rm -rf *
# ✅ Better: shellcheck-clean
cd /opt/myapp || { echo "no such dir" >&2; exit 1; }
rm -rf -- *
ShellCheck SC2164 catches this. Always pair cd with || exit or || die ….
5. [ vs [[ — Word Splitting
# ❌ With empty $var, becomes [ -z ]
if [ -z $var ]; then …
# ✅ Quote inside single brackets
if [ -z "$var" ]; then …
# ✅ Or use double brackets — no quoting needed inside
if [[ -z $var ]]; then …
Use [[ ]] for new scripts — it doesn’t word-split and supports =~, &&, ||, glob ==. Reserve [ ] for POSIX portability.
6. Integer Comparison vs String
# ❌ String comparison — "10" < "2" lexically
if [[ "$a" < "$b" ]]; then …
# ✅ Numeric — use (( )) or -lt
if (( a < b )); then …
if [[ "$a" -lt "$b" ]]; then …
| Goal | Use |
|---|---|
| Compare integers | (( a < b )) or [[ a -lt b ]] |
| Compare strings | [[ "$a" < "$b" ]] (lexical) |
| Equality (int) | (( a == b )) |
| Equality (string) | [[ "$a" == "$b" ]] |
7. Array Confusion
arr=(a b c)
# ❌ Old: assigning a single-element scalar instead of array
copy=$arr # copy = "a" only!
# ✅ Full copy
copy=( "${arr[@]}" )
# ❌ Loop variant
for x in $arr; do … # only iterates over arr[0]
for x in ${arr[@]}; do … # iterates but word-splits each element
for x in "${arr[@]}"; do … # ✅ correct
The mantra: "${arr[@]}" — quoted, with @.
8. Locale Surprises
# Sorting in en_US.UTF-8 vs C
$ LC_ALL=en_US.UTF-8 sort <<EOF
a
B
c
EOF
# a
# B
# c
$ LC_ALL=C sort <<EOF
a
B
c
EOF
# B
# a
# c
[[:upper:]] and friends are locale-dependent. For deterministic scripts:
export LC_ALL=C
at the top.
9. $(command) Trailing Newline Strip
# Capture loses trailing newlines
content=$(cat file_ending_in_newline.txt)
echo "$content" > copy.txt
# copy.txt has NO trailing newline now
Bash strips trailing newlines from $(…). If you need them, append a sentinel:
content=$(cat file.txt; printf x)
content=${content%x}
Or use mapfile.
10. set -e Inside Functions Called In Conditions
set -e
doit() {
false # would normally abort
echo "still here?" # but we get here anyway
}
if doit; then … # set -e disabled inside doit because it's in a condition
Two fixes:
# A. Check explicitly inside the function
doit() {
false || return $?
echo "won't run"
}
# B. Use 'trap ERR' which fires regardless of context
set -eE
trap 'echo "ERR at line $LINENO"' ERR
Bonus — Five More to Watch For
11. read Without -r
echo 'C:\Users' | read -r path # ✅ literal backslashes preserved
echo 'C:\Users' | read path # ❌ backslashes eaten
Always read -r.
12. Globs in == Pattern Matching
# Glob — works as expected
[[ "$f" == *.log ]] && echo "log"
# Quoted pattern — literal match, no glob!
[[ "$f" == "*.log" ]] && echo "log" # only matches the literal string *.log
Don’t quote the right-hand pattern unless you want a literal match.
13. echo With -e or Special Chars
echo "$value" # behaviour depends on $value content and shell variant
printf '%s\n' "$value" # ✅ predictable
Use printf whenever the content is arbitrary.
14. Trap That Captures $tmpdir at Trap-Set Time
# ❌ $tmpdir expanded NOW (might be empty here)
trap "rm -rf $tmpdir" EXIT
# ✅ Single quotes — expansion happens when trap fires
trap 'rm -rf "$tmpdir"' EXIT
15. mktemp Without -d or -p
# ✅ Portable, secure temp file
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
# ✅ Temp directory
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
# ❌ Never roll your own
tmp=/tmp/myfile.$$ # predictable, race-prone
How to Avoid All of These
Three habits:
- Run ShellCheck on every script. It catches 1–9 directly.
- Use strict mode —
set -euo pipefailplusIFS=$'\n\t'. - Test with weird inputs — filenames with spaces, empty strings, unicode, missing files. The bugs hide in the corners.
Next → Practice Exercises