«

Bash scripting: Do's and Don'ts

What follows is a style guide of sorts, outlining a few “gotcha’s” and anti-patterns often encountered in Bash shell scripts. Following these guidelines will make your Bash scripts look better and perform more consistently.

Note: If you’re worried about supporting POSIX, ancient versions of Bash, or other shells, you can promptly ignore everything on this page.

Don’t:

for i in $(somecommand); do
    ...
done

Do:

while IFS= read -r i; do
    ...
done < <(somecommand)

Why:

The for loop uses a space as a delimiter whereas the while loop uses a newline.

Don’t:

somecommand | while IFS= read -r i; do
    ...
done

Do:

while IFS= read -r i; do
    ...
done < <(somecommand)

Why:

The pipe causes the while loop to execute in a subshell. Any variable assignments within a subshell will be lost. While this can occasionally be desirable, it usually isn’t.

Don’t:

while IFS= read -r path; do
    ...
done < <(find)

Do:

while read -rd $'\0' path; do
    ...
done < <(find -print0)

Why:

Using a null byte as a delimiter is generally preferred wherever possible.

Also note that a while loop isn’t necessary in order to iterate over the output of find. For simple operations, you can simply use find -exec somecommand {} \; (command is run once for each matched file) or find -exec somecommand {} + (the command line is built by appending each selected file name at the end). Alternatively, you can pipe the output to xargs -0.

Don’t:

items='one two three'

for i in $items; do
    ...
done

Do:

items=(one two three)

for i in "${items[@]}"; do
    ...
done

Why:

Bash has arrays, use them! Also, they will not break if individual items contain spaces or newlines.

Don’t:

 lines=($(somecommand)) 

Do:

 readarray -t lines < <(somecommand) 

Why:

readarray will not break on spaces.

Don’t:

 newvar="$oldvar" 

Do:

 newvar=$oldvar 

Why:

There’s no need to quote direct variable assignments. The same is true with newvar=${oldvar// /}.

Feel free to ignore this advice if you have a difficult time remembering quoting rules. The double quotes, while unnecessary, don’t pose a problem here.

Don’t:

 content=$(cat FILE) 

Do:

 content=$(< FILE) 

Why:

Useless use of cat.

cat’s description is as follows:
Concatenate FILE(s), or standard input, to standard output.

If you’re using cat for some other purpose then you probably shouldn’t be using it.

Don’t:

 echo "$something" | somecommand 

Do:

 somecommand <<<"$something" 

Why:

Though either is perfectly acceptable, we can use a HERE string and avoid calling an unnecessary command or builtin.

Don’t:

 echo "$@" 

Do:

 echo "$*" 

Why:

"$*" concatenates the positional parameters into a single word, with the first character of the IFS as separator. That is, "$*" is equivalent to "$1c$2c...", where c is the first character of the value of the IFS variable.

"$@" expands each positional parameter to a separate word.

For example, with a default IFS and positional parameters of $1=one, $2=two, $3=three, "$*" expands to 'one two three' whereas "$@" expands to 'one' 'two' 'three'.

While echo "$@" and echo "$*" happen to have the same effect, with other commands this won’t always be the case. There are times when you’ll need to be aware of this.

Don’t:

 [[ $# > 0 ]] 

Do:

 [[ $# -gt 0 ]] 

or

 (( $# > 0 )) 

Why:

The > operator in [[ $# > 0 ]] performs a string comparison, not a numerical one! [[ string1 > string2 ]] is true if string1 sorts after string2 lexicographically. Instead, use one of -eq, -ne, -lt, -le, -gt, or -ge. These arithmetic binary operators return true if arg1 is equal to, not equal to, less than, less than or equal to, greater than, or greater than or equal to arg2, respectively. arg1 and arg2 may be positive or negative integers.

Alternatively, use ((...)) for arithmetic evaluation.

Don’t:

 (( $var == 1 )) 

Do:

 (( var == 1 )) 

Why:

Don’t use $ on variables in $((...)) or ((...)).

For example, (( $var == 1 )) will break when $var is null or unset.

Don’t:

somefunction(){
    if local variable=$(somecommand); then
        ...
    fi
}

Do:

somefunction(){
    local variable

    if variable=$(somecommand); then
        ...
    fi
}

Why:

The return status of local [option] [name[=value] ...] is 0 unless local is used outside a function, an invalid name is supplied, or name is a readonly variable.

Don’t:

options=

if something; then
    options=--flag
fi

somecommand "$options"

Do:

options=()

if something; then
    options=(--flag)
fi

somecommand "${options[@]}"

Why:

If $options is empty, it will pass somecommand an empty argument, which will almost always be undesired. Alternatively, if the array ${option[@]} is empty, it will pass no arguments to somecommand, not even an empty one.

For even more egregious examples of bad code, check out shellcheck’s Gallery of Bad Code. While you’re at it, consider linting your shell scripts with shellcheck.