Avoid 40 Common Bash Pitfalls: Essential Tips for Safer Shell Scripts
This article compiles over forty everyday Bash programming mistakes that both beginners and seasoned users often overlook, explains why each pattern is problematic, and provides safe, portable alternatives with clear examples to improve script reliability and maintainability.
This article, originally published on kodango.com, lists more than forty common Bash pitfalls, explains why each pattern is erroneous, and shows safer alternatives.
1. for i in $(ls *.mp3)
Using command substitution without quoting causes word splitting and filename expansion, and parsing ls output is fragile.
for i in $(ls *.mp3); do
# WRONG!
some command $i # WRONG!
done
for i in $(ls); do # WRONG!
...
done
for i in `ls`; do # WRONG!
...
done
files=($(find . -type f)) # WRONG!
for i in ${files[@]}; do # WRONG!
...
doneCorrect approach: use globbing directly.
for i in *.mp3; do
echo "$i"
doneCheck for existence when no files match:
# POSIX
for i in *.mp3; do
[ -e "$i" ] || continue
some command "$i"
done2. cp $file $target
Always quote variables to handle spaces or special characters. Use -- when filenames may start with -.
cp -- "$file" "$target"3. Filenames starting with '-'
Two solutions: use -- or prefix paths with ./.
cp -- "$file" "$target"
# or
for i in ./*.mp3; do
cp "$i" /target
...
done4. [ $foo = "bar" ]
Quote the variable and place it inside the brackets.
[ "$foo" = bar ]5. cd $(dirname "$f")
Quote the command substitution.
cd "$(dirname "$f")"6. [ "$foo" = bar && "$bar" = foo ]
Separate tests with && or use [[ syntax.
[ "$foo" = bar ] && [ "$bar" = foo ]
# or
[[ $foo == bar && $bar == foo ]]7. [[ $foo > 7 ]]
Use arithmetic evaluation (( )) for numeric comparison.
(( foo > 7 ))8. grep foo bar | while read -r; do ((count++)); done
The loop runs in a subshell, so count is not updated. Enable lastpipe in Bash ≥4.2 or restructure.
shopt -s lastpipe
grep foo bar | while read -r; do ((count++)); done9. if [grep foo myfile]
[is a command; the correct form is:
if grep -q foo myfile; then
...
fi10. if [bar="$foo"]; then ...
Spaces are required between arguments.
[ "$bar" = "$foo" ] && ...11. if [ [ a = b ] && [ c = d ] ]; then ...
Separate the two tests:
if [ a = b ] && [ c = d ]; then
...
fi
# or
if [[ a = b && c = d ]]; then
...
fi12. read $foo
Read into a variable without the leading $:
read foo
IFS= read -r foo # safer13. cat file | sed s/foo/bar/ > file
Never read and write the same file simultaneously; use a temporary file or -i with GNU sed:
sed 's/foo/bar/g' file > tmp && mv tmp file
# or
sed -i 's/foo/bar/g' file14. echo $foo
Unquoted variables undergo word splitting and pathname expansion. Use printf for reliable output.
printf "%s
" "$foo"15. $foo=bar
Assignments must not contain spaces.
foo=bar
foo="bar" # also valid16. foo = bar
Spaces turn the command into three words; the first word is treated as a command.
foo=bar # correct17. echo < echo cannot read a here‑document. Use cat or a quoted string. <code>cat <<EOF Hello world How's it going? EOF # or echo "Hello world\nHow's it going?"</code> 18. su -c 'some command' On some systems -c specifies a login class. Specify the user explicitly: <code>su root -c 'some command'</code> 19. cd /foo; bar Check the result of cd before running subsequent commands. <code>cd /foo && bar # or cd /foo || { echo "Can't cd"; exit 1; } bar</code> 20. [ bar == "$foo" ] Correct form uses a single = or quotes the right‑hand side. <code>[ bar = "$foo" ] && echo yes [[ bar == $foo ]] && echo yes</code> 21. for i in {1..10}; do ./something &; done Do not mix & and ; . Use either: <code>for i in {1..10}; do ./something & done # or for i in {1..10}; do ./something & done</code> 22. cmd1 && cmd2 || cmd3 Complex boolean logic can be confusing; prefer explicit if statements. <code>if cmd1; then cmd2 else cmd3 fi</code> 23. echo "Hello World!" In interactive Bash, ! triggers history expansion. Disable it with set +H or use single quotes. <code>set +H echo "Hello World!" # or echo 'Hello World!'</code> 24. for arg in $* $* expands to a single word; use "$@" or simply for arg in "$@"; do ...; done . <code>for arg in "$@"; do echo "parameter: '$arg'" done</code> 25. function foo() Portable function definition: <code>foo() { ... }</code> 26. echo "~" Tilde expansion occurs only when ~ is unquoted. <code>echo "~" # prints ~ echo $HOME # prints home directory</code> 27. local varname=$(command) Separate the local declaration from the command substitution to preserve the command’s exit status. <code>local varname varname=$(command) rc=$?</code> 28. export foo=~/bar For portability, assign first then export, or expand ~ via $HOME : <code>foo=~/bar; export foo export foo="$HOME/bar"</code> 29. sed 's/$foo/good bye/' Single quotes prevent variable expansion; use double quotes and escape as needed. <code>foo="hello" sed "s/$foo/good bye/" file</code> 30. tr [A-Z] [a-z] Bracket expressions are subject to pathname expansion. Use character classes or set LC_COLLATE=C for predictable ASCII conversion. <code>LC_COLLATE=C tr A-Z a-z tr '[:upper:]' '[:lower:]'</code> 31. ps ax | grep gedit Filter out the grep process or use a character class trick. <code>ps ax | grep -v grep | grep gedit # or ps ax | grep [g]edit</code> 32. printf "$foo" When $foo contains % or backslashes, specify a format string. <code>printf "%s\n" "$foo" printf "%s\n" "$foo"</code> 33. for i in {1..$n} Brace expansion does not evaluate variables. Use a C‑style loop. <code>for ((i=1; i<=n; i++)); do ... done</code> 34. if [[ $foo = $bar ]] Quote the right‑hand side to avoid pattern matching. <code>if [[ $foo = "$bar" ]]; then ... fi</code> 35. if [[ $foo =~ 'some RE' ]] Do not quote the regular expression; store it in a variable if it contains many backslashes. <code>re='some RE' if [[ $foo =~ $re ]]; then ... fi</code> 36. [ -n $foo ] or [ -z $foo ] Always quote the variable. <code>[ -n "$foo" ] [ -z "$foo" ] [[ -n $foo ]] [[ -z $foo ]]</code> 37. [[ -e "$broken_symlink" ]] returns 1 Use -e or -L to test for a broken symlink. <code>[[ -e "$broken_symlink" || -L "$broken_symlink" ]]</code> 38. ed file <<<"g/d{0,3}/s//e/g" fails Brace quantifier {0,3} is not supported by ed . Use {1,3} or a modern editor. 39. expr sub-string fails for "match" Prefix the command with + to suppress the special meaning of match , or use Bash parameter expansion. <code>expr + "$word" : ".\(.*\)" # Bash alternatives echo "${word#?}" echo "${word:1}" </code> 40. UTF‑8 and Byte‑Order Marks (BOM) UTF‑8 files normally do not need a BOM; a leading BOM can cause unexpected characters in scripts. 41. content=$( Command substitution strips trailing newlines. 42. somecmd 2>&1 >>logfile Redirections are parsed left‑to‑right; the correct order to capture both stdout and stderr is: <code>somecmd >>logfile 2>&1</code> 43. cmd; (( ! $? )) || die Prefer an explicit if test for the command’s exit status. <code>if ! cmd; then die fi</code>
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Open Source Linux
Focused on sharing Linux/Unix content, covering fundamentals, system development, network programming, automation/operations, cloud computing, and related professional knowledge.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
