Use triple backticks rather than indenting for code.

Vidar Holen
2015-10-03 21:33:27 -07:00
parent c91b073c53
commit e073e65e5e
153 changed files with 1741 additions and 1049 deletions

@@ -2,11 +2,15 @@
### Problematic code:
echo "$"
```sh
echo "$"
```
### Correct code:
echo "\$"
```sh
echo "\$"
```
### Rationale:
`$` is special in double quotes, but there are some cases where it's interpreted literally:

@@ -2,19 +2,27 @@
### Problematic code:
echo Yay \o/
```sh
echo Yay \o/
```
or
\git status
```sh
\git status
```
### Correct code:
echo 'Yay \o/'
```sh
echo 'Yay \o/'
```
or
command git status
```sh
command git status
```
### Rationale:

@@ -2,19 +2,27 @@
### Problematic code:
# I want programs to show text in dutch!
LANGUAGE= nl
```sh
# I want programs to show text in dutch!
LANGUAGE= nl
```
# I want to run the nl command with English error messages!
LANGUAGE= nl
```sh
# I want to run the nl command with English error messages!
LANGUAGE= nl
```
### Correct code:
# I want programs to show text in dutch!
LANGUAGE=nl
```sh
# I want programs to show text in dutch!
LANGUAGE=nl
```
# I want to run the nl command with English error messages!
LANGUAGE='' nl
```sh
# I want to run the nl command with English error messages!
LANGUAGE='' nl
```
### Rationale:

@@ -2,11 +2,11 @@
### Problematic code:
for f in *; do echo "$f" done
for f in *; do echo "$f" done
### Correct code:
for f in *; do echo "$f"; done
for f in *; do echo "$f"; done
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
echo “hello world”
```sh
echo “hello world”
```
### Correct code:
echo "hello world"
```sh
echo "hello world"
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
echo hello world
```sh
echo hello world
```
### Correct code:
echo 'hello world'
```sh
echo 'hello world'
```
### Rationale:

@@ -2,13 +2,17 @@
### Problematic code:
echo "Ninth parameter: $9"
echo "Tenth parameter: $10"
```sh
echo "Ninth parameter: $9"
echo "Tenth parameter: $10"
```
### Correct code:
echo "Ninth parameter: $9"
echo "Tenth parameter: ${10}"
```sh
echo "Ninth parameter: $9"
echo "Tenth parameter: ${10}"
```
### Rationale:

@@ -2,17 +2,21 @@
### Problematic code:
while IFS= read -r line
do
printf "%q\n" "$line"
done <<(curl -s http://example.com)
```sh
while IFS= read -r line
do
printf "%q\n" "$line"
done <<(curl -s http://example.com)
```
### Correct code:
while IFS= read -r line
do
printf "%q\n" "$line"
done < <(curl -s http://example.com)
```sh
while IFS= read -r line
do
printf "%q\n" "$line"
done < <(curl -s http://example.com)
```
### Rationale:

@@ -4,23 +4,29 @@
Any code using `<<-` that is indented with spaces. `cat -T script` shows
cat <<- foo
Hello world
foo
```sh
cat <<- foo
Hello world
foo
```
### Correct code:
Code using `<<-` must be indented with tabs. `cat -T script` shows
^Icat <<- foo
^I^IHello world
^Ifoo
```sh
^Icat <<- foo
^I^IHello world
^Ifoo
```
Or simply don't indent the end token:
cat <<- foo
Hello World
foo
```sh
cat <<- foo
Hello World
foo
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
foo &; bar
```sh
foo &; bar
```
### Correct code:
foo & bar
```sh
foo & bar
```
### Rationale:

@@ -2,17 +2,21 @@
### Problematic code:
foo(input) {
echo "$input"
}
foo("hello world");
```sh
foo(input) {
echo "$input"
}
foo("hello world");
```
### Correct code:
foo() {
echo "$1"
}
foo "hello world"
```sh
foo() {
echo "$1"
}
foo "hello world"
```
### Rationale:

@@ -2,17 +2,23 @@
### Problematic code:
$greeting="Hello World"
```sh
$greeting="Hello World"
```
### Correct code:
greeting="Hello World"
```sh
greeting="Hello World"
```
Alternatively, if the goal was to assign to a variable whose name is in another variable (indirection), use `declare`:
name=foo
declare "$name=hello world"
echo "$foo"
```sh
name=foo
declare "$name=hello world"
echo "$foo"
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
foo = 42
```sh
foo = 42
```
### Correct code:
foo=42
```sh
foo=42
```
### Rationale:

@@ -3,12 +3,16 @@
### Problematic code:
echo "Your username is ´whoami´"
```sh
echo "Your username is ´whoami´"
```
### Correct code:
echo "Your username is $(whoami)" # Preferred
echo "Your username is `whoami`" # Deprecated, will give [SC2006]
```sh
echo "Your username is $(whoami)" # Preferred
echo "Your username is `whoami`" # Deprecated, will give [SC2006]
```
### Rationale:
@@ -20,10 +24,14 @@ Backticks start command expansions, while forward ticks are literal. To help spo
If you want to write out literal forward ticks, such as fancyful ascii quotation marks:
echo "``Proprietary software is an injustice.´´ - Richard Stallman"
```sh
echo "``Proprietary software is an injustice.´´ - Richard Stallman"
```
use single quotes instead:
echo '``Proprietary software is an injustice.´´ - Richard Stallman'
```sh
echo '``Proprietary software is an injustice.´´ - Richard Stallman'
```
To nest forward ticks in command expansion, use `$(..)` instead of `` `..` ``.

@@ -2,13 +2,17 @@
### Problematic code:
greeting="hello
target="world"
```sh
greeting="hello
target="world"
```
### Correct code:
greeting="hello"
target="world"
```sh
greeting="hello"
target="world"
```
### Rationale:
@@ -20,12 +24,16 @@ ShellCheck warns when it detects multi-line double quoted, single quoted or back
If you do want a multiline variable, just make sure the character after it is a quote, space or line feed.
var='multiline
'value
```sh
var='multiline
'value
```
can be rewritten for readability and to remove the warning:
var='multiline
value'
```sh
var='multiline
value'
```
As always `` `..` `` should be rewritten to ``$(..)``.

@@ -2,17 +2,21 @@
### Problematic code:
If true
Then
echo "hello"
Fi
```sh
If true
Then
echo "hello"
Fi
```
### Correct code:
if true
then
echo "hello"
fi
```sh
if true
then
echo "hello"
fi
```
### Rationale:

@@ -2,19 +2,26 @@
### Problematic code:
rmf() { rm -f "$@" }
```sh
rmf() { rm -f "$@" }
```
or
eval echo \${foo}
```sh
eval echo \${foo}
```
### Correct code:
rmf() { rm -f "$@"; }
```sh
rmf() { rm -f "$@"; }
```
and
eval "echo \${foo}"
```sh
eval "echo \${foo}"
### Rationale:
Curly brackets are normally used as syntax in parameter expansion, command grouping and brace expansion.
@@ -30,3 +37,4 @@ ShellCheck does not warn about `{}`, since this is frequently used with `find` a
### Exceptions
This error is harmless when the curly brackets are supposed to be literal, in e.g. `awk {'print $1'}`. However, it's cleaner and less error prone to simply include them inside the quotes: `awk '{print $1}'`.
```

@@ -2,13 +2,17 @@
### Problematic code:
!#/bin/sh
echo "Hello World"
```sh
!#/bin/sh
echo "Hello World"
```
### Correct code:
#!/bin/sh
echo "Hello World"
```sh
#!/bin/sh
echo "Hello World"
```
### Rationale:

@@ -2,17 +2,21 @@
### Problematic code:
for $var in *
do
echo "$var"
done
```sh
for $var in *
do
echo "$var"
done
```
### Correct code:
for var in *
do
echo "$var"
done
```sh
for var in *
do
echo "$var"
done
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
echo "$array[@]"
```sh
echo "$array[@]"
```
### Correct code:
echo "${array[@]}"
```sh
echo "${array[@]}"
```
### Rationale:

@@ -2,19 +2,27 @@
### Problematic code:
grep ^(.*)\1$ file
```sh
grep ^(.*)\1$ file
```
or
var = myfunction(value)
```sh
var = myfunction(value)
```
### Correct code:
grep '^(.*)\1$' file
```sh
grep '^(.*)\1$' file
```
or
var=$(myfunction value)
```sh
var=$(myfunction value)
```
### Rationale:

@@ -2,18 +2,22 @@
### Problematic code:
if true
then
echo hello
fi
fi
```sh
if true
then
echo hello
fi
fi
```
### Correct code:
if true
then
echo hello
fi
```sh
if true
then
echo hello
fi
```
### Rationale:
@@ -21,11 +25,13 @@ This error is typically seen when there are too many `fi`, `done` or `esac`s, or
In some cases, it can even be caused by bad quoting:
var="foo
if [[ $var = "bar ]
then
echo true
fi
```sh
var="foo
if [[ $var = "bar ]
then
echo true
fi
```
In this case, the `if` ends up inside the double quotes, leaving the `then` dangling.

@@ -2,12 +2,16 @@
### Problematic code:
. "$(find_install_dir)/lib.sh"
```sh
. "$(find_install_dir)/lib.sh"
```
### Correct code:
# shellcheck source=src/lib.sh
. "$(find_install_dir)/lib.sh"
```sh
# shellcheck source=src/lib.sh
. "$(find_install_dir)/lib.sh"
```
### Rationale:

@@ -4,12 +4,16 @@ Reasons include: file not found, no permissions, not included on the command lin
### Problematic code:
source somefile
```sh
source somefile
```
### Correct code:
# shellcheck disable=SC1091
source somefile
```sh
# shellcheck disable=SC1091
source somefile
```
### Rationale:

@@ -2,12 +2,16 @@
### Problematic code:
source mylib
```sh
source mylib
```
### Correct code:
# shellcheck disable=SC1094
source mylib
```sh
# shellcheck disable=SC1094
source mylib
```
(or fix `mylib`)

@@ -2,17 +2,23 @@
### Problematic code:
var==value
```sh
var==value
```
### Correct code:
Assignment:
var=value
```sh
var=value
```
Comparison:
[ "$var" = value ]
```sh
[ "$var" = value ]
```
### Rationale:
@@ -26,4 +32,6 @@ ShellCheck has noticed that you're using `==` in an unexpected way. The two most
If you wanted to assign a literal equals sign, use quotes to make this clear:
var="=sum(A1:A10)"
```sh
var="=sum(A1:A10)"
```

@@ -2,11 +2,15 @@
### Problematic code:
eval $var=(a b)
```sh
eval $var=(a b)
```
### Correct code:
eval "$var=(a b)"
```sh
eval "$var=(a b)"
```
### Rationale:

@@ -2,17 +2,21 @@
### Problematic code:
while sleep 1
do# show time
date
done
```sh
while sleep 1
do# show time
date
done
```
### Correct code:
while sleep 1
do # show time
date
done
```sh
while sleep 1
do # show time
date
done
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
string="stirng" ; echo "$string" | sed -e "s/ir/ri/"
```sh
string="stirng" ; echo "$string" | sed -e "s/ir/ri/"
```
### Correct code:
string="stirng" ; echo "${string//ir/ri}"
```sh
string="stirng" ; echo "${string//ir/ri}"
```
### Rationale:
@@ -16,6 +20,8 @@ Let's assume somewhere earlier in your code you have put data into a variable (E
Occasionally a more complex sed substitution is required. For example, getting the last character of a string.
string="stirng" ; echo "$string" | sed -e "s/^.*\(.\)$/\1/"
```sh
string="stirng" ; echo "$string" | sed -e "s/^.*\(.\)$/\1/"
```
This is a bit simple for the example and there are alternative ways of doing this in the shell, but this SC2001 flags on several of my crazy complex sed commands which are beyond the scope of this example. Utilizing some of the more complex capabilities of sed is required occasionally and it is safe to ignore SC2001.

@@ -2,13 +2,17 @@
### Problematic code:
i=$(expr 1 + 2)
l=$(expr length "$var")
```sh
i=$(expr 1 + 2)
l=$(expr length "$var")
```
### Correct code:
i=$((1+2))
l=${#var}
```sh
i=$((1+2))
l=${#var}
```
### Rationale:

@@ -2,20 +2,26 @@
### Problematic code:
echo $(($n+1))
```sh
echo $(($n+1))
```
### Correct code:
echo $((n+1))
```sh
echo $((n+1))
```
### Rationale:
The `$` on regular variables in arithmetic contexts is unnecessary, and can even lead to subtle bugs. This is because the contents of `$((..))` is first expanded into a string, and then evaluated as an expression:
$ a='1+1'
$ echo $(($a * 5)) # becomes 1+1*5
6
$ echo $((a * 5)) # evaluates as (1+1)*5
10
```sh
$ a='1+1'
$ echo $(($a * 5)) # becomes 1+1*5
6
$ echo $((a * 5)) # evaluates as (1+1)*5
10
```
The `$` is unavoidable for special variables like `$1` vs `1`, `$#` vs `#`. ShellCheck does not warn about these cases.

@@ -2,11 +2,15 @@
### Problematic code
echo "Current time: `date`"
```sh
echo "Current time: `date`"
```
### Correct code
echo "Current time: $(date)"
```sh
echo "Current time: $(date)"
```
### Rationale

@@ -2,11 +2,15 @@
### Problematic code:
find . | echo
```sh
find . | echo
```
### Correct code:
find .
```sh
find .
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic Code:
ps ax | grep -v grep | grep "$service" > /dev/null
```sh
ps ax | grep -v grep | grep "$service" > /dev/null
```
### Correct Code:
pgrep -f "$service" > /dev/null
```sh
pgrep -f "$service" > /dev/null
```
### Rationale:
@@ -16,11 +20,15 @@ If you are just after a pid from a running program, then pgrep is a much safer a
What if you have the pid and you are looking for the matching program name?
pid=123; ps ax | grep "$pid"
```sh
pid=123; ps ax | grep "$pid"
```
What if you want a range of the ps field, like from the 16th space to the end of the line?
ps ax | grep "$pid" | cut -d" " -f16-
```sh
ps ax | grep "$pid" | cut -d" " -f16-
```
Both are valid cases where SC2009 is not valid.

@@ -2,11 +2,15 @@
### Problematic code:
ls -l | grep " $USER " | grep '\.txt$'
```sh
ls -l | grep " $USER " | grep '\.txt$'
```
### Correct code:
find . -maxdepth 1 -name '*.txt' -user "$USER"
```sh
find . -maxdepth 1 -name '*.txt' -user "$USER"
```
### Rationale:
@@ -14,11 +18,13 @@
Here's an example:
$ ls -l
total 0
-rw-r----- 1 me me 0 Feb 5 20:11 foo?bar
-rw-r----- 1 me me 0 Feb 5 2011 foo?bar
-rw-r----- 1 me me 0 Feb 5 20:11 foo?bar
```sh
$ ls -l
total 0
-rw-r----- 1 me me 0 Feb 5 20:11 foo?bar
-rw-r----- 1 me me 0 Feb 5 2011 foo?bar
-rw-r----- 1 me me 0 Feb 5 20:11 foo?bar
```
It shows three seemingly identical filenames, and did you spot the time format change? How it formats and what it redacts can differ between locale settings, `ls` version, and whether output is a tty.

@@ -2,24 +2,30 @@
### Problematic code:
for line in $(cat file | grep -v '^ *#')
do
echo "Line: $line"
done
```sh
for line in $(cat file | grep -v '^ *#')
do
echo "Line: $line"
done
```
### Correct code:
grep -v '^ *#' < file | while IFS= read -r line
do
echo "Line: $line"
done
```sh
grep -v '^ *#' < file | while IFS= read -r line
do
echo "Line: $line"
done
```
or without a subshell (bash, zsh, ksh):
while IFS= read -r line
do
echo "Line: $line"
done < <(grep -v '^ *#' < file)
```sh
while IFS= read -r line
do
echo "Line: $line"
done < <(grep -v '^ *#' < file)
```
### Rationale:
@@ -27,20 +33,26 @@ For loops by default (subject to `$IFS`) read word by word. Additionally, glob e
Given this text file:
foo *
bar
```sh
foo *
bar
```
The for loop will print:
Line: foo
Line: aardwark.jpg
Line: bullfrog.jpg
...
```sh
Line: foo
Line: aardwark.jpg
Line: bullfrog.jpg
...
```
The while loop will print:
Line: foo *
Line: bar
```sh
Line: foo *
Line: bar
```
### Exceptions

@@ -2,11 +2,15 @@
### Problematic code:
find . -name '*.tar' -exec tar xf {} -C "$(dirname {})" \;
```sh
find . -name '*.tar' -exec tar xf {} -C "$(dirname {})" \;
```
### Correct code:
find . -name '*.tar' -exec sh -c 'tar xf "$1" -C "$(dirname "$1")"' _ {} \;
```sh
find . -name '*.tar' -exec sh -c 'tar xf "$1" -C "$(dirname "$1")"' _ {} \;
```
### Rationale:
@@ -14,8 +18,10 @@ Bash evaluates any command substitutions before the command they feature in is e
To run shell code for each file, we can write a tiny script and inline it with `sh -c`. We add `_` as a dummy argument that becomes `$0`, and a filename argument that becomes `$1` in the inlined script:
$ sh -c 'echo "$1 is in $(dirname "$1")"' _ "mydir/myfile"
mydir/myfile is in mydir
```sh
$ sh -c 'echo "$1 is in $(dirname "$1")"' _ "mydir/myfile"
mydir/myfile is in mydir
```
This command can be executed by `find -exec`, with `{}` as the filename argument. It executes shell which interprets the inlined script once for each file. Note that the inlined script is single quoted, again to ensure that the expansion does not happen prematurely .

@@ -2,16 +2,20 @@
### Problematic code:
[[ $dryrun ]] && echo "Would delete file" || rm file
```sh
[[ $dryrun ]] && echo "Would delete file" || rm file
```
### Correct code:
if [[ $dryrun ]]
then
echo "Would delete file"
else
rm file
fi
```sh
if [[ $dryrun ]]
then
echo "Would delete file"
else
rm file
fi
```
### Rationale:

@@ -2,13 +2,17 @@
### Problematic code:
name=World
echo 'Hello $name'
```sh
name=World
echo 'Hello $name'
```
### Correct code:
name=World
echo "Hello $name"
```sh
name=World
echo "Hello $name"
```
### Rationale:
@@ -18,7 +22,9 @@ If you want to use the values of variables and such, use double quotes instead.
Note that if you have other items that needs single quoting, you can use both in a single word:
echo '$1 USD is '"$rate GBP"
```sh
echo '$1 USD is '"$rate GBP"
```
### Exceptions

@@ -2,11 +2,15 @@
### Problematic code:
percent=$((count/total*100))
```sh
percent=$((count/total*100))
```
### Correct code:
percent=$((count*100/total))
```sh
percent=$((count*100/total))
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
echo 'hello world' | tr 'hello' 'goodbye'
```sh
echo 'hello world' | tr 'hello' 'goodbye'
```
### Correct code:
echo 'hello world' | sed -e 's/hello/goodbye/g'
```sh
echo 'hello world' | sed -e 's/hello/goodbye/g'
```
### Rationale:

@@ -1,13 +1,17 @@
# Note that unlike globs, o* here matches 'ooo' but not 'oscar'
### Problematic code:
grep 'foo*'
```sh
grep 'foo*'
```
when wanting to match `food` and `foosball`, but not `mofo` or `keyfob`.
### Correct code:
grep '^foo'
```sh
grep '^foo'
```
### Rationale:

@@ -2,11 +2,11 @@
### Problematic code:
sudo echo 'export FOO=bar' >> /etc/profile
sudo echo 'export FOO=bar' >> /etc/profile
### Correct code:
echo 'export FOO=bar' | sudo tee -a /etc/profile > /dev/null
echo 'export FOO=bar' | sudo tee -a /etc/profile > /dev/null
### Rationale:
@@ -18,13 +18,13 @@ There is nothing special about `tee`. It's just the simplest command that can bo
Truncating:
echo 'data' | sudo dd of=file
echo 'data' | sudo sed 'w file'
echo 'data' | sudo dd of=file
echo 'data' | sudo sed 'w file'
Appending:
echo 'data' | sudo awk '{ print $0 >> "file" }'
echo 'data' | sudo sh -c 'cat >> file'
echo 'data' | sudo awk '{ print $0 >> "file" }'
echo 'data' | sudo sh -c 'cat >> file'
### Exceptions

@@ -2,11 +2,15 @@
### Problematic code:
PS1='\e[36m\$ \e(B\e[m'
```sh
PS1='\e[36m\$ \e(B\e[m'
```
### Correct code:
PS1='\[\e[36m\]\$ \[\e(B\e[m\]'
```sh
PS1='\[\e[36m\]\$ \[\e(B\e[m\]'
```
### Rationale:

@@ -2,33 +2,43 @@
### Problematic code:
alias server_uptime='ssh $host 'uptime -p''
```sh
alias server_uptime='ssh $host 'uptime -p''
```
### Correct code:
alias server_uptime='ssh $host '"'uptime -p'"
```sh
alias server_uptime='ssh $host '"'uptime -p'"
```
### Rationale:
In the first case, the user has four single quotes on a line, wishfully hoping that the shell will match them up as outer quotes around a string with literal single quotes:
# v--------match--------v
alias server_uptime='ssh $host 'uptime -p''
# ^--match--^
```sh
# v--------match--------v
alias server_uptime='ssh $host 'uptime -p''
# ^--match--^
```
The shell, meanwhile, always terminates single quoted strings at the first possible single quote:
# v---match--v
alias server_uptime='ssh $host 'uptime -p''
# ^^
```sh
# v---match--v
alias server_uptime='ssh $host 'uptime -p''
# ^^
```
Which is the same thing as `alias server_uptime='ssh $host uptime' -p`.
There is no way to nest single quotes. However, single quotes can be placed literally in double quotes, so we can instead concatenate a single quoted string and a double quoted string:
# v--match---v
alias server_uptime='ssh $host '"'uptime -p'"
# ^---match---^
```sh
# v--match---v
alias server_uptime='ssh $host '"'uptime -p'"
# ^---match---^
```
This results in an alias with embedded single quotes.

@@ -2,11 +2,15 @@
### Problematic code:
echo "You enter "$HOSTNAME". You can smell the wumpus." >> /etc/issue
```sh
echo "You enter "$HOSTNAME". You can smell the wumpus." >> /etc/issue
```
### Correct code:
echo "You enter $HOSTNAME. You can smell the wumpus." >> /etc/issue
```sh
echo "You enter $HOSTNAME. You can smell the wumpus." >> /etc/issue
```
### Rationale:
@@ -14,9 +18,11 @@ Always quoting variables and command expansions is good practice, but blindly pu
In this case, ShellCheck has noticed that the quotes around the expansion are unquoting it, because the left quote is terminating an existing double quoted string, while the right quote starts a new one:
echo "You enter "$HOSTNAME". You can smell the wumpus."
|----------| |---------------------------|
Quoted No quotes Quoted
```sh
echo "You enter "$HOSTNAME". You can smell the wumpus."
|----------| |---------------------------|
Quoted No quotes Quoted
```
If the quotes were supposed to be literal, they should be escaped. If the quotes were supposed to quote an expansion (as in the example), they should be removed because this is already a double quoted string.

@@ -2,11 +2,15 @@
### Problematic code:
echo "Name:\t$value"
```sh
echo "Name:\t$value"
```
### Correct code:
printf "Name:\t%s\n" "$value"
```sh
printf "Name:\t%s\n" "$value"
```
### Rationale:
@@ -18,7 +22,9 @@ Other, non-portable methods include `echo -e '\t'` and `echo $'\t'`. ShellCheck
If you actually wanted a literal backslash-t, use
echo "\\t"
```sh
echo "\\t"
```
### Exceptions

@@ -2,21 +2,29 @@
### Problematic code:
ssh host "echo $HOSTNAME"
```sh
ssh host "echo $HOSTNAME"
```
### Correct code:
ssh host "echo \$HOSTNAME"
```sh
ssh host "echo \$HOSTNAME"
```
or
ssh host 'echo $HOSTNAME'
```sh
ssh host 'echo $HOSTNAME'
```
### Rationale:
Bash expands all arguments that are not escaped/singlequoted. This means that the problematic code is identical to
ssh host "echo clienthostname"
```sh
ssh host "echo clienthostname"
```
and will print out the client's hostname, not the server's hostname.

@@ -29,33 +29,45 @@ Here are some constructs that cause subshells (shellcheck may not warn about all
Pipelines:
subshell1 | subshell2 | subshell3 # Bash, Dash, Ash
subshell1 | subshell2 | regular # Ksh, Zsh
```sh
subshell1 | subshell2 | subshell3 # Bash, Dash, Ash
subshell1 | subshell2 | regular # Ksh, Zsh
```
Command substitution:
regular "$(subshell1)" "`subshell2`"
```sh
regular "$(subshell1)" "`subshell2`"
```
Process substitution:
regular <(subshell1) >(subshell2)
```sh
regular <(subshell1) >(subshell2)
```
Some forms of grouping:
( subshell )
{ regular; }
```sh
( subshell )
{ regular; }
```
Backgrounding:
subshell1 &
subshell2 &
```sh
subshell1 &
subshell2 &
```
Anything executed by external processes:
find . -exec subshell1 {} \;
find . -print0 | xargs -0 subshell2
sudo subshell3
su -c subshell4
```sh
find . -exec subshell1 {} \;
find . -print0 | xargs -0 subshell2
sudo subshell3
su -c subshell4
```
This applies not only to setting variables, but also setting shell options and changing directories.

@@ -2,12 +2,16 @@
### Problematic code:
foo() { bar --baz "$@"; frob --baz "$@"; };
find . -exec foo {} +
```sh
foo() { bar --baz "$@"; frob --baz "$@"; };
find . -exec foo {} +
```
### Correct code:
find . -exec sh -c 'bar --baz "$@"; frob --baz "$@";' -- {} +
```sh
find . -exec sh -c 'bar --baz "$@"; frob --baz "$@";' -- {} +
```
### Rationale:
@@ -19,7 +23,9 @@ Instead, the function contents can be executed in a shell, either through `sh -c
If you're intentionally passing a word that happens to have the same name as a declared function, you can quote it to make shellcheck ignore it, e.g.
nobody() {
sudo -u "nobody" "$@"
}
```sh
nobody() {
sudo -u "nobody" "$@"
}
```

@@ -2,13 +2,17 @@
### Problematic code:
foo=42
echo "$FOO"
```sh
foo=42
echo "$FOO"
```
### Correct code:
foo=42
echo "$foo"
```sh
foo=42
echo "$foo"
```
### Rationale:
@@ -22,20 +26,26 @@ ShellCheck may not always realize that the variable is in use (especially with i
For throwaway variables, consider using `_` as a dummy:
read _ last _ zip _ _ <<< "$str"
echo "$last, $zip"
```sh
read _ last _ zip _ _ <<< "$str"
echo "$last, $zip"
```
or use a directive to disable the warning:
# shellcheck disable=SC2034
read first last email zip lat lng <<< "$str"
echo "$last, $zip"
```sh
# shellcheck disable=SC2034
read first last email zip lat lng <<< "$str"
echo "$last, $zip"
```
For indirection, there's not much you can do without rewriting to use arrays or similar:
bar=42 # will always appear unused
foo=bar
echo "${!foo}"
```sh
bar=42 # will always appear unused
foo=bar
echo "${!foo}"
```
This is expected behavior, and not a bug. There is no good way to statically analyze indirection in shell scripts, just like static C analyzers have a hard time preventing segfaults.

@@ -2,11 +2,15 @@
### Problematic code:
sum=find | wc -l
```sh
sum=find | wc -l
```
### Correct code:
sum=$(find | wc -l)
```sh
sum=$(find | wc -l)
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
find . -type f | xargs md5sum
```sh
find . -type f | xargs md5sum
```
### Correct code:
find . -type f -print0 | xargs -0 md5sum
```sh
find . -type f -print0 | xargs -0 md5sum
```
### Rationale:

@@ -2,17 +2,21 @@
### Problematic code:
for i in 'seq 1 10'
do
echo "$i"
done
```sh
for i in 'seq 1 10'
do
echo "$i"
done
```
### Correct code:
for i in $(seq 1 10)
do
echo "$i"
done
```sh
for i in $(seq 1 10)
do
echo "$i"
done
```
### Rationale:

@@ -2,10 +2,12 @@
### Problematic code:
for var in value
do
echo "$var"
done
```sh
for var in value
do
echo "$var"
done
```
### Correct code:
@@ -13,19 +15,27 @@ Correct code depends on what you want to do.
To iterate over files in a directory, instead of `for var in /my/dir` use:
for var in /my/dir/* ; do echo "$var"; done
```sh
for var in /my/dir/* ; do echo "$var"; done
```
To iterate over lines in a file or command output, use a while read loop instead:
mycommand | while IFS= read -r line; do echo "$line"; done
```sh
mycommand | while IFS= read -r line; do echo "$line"; done
```
To iterate over *words* written to a command or function's stdout, instead of `for var in myfunction`, use
for var in $(myfunction); do echo "$var"; done
```sh
for var in $(myfunction); do echo "$var"; done
```
To iterate over *words* in a variable, instead of `for var in myvariable`, use
for var in $myvariable; do echo "$var"; done
```sh
for var in $myvariable; do echo "$var"; done
```

@@ -2,13 +2,15 @@
### Problematic code:
for file in $(find mydir -mtime -7 -name '*.mp3')
do
let count++
echo "Playing file no. $count"
play "$file"
done
echo "Played $count files"
```sh
for file in $(find mydir -mtime -7 -name '*.mp3')
do
let count++
echo "Playing file no. $count"
play "$file"
done
echo "Played $count files"
```
This will fail for filenames containing spaces and similar, such as `My File.mp3`, and has a series of potential globbing issues depending on other filenames in the directory like (if you have `MyFile2.mp3` and `MyFile[2014].mp3`, the former file will play twice and the latter will not play at all).
@@ -18,13 +20,15 @@ There are many possible fixes, each with its pros and cons.
The most general fix (that requires the least amount of thinking to apply) is having `find` output a `\0` separated list of files and consuming them in a `while read` loop:
while IFS= read -r -d '' file
do
let count++
echo "Playing file no. $count"
play "$file"
done < <(find mydir -mtime -7 -name '*.mp3' -print0)
echo "Played $count files"
```sh
while IFS= read -r -d '' file
do
let count++
echo "Playing file no. $count"
play "$file"
done < <(find mydir -mtime -7 -name '*.mp3' -print0)
echo "Played $count files"
```
In usage it's very similar to the `for` loop: it gets its output from a `find` statement, it executes a shell script body, it allows updating/aggregating variables, and the variables are available when the loop ends.
@@ -34,14 +38,16 @@ It requires Bash, and works with GNU, Busybox, OS X, FreeBSD and OpenBSD find, b
If you don't need `find` logic like `-mtime -7` and just use it to match globs recursively (all `*.mp3` files under a directory), you can instead use `globstar` and `nullglob` instead of `find`, and still use a `for` loop:
shopt -s globstar nullglob
for file in mydir/**/*.mp3
do
let count++
echo "Playing file no. $count"
play "$file"
done
echo "Played $count files"
```sh
shopt -s globstar nullglob
for file in mydir/**/*.mp3
do
let count++
echo "Playing file no. $count"
play "$file"
done
echo "Played $count files"
```
This is bash specific.
@@ -50,15 +56,17 @@ This is bash specific.
If you need POSIX compliance, this is a fair approach:
find mydir ! -name "$(printf "*\n*")" -name '*.mp3' > tmp
while IFS= read -r file
do
let count++
echo "Playing file #$count"
play "$file"
done < tmp
rm tmp
echo "Played $count files"
```sh
find mydir ! -name "$(printf "*\n*")" -name '*.mp3' > tmp
while IFS= read -r file
do
let count++
echo "Playing file #$count"
play "$file"
done < tmp
rm tmp
echo "Played $count files"
```
The only problem is for filenames containing line feeds. A `! -name "$(printf "*\n*")"` has been added to simply skip these files, just in case there are any.
@@ -68,8 +76,10 @@ If you don't need variables to be available after the loop (here, if you don't n
If you don't need a shell script loop body or any form of variable like if we only wanted to play the file, we can dramatically simplify while maintaining POSIX compatibility:
# Simple and POSIX
find mydir -name '*.mp3' -exec play {} \;
```sh
# Simple and POSIX
find mydir -name '*.mp3' -exec play {} \;
```
This does not allow things like `let counter++` because `let` is a shell builtin, not an external command.
@@ -77,10 +87,12 @@ This does not allow things like `let counter++` because `let` is a shell builtin
If we do need a shell script body but no aggregation, you can do the above but invoking `sh` (this is still POSIX):
find mydir -name '*.mp3' -exec sh -c '
echo "Playing ${1%.mp3}"
play "$1"
' sh {} \;
```sh
find mydir -name '*.mp3' -exec sh -c '
echo "Playing ${1%.mp3}"
play "$1"
' sh {} \;
```
This would not be possible without `sh`, because `${1%.mp3}` is a shell construct that `find` can't evaluate by itself. If we had tried to `let counter++` in this loop, we would have found that the value never changes.

@@ -2,11 +2,15 @@
### Problematic code:
cp $* ~/dir
```sh
cp $* ~/dir
```
### Correct code:
cp "$@" ~/dir
```sh
cp "$@" ~/dir
```
### Rationale:

@@ -2,17 +2,17 @@
### Problematic code:
for i in {1..$n}
do
echo "$i"
done
for i in {1..$n}
do
echo "$i"
done
### Correct code:
for ((i=0; i<n; i++))
do
echo "$i"
done
for ((i=0; i<n; i++))
do
echo "$i"
done
### Rationale:
@@ -20,16 +20,16 @@ In Bash, brace expansion happens before variable expansion. This means that brac
For integers, use an arithmetic for loop instead. For zero-padded numbers or letters, use of eval may be warranted:
from="a" to="m"
for c in $(eval "echo {$from..$to}"); do echo "$c"; done
from="a" to="m"
for c in $(eval "echo {$from..$to}"); do echo "$c"; done
or more carefully (if `from`/`to` could be user input, or if the brace expansion could have spaces):
from="a" to="m"
while IFS= read -d '' -r c
do
echo "Read $c"
done < <(eval "printf '%s\0' $(printf "{%q..%q}.jpg" "$from" "$to")")
from="a" to="m"
while IFS= read -d '' -r c
do
echo "Read $c"
done < <(eval "printf '%s\0' $(printf "{%q..%q}.jpg" "$from" "$to")")
### Exceptions

@@ -2,17 +2,21 @@
### Problematic code:
if [[ $1 != foo || $1 != bar ]]
then
echo "$1 is not foo or bar"
fi
```sh
if [[ $1 != foo || $1 != bar ]]
then
echo "$1 is not foo or bar"
fi
```
### Correct code:
if [[ $1 != foo && $1 != bar ]]
then
echo "$1 is not foo or bar"
fi
```sh
if [[ $1 != foo && $1 != bar ]]
then
echo "$1 is not foo or bar"
fi
```
### Rationale:

@@ -2,19 +2,25 @@
### Problematic code:
printf "Hello, $NAME\n"
```sh
printf "Hello, $NAME\n"
```
### Correct code:
printf "Hello, %s\n" "$NAME"
```sh
printf "Hello, %s\n" "$NAME"
```
### Rationale:
`printf` interprets escape sequences and format specifiers in the format string. If variables are included, any escape sequences or format specifiers in the data will be interpreted too, when you most likely wanted to treat it as data. Example:
coverage='96%'
printf "Unit test coverage: %s\n" "$coverage"
printf "Unit test coverage: $coverage\n"
```sh
coverage='96%'
printf "Unit test coverage: %s\n" "$coverage"
printf "Unit test coverage: $coverage\n"
```
The first printf writes `Unit test coverage: 96%`.
@@ -24,7 +30,9 @@ The second writes ``bash: printf: `\': invalid format character``
Sometimes you may actually want to interpret data as a format string, like in:
hexToAscii() { printf "\x$1"; }
hexToAscii 21
```sh
hexToAscii() { printf "\x$1"; }
hexToAscii 21
```
Like all warnings, you can selectively silence this warning with a [directive](Directive).

@@ -2,11 +2,15 @@
### Problematic code:
tr -cd [:digit:]
```sh
tr -cd [:digit:]
```
### Correct code:
tr -cd '[:digit:]'
```sh
tr -cd '[:digit:]'
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
grep '*foo*'
```sh
grep '*foo*'
```
### Correct code:
grep 'foo' # or more explicitly, grep '.*foo.*'
```sh
grep 'foo' # or more explicitly, grep '.*foo.*'
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
trap "echo \"Finished on $(date)\"" EXIT
```sh
trap "echo \"Finished on $(date)\"" EXIT
```
### Correct code:
trap 'echo "Finished on $(date)"' EXIT
```sh
trap 'echo "Finished on $(date)"' EXIT
```
### Rationale:

@@ -2,30 +2,40 @@
### Problematic code:
for s in "$(mycommand)"; do echo "$s"; done
```sh
for s in "$(mycommand)"; do echo "$s"; done
```
### Correct code:
The correct code depends on your intention. Let's say you're in a directory with the files `file.png` and `My cat.png`, and you want to loop over a command that outputs (or variable that contains):
hello world
My *.png
```sh
hello world
My *.png
```
#### Loop over each line without globbing (`hello world`, `My *.png`)
mycommand | while IFS= read -r s; do echo "$s"; done
```sh
mycommand | while IFS= read -r s; do echo "$s"; done
```
#### Loop over each word with globbing (`hello`, `world`, `My`, `file.png`, `My cat.png`):
# relies on the fact that IFS by default contains space-tab-linefeed
for s in $(mycommand); do echo "$s"; done
```sh
# relies on the fact that IFS by default contains space-tab-linefeed
for s in $(mycommand); do echo "$s"; done
```
#### Loop over each line with globbing (`hello world`, `My cat.png`)
# explicitly set IFS to contain only a line feed
IFS='
'
for s in $(mycommand); do echo "$s"; done
```sh
# explicitly set IFS to contain only a line feed
IFS='
'
for s in $(mycommand); do echo "$s"; done
```
### Rationale:

@@ -2,13 +2,17 @@
### Problematic code:
find . -type f -exec shellcheck {} | wc -l \;
find . -exec echo {} ;
```sh
find . -type f -exec shellcheck {} | wc -l \;
find . -exec echo {} ;
```
### Correct code:
find . -type f -exec sh -c 'shellcheck "$1" | wc -l' -- {} \;
find . -exec echo {} \;
```sh
find . -type f -exec sh -c 'shellcheck "$1" | wc -l' -- {} \;
find . -exec echo {} \;
```
### Rationale:
@@ -18,11 +22,15 @@
To instead go through each file and run `foo file && bar file` on it, invoke a shell that can interpret `&&`:
find . -exec sh 'foo "$1" && bar "$1"' -- {} \;
```sh
find . -exec sh 'foo "$1" && bar "$1"' -- {} \;
```
You can also use find `-a` instead of shell `&&`:
find . -exec foo {} \; -a -exec bar {} \;
```sh
find . -exec foo {} \; -a -exec bar {} \;
```
This will have the same effect (`-a` is also the default when two commands are specified, and can therefore be omitted).

@@ -2,11 +2,15 @@
### Problematic code:
cp $@ ~/dir
```sh
cp $@ ~/dir
```
### Correct code:
cp "$@" ~/dir
```sh
cp "$@" ~/dir
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
firefox 2>&1 > /dev/null
```sh
firefox 2>&1 > /dev/null
```
### Correct code:
firefox > /dev/null 2>&1
```sh
firefox > /dev/null 2>&1
```
### Rationale:

@@ -2,40 +2,48 @@
### Problematic code:
if [ -n $var ]
then
echo "var has a value"
else
echo "var is empty"
fi
```sh
if [ -n $var ]
then
echo "var has a value"
else
echo "var is empty"
fi
```
### Correct code:
In bash/ksh:
if [[ -n $var ]]
then
echo "var has a value"
else
echo "var is empty"
fi
```sh
if [[ -n $var ]]
then
echo "var has a value"
else
echo "var is empty"
fi
```
In POSIX:
if [ -n "$var" ]
then
echo "var has a value"
else
echo "var is empty"
fi
```sh
if [ -n "$var" ]
then
echo "var has a value"
else
echo "var is empty"
fi
```
### Rationale:
When `$var` is unquoted, a blank value will cause it to wordsplit and disappear. If `$var` is empty, these two statements are identical:
[ -n $var ]
[ -n ]
```sh
[ -n $var ]
[ -n ]
```
`[ string ]` is shorthand for testing if a string is empty. This is still true if `string` happens to be `-n`. `[ -n ]` is therefore true, and by extension so is `[ -n $var ]`.

@@ -2,12 +2,16 @@
### Problematic code:
[[ 2 -lt 3.14 ]]
```sh
[[ 2 -lt 3.14 ]]
```
### Correct code:
[[ 200 -lt 314 ]] # Use fixed point math
[[ $(echo "2 < 3.14" | bc) == 1 ]] # Use bc
```sh
[[ 200 -lt 314 ]] # Use fixed point math
[[ $(echo "2 < 3.14" | bc) == 1 ]] # Use bc
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
[[ $foo =~ "^fo+bar$" ]]
```sh
[[ $foo =~ "^fo+bar$" ]]
```
### Correct code:
[[ $foo =~ ^fo+bar$ ]]
```sh
[[ $foo =~ ^fo+bar$ ]]
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
[[ 0=1 ]]
```sh
[[ 0=1 ]]
```
### Correct code:
[[ 0 = 1 ]]
```sh
[[ 0 = 1 ]]
```
### Rationale:

@@ -2,35 +2,43 @@
### Problematic code:
var_1="hello world"
n=1
echo "${var_$n}"
```sh
var_1="hello world"
n=1
echo "${var_$n}"
```
### Correct code:
Bash/ksh:
# Use arrays instead of dynamic names
declare -a var
var[1]="hello world"
n=1
echo "${var[n]}"
```sh
# Use arrays instead of dynamic names
declare -a var
var[1]="hello world"
n=1
echo "${var[n]}"
```
or
# Expand variable names dynamically
var_1="hello world"
n=1
name="var_$n"
echo "${!name}"
```sh
# Expand variable names dynamically
var_1="hello world"
n=1
name="var_$n"
echo "${!name}"
```
POSIX sh:
# Expand dynamically with eval
var_1="hello world"
n=1
eval "tmp=\$var_$n"
echo "${tmp}"
```sh
# Expand dynamically with eval
var_1="hello world"
n=1
eval "tmp=\$var_$n"
echo "${tmp}"
```
### Rationale:

@@ -2,41 +2,53 @@
### Problematic code:
i=4
$(( i++ ))
```sh
i=4
$(( i++ ))
```
### Correct code:
Bash, Ksh:
i=4
(( i++ ))
```sh
i=4
(( i++ ))
```
POSIX (assuming `++` is supported):
i=4
_=$(( i++ ))
```sh
i=4
_=$(( i++ ))
```
Alternative POSIX version that does not preserve the exit code:
: $(( i++ ))
```sh
: $(( i++ ))
```
### Rationale:
`$((..))` expands to a number. If it's the only word on the line, the shell will try to execute this number as a command name:
$ i=4
$ $(( i++ ))
4: command not found
$ echo $i
5
```sh
$ i=4
$ $(( i++ ))
4: command not found
$ echo $i
5
```
To avoid trying to execute the number as a command name, use one of the methods mentioned:
$ i=4
$ _=$(( i++ ))
$ echo $i
5
```sh
$ i=4
$ _=$(( i++ ))
$ echo $i
5
```
### Exceptions:

@@ -2,15 +2,18 @@
### Problematic code:
ssh host.example.com << EOF
echo "Logged in on $HOSTNAME"
EOF
```sh
ssh host.example.com << EOF
echo "Logged in on $HOSTNAME"
EOF
### Correct code:
ssh host.example.com << "EOF"
echo "Logged in on $HOSTNAME"
EOF
```sh
ssh host.example.com << "EOF"
echo "Logged in on $HOSTNAME"
EOF
```
### Rationale:
@@ -20,15 +23,19 @@ This means that before sending the commands to the server, the client replaces `
Scripts with any kind of variable use are especially problematic because all references will be expanded before the script run. For example,
ssh host << EOF
x="$(uname -a)"
echo "$x"
EOF
```sh
ssh host << EOF
x="$(uname -a)"
echo "$x"
EOF
```
will never print anything, neither client nor server details, since before evaluation, it will be expanded to:
x="Linux localhost ... x86_64 GNU/Linux"
echo ""
```sh
x="Linux localhost ... x86_64 GNU/Linux"
echo ""
```
By quoting the here token, local expansion will not take place, so the server sees `echo "Logged in on $HOSTNAME"` which is expanded and printed with the server's hostname, which is usually the intention.
@@ -38,6 +45,8 @@ If the client should expand some or all variables, this message can and should b
To expand a mix of local and remote variables, the here doc end token should be unquoted, and the remote variables should be escaped, e.g.
ssh host.example.com << EOF
echo "Logged in on \$HOSTNAME from $HOSTNAME"
EOF
```sh
ssh host.example.com << EOF
echo "Logged in on \$HOSTNAME from $HOSTNAME"
EOF
```

@@ -2,11 +2,15 @@
### Problematic code:
rm "~/Desktop/$filename"
```sh
rm "~/Desktop/$filename"
```
### Correct code:
rm "$HOME/Desktop/$filename"
```sh
rm "$HOME/Desktop/$filename"
```
### Rationale:

@@ -2,21 +2,27 @@
### Problematic code:
args='-lh "My File.txt"'
ls $args
```sh
args='-lh "My File.txt"'
ls $args
```
### Correct code:
args=(-lh "My File.txt")
ls "${args[@]}"
```sh
args=(-lh "My File.txt")
ls "${args[@]}"
```
### Rationale:
Bash does not interpret data as code. Consider almost any other languages, such as Python:
print 1+1 # prints 2
a="1+1"
print a # prints 1+1, not 2
```sh
print 1+1 # prints 2
a="1+1"
print a # prints 1+1, not 2
```
Here, `1+1` is Python syntax for adding numbers. However, passing a literal string containing this expression does not cause Python to interpret it, see the `+` and produce the calculated result.
@@ -26,16 +32,20 @@ The solution is to use an array instead, whenever possible.
If you due to `sh` compatibility can't use arrays, you can use `eval` instead. However, this is very insecure and easy to get wrong, leading to various forms of security vulnerabilities and breakage:
quote() { local q=${1//\'/\'\\\'\'}; echo "'$q'"; }
args="-lh $(quote "My File.txt")"
eval ls "$args" # Do not use unless you understand implications
```sh
quote() { local q=${1//\'/\'\\\'\'}; echo "'$q'"; }
args="-lh $(quote "My File.txt")"
eval ls "$args" # Do not use unless you understand implications
```
If you ever accidentally forget to use proper quotes, such as with:
for f in *.txt; do
args="-lh '$1'" # Example security exploit
eval ls "$args" # Do not copy and use
done
```sh
for f in *.txt; do
args="-lh '$1'" # Example security exploit
eval ls "$args" # Do not copy and use
done
```
Then you can use `touch "'; rm -rf \$'\x2F'; '.txt"` (or someone can trick you into downloading a file with this name, or create a zip file or git repo containing it, or changing their nick and have your chat client create the file for a chat log, or...), and running the script to list your files will run the command `rm -rf /`.

@@ -2,17 +2,21 @@
### Problematic code:
if $(which epstopdf)
then
echo "Found epstopdf"
fi
```sh
if $(which epstopdf)
then
echo "Found epstopdf"
fi
```
### Correct code:
if which epstopdf
then
echo "Found epstopdf"
fi
```sh
if which epstopdf
then
echo "Found epstopdf"
fi
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
grep foo file.txt | sed -e 's/foo/bar/g' > file.txt
```sh
grep foo file.txt | sed -e 's/foo/bar/g' > file.txt
```
### Correct code:
grep foo file.txt | sed -e 's/foo/bar/g' > tmpfile && mv tmpfile file.txt
```sh
grep foo file.txt | sed -e 's/foo/bar/g' > tmpfile && mv tmpfile file.txt
```
### Rationale:

@@ -2,12 +2,16 @@
### Problematic code:
#!/usr/bin/env bash -x
```sh
#!/usr/bin/env bash -x
```
### Correct code:
#!/usr/bin/env bash
set -x
```sh
#!/usr/bin/env bash
set -x
```
### Rationale:

@@ -2,19 +2,25 @@
### Problematic code:
name=World cmd -m "Hello $name"
```sh
name=World cmd -m "Hello $name"
```
### Correct code:
export name=World
cmd -m "Hello $name"
```sh
export name=World
cmd -m "Hello $name"
```
To prevent setting the variable, this can also be done in a subshell:
(
export name=World
cmd -m "Hello $name"
) # 'name' does not leave this subshell
```sh
(
export name=World
cmd -m "Hello $name"
) # 'name' does not leave this subshell
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
gzip file[:digit:]*.txt
```sh
gzip file[:digit:]*.txt
```
### Correct code:
gzip file[[:digit:]]*.txt
```sh
gzip file[[:digit:]]*.txt
```
### Rationale:

@@ -2,32 +2,38 @@
### Problematic code:
for dir in */
do
cd "$dir"
convert index.png index.jpg
cd ..
done
```sh
for dir in */
do
cd "$dir"
convert index.png index.jpg
cd ..
done
```
### Correct code:
for dir in */
do
(
cd "$dir" || exit
convert index.png index.jpg
)
done
```sh
for dir in */
do
(
cd "$dir" || exit
convert index.png index.jpg
)
done
```
or
for dir in */
do
cd "$dir" || exit
convert index.png index.jpg
cd ..
done
```sh
for dir in */
do
cd "$dir" || exit
convert index.png index.jpg
cd ..
done
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
[ "$1" = "-v" && -z "$2" ]
```sh
[ "$1" = "-v" && -z "$2" ]
```
### Correct code:
[ "$1" = "-v" ] && [ -z "$2" ]
```sh
[ "$1" = "-v" ] && [ -z "$2" ]
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
[[ "$1" = "-v" -a -z "$2" ]]
```sh
[[ "$1" = "-v" -a -z "$2" ]]
```
### Correct code:
[[ "$1" = "-v" && -z "$2" ]]
```sh
[[ "$1" = "-v" && -z "$2" ]]
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
[ "$1" = "-v" || "$1" = "-help" ]
```sh
[ "$1" = "-v" || "$1" = "-help" ]
```
### Correct code:
[ "$1" = "-v" ] || [ "$1" = "-help" ]
```sh
[ "$1" = "-v" ] || [ "$1" = "-help" ]
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
[[ "$1" = "-v" -o "$1" = "-help" ]]
```sh
[[ "$1" = "-v" -o "$1" = "-help" ]]
```
### Correct code:
[[ "$1" = "-v" || "$1" = "-help" ]]
```sh
[[ "$1" = "-v" || "$1" = "-help" ]]
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
rm -rf /usr /lib/nvidia-current/xorg/xorg
```sh
rm -rf /usr /lib/nvidia-current/xorg/xorg
```
### Correct code:
rm -rf /usr/lib/nvidia-current/xorg/xorg
```sh
rm -rf /usr/lib/nvidia-current/xorg/xorg
```
### Rationale:
@@ -18,6 +22,8 @@ Due to an accidental space, it deleted `/usr` instead of just the particular dir
In cases of chroot, initramfs and similar, it's reasonable to delete otherwise important directories. Due to this, Shellcheck will not warn if the command contains `--`:
rm -rf -- /usr
```sh
rm -rf -- /usr
```
This is an arbitrary convention to allow deleting such directories without having to use a [[directive]] to silence the warning.

@@ -2,11 +2,15 @@
### Problematic code:
rm -rf "$STEAMROOT/"*
```sh
rm -rf "$STEAMROOT/"*
```
### Correct code:
rm -rf "${STEAMROOT:?}/"*
```sh
rm -rf "${STEAMROOT:?}/"*
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
a=$(echo $?)
```sh
a=$(echo $?)
```
### Correct code:
a="$?"
```sh
a="$?"
```
### Rationale:

@@ -2,14 +2,18 @@
### Problematic code:
whoami
su
whoami
```sh
whoami
su
whoami
```
### Correct code:
whoami
sudo whoami
```sh
whoami
sudo whoami
```
### Rationale:

@@ -2,19 +2,23 @@
### Problematic code:
sayhello() {
echo "Hello $1"
}
sayhello
```sh
sayhello() {
echo "Hello $1"
}
sayhello
```
`./myscript World` just prints "Hello " instead of "Hello World".
### Correct code:
sayhello() {
echo "Hello $1"
}
sayhello "$@"
```sh
sayhello() {
echo "Hello $1"
}
sayhello "$@"
```
`./myscript World` now prints "Hello World".
@@ -26,9 +30,11 @@ If you want to process your script's parameters in a function, you have to expli
Note that `"$@"` refers to the current context's positional parameters, so if you call a function from a function, you have to pass in `"$@"` to both of them:
first() { second "$@"; }
second() { echo "The first script parameter is: $1"; }
first "$@"
```sh
first() { second "$@"; }
second() { echo "The first script parameter is: $1"; }
first "$@"
```
### Exceptions

@@ -2,12 +2,16 @@
### Problematic code:
set var=42
set var 42
```sh
set var=42
set var 42
```
### Correct code:
var=42
```sh
var=42
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
[[ a &lt;= b ]]
```sh
[[ a &lt;= b ]]
```
### Correct code:
[[ ! a > b ]]
```sh
[[ ! a > b ]]
```
### Rationale:

@@ -2,20 +2,26 @@
### Problematic code:
PATH=/my/dir
cat "$PATH/myfile"
```sh
PATH=/my/dir
cat "$PATH/myfile"
```
### Correct code:
Good practice: always use lowercase for unexported variables.
path=/my/dir
cat "$path/myfile"
```sh
path=/my/dir
cat "$path/myfile"
```
Bad practice: use another uppercase name.
MYPATH=/my/dir
cat "$MYPATH/myfile"
```sh
MYPATH=/my/dir
cat "$MYPATH/myfile"
```
### Rationale:

@@ -2,25 +2,33 @@
### Problematic code:
var=$@
for i in $var; do ..; done
```sh
var=$@
for i in $var; do ..; done
```
or
set -- Hello World
msg=$@
echo "You said $msg"
```sh
set -- Hello World
msg=$@
echo "You said $msg"
```
### Correct code:
var=( "$@" )
for i in "${var[@]}"; do ..; done
```sh
var=( "$@" )
for i in "${var[@]}"; do ..; done
```
or
set -- Hello World
msg=$*
echo "You said $msg"
```sh
set -- Hello World
msg=$*
echo "You said $msg"
```
### Rationale:

@@ -2,13 +2,17 @@
### Problematic code:
foo={1..9}
echo $foo
```sh
foo={1..9}
echo $foo
```
### Correct code:
foo=( {1..9} )
echo "${foo[@]}"
```sh
foo=( {1..9} )
echo "${foo[@]}"
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
grep foo | wc -l
```sh
grep foo | wc -l
```
### Correct code:
grep -c foo
```sh
grep -c foo
```
### Rationale:
@@ -14,10 +18,12 @@ This is purely a stylistic issue. `grep` can count lines without piping to `wc`.
Note that in many cases, this number is only used to see whether there are matches (i.e. `> 0`). In these cases, it's better and more efficient to use `grep -q` and check its exit status:
if grep -q pattern file
then
echo "The file contains the pattern"
fi
```sh
if grep -q pattern file
then
echo "The file contains the pattern"
fi
```
### Exceptions

@@ -2,19 +2,23 @@
### Problematic code:
myarray=(foo bar)
for f in $myarray
do
cat "$f"
done
```sh
myarray=(foo bar)
for f in $myarray
do
cat "$f"
done
```
### Correct code:
myarray=(foo bar)
for f in "${myarray[@]}"
do
cat "$f"
done
```sh
myarray=(foo bar)
for f in "${myarray[@]}"
do
cat "$f"
done
```
### Rationale:

@@ -2,18 +2,22 @@
### Problematic code:
echo foo >> file
date >> file
cat stuff >> file
```sh
echo foo >> file
date >> file
cat stuff >> file
```
### Correct code:
{
echo foo
date
cat stuff
} >> file
```sh
{
echo foo
date
cat stuff
} >> file
```
### Rationale:

@@ -2,11 +2,15 @@
### Problematic code:
[[ $foo -eq "Y" ]]
```sh
[[ $foo -eq "Y" ]]
```
### Correct code:
[[ $foo = "Y" ]]
```sh
[[ $foo = "Y" ]]
```
### Rationale:

Some files were not shown because too many files have changed in this diff Show More