ShellCheck: A Second Pair of Eyes
ShellCheck reads a script without running it and points out likely mistakes, each with its line, a code such as SC2086 and a plain message; the codes worth knowing are an unquoted variable, a cd that might fail and a loop over ls, and a disable comment silences one warning on purpose.
Journeyman · Unix · Linux · Updated
After this lesson you can
- Run ShellCheck on a script and read each warning, its line, its code and its message
- Fix the problems behind SC2086, SC2164 and SC2045, and silence one warning on purpose with a disable comment
Key takeaways
- ShellCheck is a free linter for shell scripts: shellcheck script.sh reads the script without running it and lists the lines that are likely mistakes.
- Each warning shows the line, points at the part with ^, and gives a code such as SC2086, a level (error, warning, info or style) and a message.
- SC2086 asks for double quotes around a variable, SC2164 for cd ... || exit, and SC2045 for a wildcard instead of a loop over ls.
- Every code has a page at shellcheck.net/wiki with an example and the fix.
shellcheck disable=SC2086 on the line above silences one warning there. Use it only when you've read the warning and are sure.
Before an essay goes in, a friend reads it and circles the sentence that doesn't say what you meant. Scripts have the same kind of friend. ShellCheck is a linter, a program that reads your code without running it and points out lines that are likely mistakes. If you write scripts in VS Code with its ShellCheck extension (Writing Scripts in VS Code), you've met its wavy underlines. This lesson runs it in the terminal, where it can check any script, on any computer, before the script ever touches a file. Teams that keep scripts in a shared project often run it on every change, so a risky line is caught before anyone runs it.
A script that looks fine
Here's a mover written in a hurry, quick-move.sh, for the mover practice folder from Errors a User Can Act On. It goes into the folder it's given and moves everything up into photos, with mv -n so nothing is replaced (Copy, Move and Rename: cp and mv). Make it with nano and chmod +x it:
#!/usr/bin/env bash
# quick-move.sh: a mover written in a hurry.
cd $1
for file in $(ls); do
mv -n $file ../photos/
done
Run the practice lines for a fresh folder, add a photo with a space in its name, and try it:
printf 'waves\n' > "inbox/beach day.jpg"
./quick-move.sh inbox
ls inbox
mv: cannot stat 'beach': No such file or directory
mv: cannot stat 'day.jpg': No such file or directory
IMG_0412.jpg 'beach day.jpg'
beach day.jpg was split into two names that don't exist. (IMG_0412.jpg stayed because mv -n won't replace the cake, without a word.) Each of the script's lines has a problem, and ShellCheck finds them all without running anything.
Install ShellCheck and run it
Install it once with your package manager (Installing Tools: Homebrew and apt); sudo asks for your password:
sudo apt install shellcheck # Ubuntu, and Ubuntu in WSL
brew install shellcheck # a Mac, with Homebrew
Then give it a script:
shellcheck quick-move.sh
ShellCheck prints each line it has something to say about, then a ^ and a row of dashes under the exact part it means, and then:
- the code, such as
SC2086, the same code VS Code shows; - the level:
error(it will go wrong),warning(it can go wrong),infoorstyle(worth a look); - a message in one sentence.
When it has nothing to say, it prints nothing, and its exit code is 0, so a script can check another script with shellcheck script.sh && echo "Looks good". You can also paste a script into the box at shellcheck.net in a browser.
The codes worth knowing
A handful of codes come up again and again. The messages here are quoted from ShellCheck's wiki:
- SC2086, Double quote to prevent globbing and word splitting. Write
"$file"and"$1": quotes around every variable, so a name with a space stays one name. - SC2164, Use
cd ... || exitin casecdfails. Writecd "$1" || exit, so a wrong folder name stops the script instead of running the rest in the folder you happened to be in. - SC2045, Iterating over ls output is fragile. Use globs. Write
for file in *instead offor file in $(ls). - SC2154, var is referenced but not assigned. Fix the variable's name: it catches a typo such as
$photo_dribefore the script runs, where strict mode catches it only when that line runs. - SC2162,
readwithout-rwill mangle backslashes. Writeread -r.
Every code has its own page, with an example and the fix: https://www.shellcheck.net/wiki/SC2045, with the code you want at the end. Reading that page is the quickest way to understand a warning you haven't seen before.
Fix it, or silence one on purpose
Here's quick-move.sh with ShellCheck's advice taken: quotes around each variable, || exit after cd, and a wildcard instead of $(ls):
#!/usr/bin/env bash
# quick-move.sh: a mover written in a hurry, then fixed.
cd "$1" || exit
for file in *; do
mv -n "$file" ../photos/
done
Run it on the same folder:
./quick-move.sh inbox
ls inbox
IMG_0412.jpg
beach day.jpg moved in one piece. (The cake is still safe, and IMG_0412.jpg stays put, silently: the full mover.sh is the one that says so.)
Once in a while a warning is about something you meant. Then put a comment on the line right above it, naming the code, with a line of your own above that saying why, so the next reader knows it wasn't an accident:
options="-l -h"
# options holds two options on purpose, so let bash split it into two words.
# shellcheck disable=SC2086
ls $options
The comment silences that one code on that one line, and nothing else. Use it rarely: most warnings are right, even when the script happens to work today.
Check yourself
2 questions to try on the lesson page.
So what
ShellCheck reads every script you write the way a careful colleague would, and catches quoting, cd and loop mistakes before they ever run on real files.