Linux Basic — ShellCheck

3 minute read

ShellCheck

ShellCheck is a static analyser for shell scripts. It catches quoting bugs, unused variables, wrong test operators, and dozens of patterns that look correct but aren’t. Running it before every commit is the single highest-ROI habit a shell programmer can adopt.


Install

# macOS
brew install shellcheck

# Ubuntu/Debian
sudo apt install shellcheck

# Fedora
sudo dnf install shellcheck

# Or download a static binary from https://github.com/koalaman/shellcheck/releases

Most IDEs have a ShellCheck plugin (VS Code, Vim, Neovim, Emacs, Sublime). Set it up so warnings appear inline as you type — it’s transformative.


Run It

shellcheck script.sh

Sample output:

In script.sh line 5:
files=$(ls *.csv)
         ^-- SC2010 (warning): Don't use ls | grep. Use a glob or a for loop with a condition.
         ^-- SC2006 (style): Use $(...) notation instead of legacy backticks `...`.

In script.sh line 7:
for f in $files; do
         ^-- SC2086 (info): Double quote to prevent globbing and word splitting.

Each diagnostic has:

  • A code (e.g. SC2086) — Google it for examples and rationale
  • A severity (error / warning / info / style)
  • A pointer to the offending characters

The Top Rules You’ll See

Code Meaning
SC2086 “Double quote to prevent globbing and word splitting.” Probably the most common warning — quote your variables.
SC2034 Variable assigned but never used (typo? leftover?)
SC2155 “Declare and assign separately to avoid masking return values.” local x=$(cmd) always returns 0 — split the line.
SC2046 “Quote this to prevent word splitting.” (Result of command substitution.)
SC2164 “Use cd ... \|\| exit in case cd fails.”
SC2181 “Check the exit code directly with if cmd; not via $?.”
SC2207 “Use mapfile or read -a instead of arr=( $(cmd) ).”
SC2148 “Tips depend on target shell — add a shebang.”
SC1090 “Can’t follow non-constant source.” (Pass -x to follow.)

Read each one once with the wiki open — the rationale is usually a one-paragraph explanation of a real bug class.


Silence False Positives

Sometimes ShellCheck is being conservative and you genuinely know what you’re doing. Disable per-line or per-block with a directive:

# shellcheck disable=SC2086
unquoted_glob_intentional $variable

# Multiple at once
# shellcheck disable=SC2086,SC2046
cmd $intentionally_unquoted $intentionally_unquoted_too

Or at the top of the file to silence it everywhere:

#!/usr/bin/env bash
# shellcheck disable=SC2086

# ... whole script can have unquoted vars without warnings

Use directives sparingly — a # shellcheck disable line should always have a comment explaining why the warning doesn’t apply:

# shellcheck disable=SC2086  # intentional word-splitting of "$ssh_opts"
ssh $ssh_opts user@host

Source-File Following

For scripts that . lib.sh, ShellCheck needs to know to follow the source:

#!/usr/bin/env bash
# shellcheck source=lib.sh
. ./lib.sh

Or from the CLI:

shellcheck -x main.sh           # follow `source` statements

Without -x, ShellCheck reports SC1091 (“Not following: lib.sh”). With it, you get full cross-file analysis.


CI Integration

In a GitHub Actions workflow:

- name: ShellCheck
  uses: ludeeus/action-shellcheck@master
  with:
    severity: warning

GitLab CI:

shellcheck:
  image: koalaman/shellcheck-alpine
  script:
    - shellcheck scripts/*.sh

Local pre-commit hook (using pre-commit):

- repo: https://github.com/shellcheck-py/shellcheck-py
  rev: v0.10.0.1
  hooks:
    - id: shellcheck

Or a plain git hook:

# .git/hooks/pre-commit
#!/bin/sh
for f in $(git diff --cached --name-only --diff-filter=ACM | grep -E '\.sh$'); do
    shellcheck "$f" || exit 1
done

Specify the Target Shell

ShellCheck infers the shell from the shebang. Override if needed:

shellcheck -s bash   script.sh
shellcheck -s sh     script.sh        # POSIX-only mode (no bashisms allowed)
shellcheck -s dash   script.sh

Run with -s sh to verify a script is genuinely POSIX-portable.


A Before-and-After Example

#!/bin/bash
files=$(ls *.csv)
for f in $files; do
    head -1 $f
done

ShellCheck finds:

  • SC2148 — missing shebang (well, has bash but #!/usr/bin/env bash is preferred)
  • SC2010 — don’t parse ls
  • SC2207 — use mapfile or read -a
  • SC2086 — quote $files and $f

After fixes:

#!/usr/bin/env bash
set -euo pipefail
shopt -s nullglob

files=( *.csv )
for f in "${files[@]}"; do
    head -1 "$f"
done

Zero warnings. Handles edge cases (empty dir, filenames with spaces) the first version silently broke on.


Run It Right Now

If you have any shell script in your repo, run:

shellcheck **/*.sh

You’ll likely find a handful of latent bugs. Each one is documented at https://www.shellcheck.net/wiki/SCxxxx — read, fix, learn the rule, never write that bug again.


Common Mistakes

❌ Wrong ✅ Right Issue
Ignoring SC2086 (“it works for me”) quote your vars works until a filename has a space
Sprinkling # shellcheck disable= to silence noise fix the warning, or comment why disables hide real bugs
Running shellcheck only at the end run on save (editor plugin) catch bugs as you type
Forgetting -x for sourced libs use # shellcheck source=… or -x cross-file checks needed
Treating warnings as nice-to-have each warning is a documented bug class shellcheck is consensus best practice

Next → Debug Mode

Updated: