Linux Basic — ShellCheck
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 bashis preferred) - SC2010 — don’t parse
ls - SC2207 — use
mapfileorread -a - SC2086 — quote
$filesand$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