Skip to main content
RunBook Academy

LinuxXXXV · Shell Scripting for SysadminsShellCheck

ShellCheck and static analysis - catching bugs before runtime

Foundation⏱ ~10 minshellcheck

What you'll learn

  • Run ShellCheck
  • Interpret ShellCheck output
  • Fix common ShellCheck warnings
  • Integrate ShellCheck into CI

Prerequisites

Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09

Not yet marked complete on this device.

ShellCheck is a static analysis tool for shell scripts. It catches dozens of common bugs and style issues. Run it before commit; gate CI on it.

Install ShellCheck

# Debian/Ubuntu
sudo apt install shellcheck

# RHEL family
sudo dnf install ShellCheck

# macOS
brew install shellcheck

Run ShellCheck

shellcheck my-script.sh

For a script containing this:

#!/usr/bin/env bash

foo=$1

if [ $foo = "bar" ]; then
  echo "matched"
fi

cd /tmp/my-temp
rm -f ./stale.lock

ShellCheck 0.11 prints:

In my-script.sh line 5:
if [ $foo = "bar" ]; then
     ^--^ SC2086 (info): Double quote to prevent globbing and word splitting.

Did you mean:
if [ "$foo" = "bar" ]; then


In my-script.sh line 9:
cd /tmp/my-temp
^-------------^ SC2164 (warning): Use 'cd ... || exit' or 'cd ... || return' in case cd fails.

Did you mean:
cd /tmp/my-temp || exit

For more information:
  https://www.shellcheck.net/wiki/SC2164 -- Use 'cd ... || exit' or 'cd ... |...
  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...

Read the shape of that output. Each finding gives the file and line, the offending source line, a caret range pointing at the exact token, a code (SC####), the severity in brackets, and a suggested rewrite. The wiki links at the end explain each code. ShellCheck exits non-zero when it reports anything at or above the configured severity, which is what makes it usable as a gate.

Common warnings

CodeWarningFix
SC2086Double quote to prevent globbingUse "$var"
SC2164Use `cd …
SC2034Variable appears unusedUse the variable or remove
SC2155Declare and assign separatelylocal foo; foo=$(cmd)
SC2068Quote array expansionsUse "${array[@]}"
SC2046Quote command substitutionUse "$(cmd)"

Severity levels

ShellCheck assigns severities:

  • error: likely a bug.
  • warning: probably a bug.
  • info: style or minor.
  • style: cosmetic.

Gate CI on errors. Review warnings. Optional: address info and style.

Disable specific checks

For the rare case where the warning is genuinely wrong, you can suppress it with a # shellcheck disable= directive. Placement matters more than the code you pass.

A directive must be on its own line, before the thing it applies to. It attaches forwards, never backwards.

# Disable one check for the NEXT command only
# shellcheck disable=SC2086  # word splitting is intended: FLAGS is a flag list
echo $FLAGS

# Disable for a whole function: put the directive immediately above it
# shellcheck disable=SC2034
setup_defaults() {
  UNUSED_BUT_EXPORTED_LATER=1
}

To disable a check for the whole file, put the directive immediately after the shebang:

#!/usr/bin/env bash
# shellcheck disable=SC1091   # sourced files live outside the repo

source /etc/myapp/env.sh

A trailing directive on the same line as the command does not work, and it fails loudly and confusingly:

In trailing.sh line 3:
echo $FLAGS # shellcheck disable=SC2086
^-- SC1073 (error): Couldn't parse this simple command. Fix to allow more checks.
            ^-- SC1126 (error): Place shellcheck directives before commands, not after.
                                       ^-- SC1072 (error):  Fix any mentioned problems and try again.

Note the scope rule: a directive above a command applies to that one command only, not to the rest of the file. If you want two lines suppressed, you need two directives, or you move the directive up to the enclosing function or to the top of the file.

Use suppressions sparingly, and always leave a comment explaining why the tool is wrong. An unexplained disable= is indistinguishable from someone silencing a real bug.

Integrate with editors

Most editors have a ShellCheck plugin:

  • VSCode: timonwong.shellcheck
  • Vim/Neovim: ALE or syntastic
  • Sublime: SublimeLinter-shellcheck

Real-time feedback in the editor catches issues at write time.

Integrate with CI

# GitHub Actions
- name: ShellCheck
  run: |
    mapfile -d '' -t files < <(find . -name '*.sh' -type f -print0)
    shellcheck --severity=warning -x -- "${files[@]}"

-x lets ShellCheck follow sourced files. --severity=warning sets the level at which the exit status turns non-zero. Reading the file list with -print0 and mapfile -d '' keeps paths containing spaces or newlines intact.

A failing ShellCheck run now fails the build. The script is not shippable until it passes.

Verify the gate before you trust it. Push a branch with a deliberately broken script and confirm the job goes red. A gate you have never seen fail is not a gate.

What ShellCheck does not catch

ShellCheck is a static analyser. It does not catch:

  • Runtime errors (the script still runs but does the wrong thing).
  • Performance issues.
  • Security holes beyond common patterns.
  • Logic bugs.

For deeper verification, test the script with real inputs.

Knowledge check

Knowledge check · 5 questions

  1. Q1. What is the right command to lint a script with ShellCheck?

  2. Q2. ShellCheck catches all shell bugs.

  3. Q3. Which of the following are valid ways to use ShellCheck? Select all that apply.

  4. Q4. Your ShellCheck job has been green for eighteen months. The step is `find . -name "*.sh" -exec shellcheck {} \;`. A reviewer points out that the job log is full of SC2086 findings. What is happening?

  5. Q5. An engineer adds `echo $FLAGS # shellcheck disable=SC2086` on line 40 of a 300-line script. CI now reports only SC1073 on line 40 and nothing else. Why?

Passing score: 75%. Answers are checked in this browser.