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:
Do:
Why:
The for
loop uses a space as a delimiter whereas the while
loop uses a newline.
Don’t:
Do:
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:
Do:
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:
Do:
Why:
Bash has arrays, use them! Also, they will not break if individual items contain spaces or newlines.
Don’t:
Do:
Why:
readarray
will not break on spaces.
Don’t:
Do:
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:
Do:
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:
Do:
Why:
Though either is perfectly acceptable, we can use a HERE string and avoid calling an unnecessary command or builtin.
Don’t:
Do:
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:
Do:
or
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:
Do:
Why:
Don’t use $
on variables in $((...))
or ((...))
.
For example, (( $var == 1 ))
will break when $var
is null or unset.
Don’t:
Do:
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:
Do:
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.