Fundamentals 32 min read

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.

Open Source Linux
Open Source Linux
Open Source Linux
Avoid 40 Common Bash Pitfalls: Essential Tips for Safer Shell Scripts

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

done

Correct approach: use globbing directly.

for i in *.mp3; do
  echo "$i"
done

Check for existence when no files match:

# POSIX
for i in *.mp3; do
  [ -e "$i" ] || continue
  some command "$i"
done

2. 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
  ...
done

4. [ $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++)); done

9. if [grep foo myfile]

[

is a command; the correct form is:

if grep -q foo myfile; then
  ...
fi

10. 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
  ...
fi

12. read $foo

Read into a variable without the leading $:

read foo
IFS= read -r foo   # safer

13. 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' file

14. 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 valid

16. foo = bar

Spaces turn the command into three words; the first word is treated as a command.

foo=bar   # correct

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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BashShell scriptingbash pitfalls
Open Source Linux
Written by

Open Source Linux

Focused on sharing Linux/Unix content, covering fundamentals, system development, network programming, automation/operations, cloud computing, and related professional knowledge.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.