# ShellCheck Administrator & Developer Guide

This guide is the operational and architectural reference for ShellCheck — the GPLv3 static analysis tool for shell scripts. It is written for the people who **deploy** ShellCheck across a team or CI fleet, **build** it from source, **integrate** it into tooling, and **extend or contribute** to it. For end-user CLI usage, see the companion User Guide.

ShellCheck is a Haskell program of about 700 KB of source across ~30 modules. It parses a shell script into an abstract syntax tree, runs a battery of analyzers over that tree, filters the results by severity and user directives, and renders them through a pluggable formatter. This document covers every layer of that pipeline.

All facts here are derived from the ShellCheck source at version `0.11.0`, commit `764802b`.

---

## 1. Executive Summary

| Attribute | Value |
|-----------|-------|
| Project | ShellCheck — shell script static analysis tool |
| Version | `0.11.0` |
| Language | Haskell (Haskell2010, GHC 8.0 – 9.6.6) |
| License | GNU GPL v3 or later |
| Build systems | Cabal (primary) and Stack |
| Primary artifact | `shellcheck` — a single self-contained binary |
| Distributed as | Static Linux binaries, macOS/Windows binaries, ~20 package managers, Docker images |
| External runtime deps | None (statically linked binary) |
| Source size | ~701 KB across 84 files; library is 15 modules in `src/ShellCheck/` |
| Homepage | <https://www.shellcheck.net> |

**What it is:** a linter that catches shell-scripting bugs a shell would only reveal at runtime — quoting mistakes, broken conditionals, portability hazards, and dangerous constructs. It gives **shell-aware** advice, tailoring warnings to `sh`, `bash`, `dash`, `ksh`, or `busybox`.

**What it is not:** a formatter (it deliberately ignores indentation and style — pair it with `shfmt`), and not a `zsh`/`fish` checker.

**Operational profile:** ShellCheck is a stateless, single-shot CLI. There is no server, no daemon, no database, no network access at runtime. "Operating" it means installing the binary, pinning its version, providing configuration (`.shellcheckrc` / `SHELLCHECK_OPTS`), and gating builds on its exit code.

---

## 2. Architecture

### 2.1 High-level pipeline

ShellCheck processes each file through a strict pipeline. The CLI front-end (`shellcheck.hs`) handles options and I/O; the library (`src/ShellCheck/`) does the analysis.

```
                          shellcheck.hs (CLI front-end)
                                    |
   getArgs + SHELLCHECK_OPTS ─► parseArguments ─► Options
                                    |
                          per file: siReadFile
                                    |
                                    v
            ShellCheck.Checker.checkScript (orchestrator)
                                    |
              ┌─────────────────────┼──────────────────────┐
              v                     v                       v
   ShellCheck.Parser        ShellCheck.Analyzer      severity / include /
   parseScript              analyzeScript            exclude / directive
   (parsec → AST,           (run all checkers        filtering
    parse comments,          over the AST)           (shouldInclude)
    token positions)               |
              |                     |
              └─────────► comments + AST ◄──────────┘
                                    |
                                    v
                  Formatter (tty | gcc | checkstyle |
                  diff | json | json1 | quiet)
                                    |
                                    v
                       stdout  +  exit code (0–4)
```

The orchestration is visible in `ShellCheck.Checker.checkScript`: it calls `parseScript` to obtain the AST and parse-level comments, builds an `AnalysisSpec`, calls `analyzeScript`, then merges parse comments with analysis comments, de-duplicates (`nub`), sorts by position/severity/code, and filters.

### 2.2 The two comment sources

ShellCheck issues come from **two** stages, and both feed the same output:

1. **Parse-level comments** (`prComments`) — produced by `ShellCheck.Parser` while building the AST. These are syntax errors (the `SC10xx` family) detected during parsing.
2. **Analysis comments** (`arComments`) — produced by `ShellCheck.Analyzer` by walking the completed AST. These are the semantic and stylistic checks (the `SC2xxx`/`SC3xxx` families).

Parse comments use raw token IDs; `tokenToPosition` translates them into file/line/column positions using the parser's `prTokenPositions` map before they're merged with analysis comments.

### 2.3 The analyzer and its five checkers

`ShellCheck.Analyzer.analyzeScript` runs **five** independent checker modules over the AST and concatenates their findings (`mconcat`), then applies `filterByAnnotation` to honor in-script `disable`/`enable` directives:

| Checker module | Responsibility |
|----------------|----------------|
| `ShellCheck.Analytics` | The main body of checks — quoting, conditionals, common mistakes, robustness |
| `ShellCheck.Checks.Commands` | Per-command checks keyed on the command name (e.g. misuse of `find`, `grep`, `printf`, `which`) |
| `ShellCheck.Checks.ControlFlow` | Control-flow / dataflow-graph based checks (unreachable code, masked returns) |
| `ShellCheck.Checks.Custom` | A deliberately empty hook for organizations to add their own checks |
| `ShellCheck.Checks.ShellSupport` | Portability — flags constructs unsupported by the target shell dialect |

Each checker is a function of `AnalysisSpec → Parameters → [TokenComment]`. They are combined as monoids, which is how ShellCheck stays modular: adding a checker means adding one entry to the `checkers` list in `Analyzer.hs`.

### 2.4 Optional checks

Three of the five checkers (`Analytics`, `Checks.Commands`, `Checks.ControlFlow`) also expose an `optionalChecks` list of opt-in checks that are off by default. `Analyzer.optionalChecks` concatenates them; `shellcheck.hs`'s `--list-optional` prints them via `printOptional`. Each is a `CheckDescription` with a `cdName`, `cdDescription`, and positive/negative examples. There are 11 in this version (see [§7.2](#72-optional-checks)).

### 2.5 Dataflow (extended) analysis

`ShellCheck.CFG` builds a control-flow graph (using the `fgl` graph library), and `ShellCheck.CFGAnalysis` performs dataflow analysis over it. This is what catches issues like code that can never execute after an `exit`. It is **on by default** but can be disabled per-run (`--extended-analysis=false`), per-rc-file, or per-file (`# shellcheck extended-analysis=false`), because it is the most CPU- and memory-intensive part of the pipeline on very large scripts.

### 2.6 Shell-dialect resolution

`ShellCheck.Checker.shellFromFilename` and the option/directive/shebang logic together resolve the dialect in this priority order:

1. `--shell` / `-s` flag (`csShellTypeOverride`)
2. `# shellcheck shell=...` directive
3. Shebang line
4. Filename extension: `.ksh`→Ksh, `.bash`/`.bats`/`.envrc`→Bash, `.dash`→Dash
5. Fallback to Bash, emitting `SC2148` if there was no shebang

`.sh` is intentionally **not** in the extension map — it's too generic to imply a dialect — so a `.sh` file with no shebang triggers `SC2148`.

### 2.7 Source resolution and the `SystemInterface`

All file access is abstracted behind a `SystemInterface` record (`ShellCheck.Interface`), constructed in `shellcheck.hs`'s `ioInterface`. This is the seam that makes ShellCheck testable and that powers <https://www.shellcheck.net> (which supplies a non-filesystem interface).

It carries three operations:

- `siReadFile` — read a script (with caching via an `IORef`-backed map; stdin is `-`)
- `siFindSource` — resolve a `source`/`.` target, honoring `--source-path`, `SCRIPTDIR`, and the external-sources policy
- `siGetConfig` — locate and read the applicable `.shellcheckrc`

The `allowable` predicate enforces the external-sources security policy: by default only files named as input (plus `/dev/null`) are followed; `-x`/`--external-sources` or `external-sources=true` widens this.

---

## 3. Repository Structure

```
shellcheck/
├── shellcheck.hs            CLI entry point (main-is); option parsing, I/O, formatter dispatch
├── shellcheck-dev.hs        Dev-only helper (cabal -fdev-mode); `ast` command to dump a parse tree
├── ShellCheck.cabal         Package definition: deps, modules, executables, test suite
├── stack.yaml               Stack build config (resolver lts-18.15)
├── shellcheck.1.md          The man page (pandoc markdown) — authoritative CLI reference
├── README.md                Project overview, install matrix, "gallery of bad code"
├── CHANGELOG.md             Release history
├── Dockerfile.multi-arch    Multi-architecture container build
├── manpage / nextnumber /   Maintainer helper scripts (build man page, allocate next SC code,
│   quickrun / quicktest /   fast local run/test, strip tests, set git version)
│   setgitversion / striptests
├── src/ShellCheck/          The library (all analysis logic)
│   ├── AST.hs               Abstract syntax tree types for shell scripts
│   ├── ASTLib.hs            Helpers for traversing/querying the AST
│   ├── Parser.hs            parsec-based shell parser → AST (+ SC10xx parse errors)
│   ├── Analyzer.hs          Runs the 5 checkers; exposes optionalChecks
│   ├── Analytics.hs         The largest checker — most SC2xxx checks
│   ├── AnalyzerLib.hs       Shared analysis utilities, Parameters, token plumbing
│   ├── Checker.hs           Top-level checkScript orchestration + filtering
│   ├── CFG.hs               Control-flow graph construction (fgl)
│   ├── CFGAnalysis.hs       Dataflow analysis over the CFG
│   ├── Checks/
│   │   ├── Commands.hs      Per-command-name checks
│   │   ├── ControlFlow.hs   CFG/dataflow-based checks
│   │   ├── Custom.hs        Empty extension hook for custom checks
│   │   └── ShellSupport.hs  Shell-dialect portability checks
│   ├── Formatter/
│   │   ├── Format.hs        Formatter interface (header/onResult/onFailure/footer)
│   │   ├── TTY.hs           Default human-readable output
│   │   ├── GCC.hs           gcc-compatible one-line-per-issue output
│   │   ├── CheckStyle.hs    Checkstyle XML
│   │   ├── Diff.hs          Unified-diff auto-fixes
│   │   ├── JSON.hs          Legacy JSON (tab stop 8)
│   │   ├── JSON1.hs         Compact JSON (tab stop 1)
│   │   └── Quiet.hs         No output; exit code only
│   ├── Interface.hs         Core data types: CheckSpec, CheckResult, Severity, Shell,
│   │                        SystemInterface, Comment, PositionedComment, Fix
│   ├── Fixer.hs             Applies/merges fixes (powers the diff format)
│   ├── Data.hs              Static data: version string, constants
│   ├── Regex.hs             regex-tdfa wrappers
│   └── Prelude.hs           Small custom prelude
├── builders/                Per-target Docker cross-build definitions
│   ├── linux.{x86_64,aarch64,armv6hf,riscv64}/
│   ├── darwin.{x86_64,aarch64}/
│   └── windows.x86_64/
├── snap/snapcraft.yaml      Snap package definition
└── test/                    Build/release/distro integration test scripts + test runner
```

> [!NOTE]
> The unit tests are **inline** with the source as QuickCheck properties (functions prefixed `prop_`), not in a separate test tree. `test/shellcheck.hs` is the runner that invokes each module's `runTests`. See [§9](#9-testing).

---

## 4. System Requirements

### 4.1 Runtime

The released `shellcheck` binary is statically linked and has **no runtime dependencies** — drop it on the `PATH` and run it. The Linux release binaries in particular are fully static, so they run on any modern Linux without library concerns.

| Resource | Requirement |
|----------|-------------|
| OS | Linux, macOS, Windows, *BSD (anywhere a binary is published or it compiles) |
| Disk | A few MB for the binary |
| Memory | Trivial for normal scripts; can grow with extended analysis on very large (2000+ line) files |
| Network | None |

### 4.2 Build-time

Compiling from source requires a Haskell toolchain and **~2 GB of RAM**.

| Tool | Detail |
|------|--------|
| GHC | Tested with 8.0, 8.10, 9.0.1, 9.4.7, 9.6.6 |
| Cabal | `cabal-install` (primary build path) |
| Stack | Optional alternative; `stack.yaml` pins resolver `lts-18.15` |
| pandoc | Only needed to regenerate the man page from `shellcheck.1.md` |

---

## 5. Building From Source

### 5.1 With Cabal (primary)

```bash
git clone https://github.com/koalaman/shellcheck.git
cd shellcheck
cabal update
cabal install
```

This compiles ShellCheck and installs the binary to `~/.cabal/bin`. Add it to your `PATH`:

```bash
export PATH="$HOME/.cabal/bin:$PATH"
which shellcheck    # → ~/.cabal/bin/shellcheck
```

### 5.2 With Stack

```bash
stack update
stack install ShellCheck    # installs to ~/.local/bin
```

### 5.3 Build the man page

The distro packages ship a man page; if you build from source, regenerate it with pandoc:

```bash
pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1
sudo mv shellcheck.1 /usr/share/man/man1
```

### 5.4 Cabal flags and executables

`ShellCheck.cabal` defines:

| Component | Notes |
|-----------|-------|
| `library` | The 15 `src/ShellCheck/` modules (exposed) + `Paths_ShellCheck` |
| `executable shellcheck` | `main-is: shellcheck.hs`, links the library |
| `executable shellcheck-dev` | Buildable only with `-fdev-mode`; exposes `ShellCheck.Debug` and an `ast` dump command for AI/maintainer use |
| `test-suite test-shellcheck` | `exitcode-stdio-1.0`, `main-is: test/shellcheck.hs` |
| `flag dev-mode` | Manual, default `False`; gates the dev executable and `Debug` module |

```bash
# Build and run the dev AST dumper
cabal run -fdev-mode shellcheck-dev -- ast 'echo $foo'
```

### 5.5 Cross-compilation and packaging

The `builders/` tree contains per-target Docker build definitions (`Dockerfile`, `build`, `tag`) for Linux `x86_64`/`aarch64`/`armv6hf`/`riscv64`, macOS `x86_64`/`aarch64`, and Windows `x86_64`. `Dockerfile.multi-arch` and the `builders/build_builder`/`run_builder` scripts orchestrate the release matrix. `snap/snapcraft.yaml` builds the Snap package.

> [!TIP]
> To reproduce the official static Linux binary, use the `builders/linux.x86_64` Docker definition rather than a host cabal build — it pins the toolchain and produces the statically-linked artifact distributed on GitHub Releases.

---

## 6. Dependencies

ShellCheck's library depends on a focused set of Haskell packages (from `ShellCheck.cabal`). Version bounds are based on GHC 7.10.3 (lower) through 9.12.1 (upper).

| Package | Role in ShellCheck |
|---------|--------------------|
| `parsec` | The parser combinator library that powers `ShellCheck.Parser` |
| `regex-tdfa` | Regular expression engine behind `ShellCheck.Regex` |
| `fgl` | Functional Graph Library — backs the control-flow graph (`CFG`/`CFGAnalysis`) |
| `aeson` | JSON serialization for the `json`/`json1` formatters |
| `Diff` | Diff generation for the `diff` (auto-fix) formatter |
| `containers` | `Map`/`Set` used throughout (token maps, caches) |
| `array` | Efficient arrays in analysis |
| `mtl` / `transformers` | Monad transformer stacks (`ExceptT`, etc. in the CLI and analysis) |
| `deepseq` | Forcing evaluation (`seq` on file contents) |
| `filepath` / `directory` | Path manipulation and config-file discovery (`getXdgDirectory` needs `directory ≥ 1.2.3`) |
| `bytestring` | Binary-safe file I/O |
| `base` | Standard library |
| `QuickCheck` | Property-based testing (the inline `prop_` checks) |
| `semigroups` | Only for `ghc < 8.0` compatibility |

> [!IMPORTANT]
> There are **no** runtime services or native libraries here — every dependency is a pure Haskell package compiled into the binary. The consumer-facing supply-chain surface of the distributed binary is effectively zero, which is a strong security posture worth noting in any vendor review.

---

## 7. Configuration Mastery

ShellCheck has four configuration surfaces. Understanding their precedence is the key operational skill.

### 7.1 Precedence model

From lowest to highest priority:

1. **`SHELLCHECK_OPTS`** environment variable — flags split on spaces and **prepended** to the argument list on every invocation (`getEnvArgs` in `shellcheck.hs`).
2. **`.shellcheckrc`** — discovered per script (see [§7.3](#73-shellcheckrc-discovery)).
3. **In-file `# shellcheck` directives** — file-wide (before the first command) or scoped to the following command/block.
4. **Command-line flags** — the final word.

Two settings have explicit override semantics encoded in the code:

- `--extended-analysis` (flag) **overrides** both directives and rc files.
- `--external-sources`: a flag or rc setting can enable it; a **file directive can only disable** it, never enable it (attempting to enable via directive yields `SC1144`). This is a security guard rail — a checked script cannot grant itself permission to read arbitrary files.

### 7.2 Optional checks

Enable with `--enable=NAME,…` (or `all`), or `enable=NAME` in `.shellcheckrc`. List them with `--list-optional`.

| Name | Description | Bad → Good |
|------|-------------|------------|
| `quote-safe-variables` | Suggest quoting variables without metacharacters | `echo $var` → `echo "$var"` |
| `avoid-nullary-conditions` | Suggest explicitly using `-n` in `[ $var ]` | `[ "$var" ]` → `[ -n "$var" ]` |
| `avoid-negated-conditions` | Suggest removing unnecessary comparison negations | `[ ! "$var" -eq 1 ]` → `[ "$var" -ne 1 ]` |
| `add-default-case` | Suggest adding a default case in `case` statements | add `*) … ;;` |
| `require-variable-braces` | Suggest braces around all variable references | `echo $var` → `echo ${var}` |
| `check-unassigned-uppercase` | Warn when uppercase variables are unassigned | `echo $VAR` → `VAR=hello; echo $VAR` |
| `require-double-brackets` | Require `[[ ]]` and warn about `[ ]` in Bash/Ksh | `[ -e /etc/issue ]` → `[[ -e /etc/issue ]]` |
| `check-set-e-suppressed` | Notify when `set -e` is suppressed during function invocation | use `func; echo ok` patterns carefully |
| `check-extra-masked-returns` | Flag additional cases where exit codes are masked | split assignment from `rm` |
| `useless-use-of-cat` | Detect Useless Use Of Cat (UUOC) | `cat foo \| grep bar` → `grep bar foo` |
| `deprecate-which` | Suggest `command -v` instead of `which` | `which javac` → `command -v javac` |

### 7.3 `.shellcheckrc` discovery

The discovery algorithm (in `shellcheck.hs`'s `getConfig`/`getConfigPaths`):

1. Starting from the script's directory, walk **up** through every parent directory looking for `.shellcheckrc` or `shellcheckrc`. The first match wins.
2. If none found, try the user home config: `~/.shellcheckrc`, then `$XDG_CONFIG_HOME/shellcheckrc` (usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on Windows.
3. `--rcfile PATH` bypasses discovery entirely; `--norc` disables it.

A broken rc file does not abort the run — ShellCheck emits `SC1134` and continues.

### 7.4 Directive and rc key reference

| Key | Scope | Effect |
|-----|-------|--------|
| `disable` | command/block/file | Disable codes (comma list, ranges `SCxxxx-SCyyyy`, or `all`) |
| `enable` | file-wide only | Enable an optional check by name |
| `extended-analysis` | file/rc | `true`/`false` dataflow toggle |
| `external-sources` | rc (enable), directive (disable only) | Allow opening arbitrary `source`d files |
| `source` | command | Override the path a `source`/`.` reads (or `/dev/null` to skip) |
| `source-path` | command/file/rc | Add a search directory (`SCRIPTDIR` = script's dir); accumulates |
| `shell` | file-wide | Override detected shell dialect |

> [!NOTE]
> Values in `.shellcheckrc` may be quoted with `'` or `"` (since 0.9.0) to allow spaces, e.g. `source-path="My Documents/scripts"`.

---

## 8. Operations & Deployment

ShellCheck is not a service, so "operations" means standardizing how a team runs it.

### 8.1 Recommended fleet setup

- [ ] Install a **pinned** version everywhere (don't track `stable`/`latest` in CI).
- [ ] Commit a project `.shellcheckrc` so every developer and the CI server share one config.
- [ ] Set a severity gate in CI appropriate to the codebase's maturity (`--severity=error` for legacy, `style` for greenfield).
- [ ] Add a pre-commit hook so issues are caught before review.
- [ ] Pair ShellCheck with `shfmt` for formatting, since ShellCheck intentionally ignores it.

### 8.2 CI gate pattern

```bash
#!/usr/bin/env bash
set -euo pipefail
# Fail the pipeline on any error- or warning-level issue across all scripts
find . -type f -name '*.sh' -print0 \
  | xargs -0 shellcheck --severity=warning --format=gcc
```

ShellCheck's exit codes drive the gate (see the User Guide §11). In short: `0` = clean, `1` = issues found, `2`/`3`/`4` = operational/usage errors (treat these as "ShellCheck itself failed," distinct from "the script has lint").

> [!TIP]
> Distinguish exit `1` (lint found — fix the script) from exit `2`/`3`/`4` (file missing / bad flag / bad formatter — fix the **invocation**). In a wrapper script, branch on `$?` so a typo in a flag doesn't masquerade as a "passing" or "failing" lint result.

### 8.3 Containerized invocation

```bash
docker run --rm -v "$PWD:/mnt" koalaman/shellcheck:v0.11.0 --severity=warning myscript
```

Remember the container can only see mounted paths — mount the project root and put `.shellcheckrc` inside it, since `~/.shellcheckrc` on the host is invisible to the container.

### 8.4 Upgrades

New releases add new checks. Schedule upgrades deliberately:

1. Bump the pinned version in one branch.
2. Run a full scan and review newly-surfaced warnings.
3. Fix or `disable` them in `.shellcheckrc`, then merge.

This avoids a "green yesterday, red today" surprise across the whole team.

---

## 9. Testing

ShellCheck's tests are **QuickCheck properties co-located with the code they test**. Each module defines `prop_*` functions and a `runTests` (built with Template Haskell's `$quickCheckAll`); `test/shellcheck.hs` aggregates and runs them.

```bash
cabal test
# or, for fast local iteration, the maintainer scripts:
./quicktest
```

The property style means a check and its tests live together — for example `ShellCheck.Checker` carries dozens of `prop_*` properties asserting exact code lists for given scripts (`check "echo $1" == [2086]`). When adding or modifying a check, add `prop_` properties in the same module asserting which codes a snippet should and shouldn't produce.

`test/` also holds **integration** scripts: `buildtest`, `check_release`, `distrotest`, and `stacktest` validate packaging and distro builds rather than analysis logic.

---

## 10. Extending ShellCheck

### 10.1 Adding a check

1. Choose the right module: general checks go in `Analytics.hs`; command-name-specific checks in `Checks/Commands.hs`; control-flow-dependent checks in `Checks/ControlFlow.hs`; portability checks in `Checks/ShellSupport.hs`. Organization-private checks belong in the otherwise-empty `Checks/Custom.hs`.
2. Allocate the next `SC` code with the `nextnumber` maintainer script.
3. Implement the check against the AST (`ShellCheck.AST` / `ShellCheck.ASTLib`).
4. Add `prop_*` QuickCheck properties in the same module.
5. If it should be opt-in, add a `CheckDescription` to that module's `optionalChecks`.

### 10.2 Adding an output format

Implement the `Formatter` interface from `ShellCheck.Formatter.Format` (the `header`/`onResult`/`onFailure`/`footer` callbacks), add the module under `Formatter/`, register it in the `formats` map in `shellcheck.hs`, and list it in the cabal `exposed-modules`.

### 10.3 The `SystemInterface` seam

Because all I/O goes through `SystemInterface`, you can embed ShellCheck's library in another Haskell program and supply scripts from memory, a database, or a network request — this is exactly how shellcheck.net works. `mockedSystemInterface` (used throughout the tests) shows the pattern.

### 10.4 Contributing

Contributions go through GitHub pull requests and must be licensed under GPLv3 (the contributor retains copyright). See the [DevGuide](https://github.com/koalaman/shellcheck/wiki/DevGuide) on the wiki. Long-form descriptions of every check live at `https://github.com/koalaman/shellcheck/wiki/SC<NNNN>`.

---

## 11. Security Analysis

| Concern | Assessment |
|---------|------------|
| **Untrusted input** | ShellCheck's lineage is a public web service checking untrusted scripts, so it's hardened against malicious **input scripts** — it parses, never executes them. |
| **External sources** | The default `external-sources=false` is a deliberate guard: ShellCheck won't open files a script `source`s unless explicitly allowed. A checked script **cannot enable** external sourcing via its own directive (`SC1144`) — only the operator can, via flag or rc. |
| **No code execution** | ShellCheck performs pure static analysis. It does not run, source, or `eval` the scripts it checks. |
| **No network** | The binary makes no network calls. (The wiki links in output are just text.) |
| **Supply chain** | Statically-linked binary with only compiled-in Haskell deps; zero runtime dependency surface for consumers. |
| **File access** | Limited to files named as input plus resolved `source` targets within policy. Path resolution is `canonicalizePath`-based with safe fallbacks. |
| **Encoding safety** | Files are leniently decoded as UTF-8 with ISO-8859-1 fallback (`decodeString`), avoiding crashes on malformed bytes. |

> [!CAUTION]
> When you enable `--external-sources` in CI, ShellCheck will read any file a script references via `source`. On a build agent that checks untrusted third-party scripts, prefer the default (input files only) so a hostile script can't coax ShellCheck into reading sensitive files via crafted `source` paths.

---

## 12. Code Quality & Conventions

- **Modular monoid checkers.** Checks are combined with `mconcat`/`<>`; adding one is a one-line registration. The analyzer is a pipeline of independent passes.
- **Abstraction at the I/O boundary.** `SystemInterface` cleanly separates pure analysis from effectful file access, enabling both testing and the web service.
- **Tests beside code.** QuickCheck `prop_*` properties live in the module they exercise, asserting exact diagnostic code lists — a precise regression net.
- **Haskell2010, broad GHC support.** The codebase compiles across GHC 8.0–9.6+ with conditional compatibility shims (`semigroups` for `ghc < 8.0`).
- **Single static binary.** Distribution is dead simple — no interpreter, no shared libraries.

Observed rough edges (from source comments) worth a maintainer's attention:

- `Checker.hs` imports `Debug.Trace` with a `-- DO NOT SUBMIT` marker and uses `traceShowId` in one property (`prop_fileCanSuppressDfa`) — leftover debugging that should be removed.
- A `-- TODO: Clean up the cruft this is layered on` sits atop `analyzeScript`, acknowledging accumulated complexity in the analyzer plumbing.
- A `FIXME` in `Checker.hs` notes that recursively-sourced files should emit `SC1093` ("recursively sourced") but currently don't (`prop_noInfiniteSourcing`).

---

## 13. Potential Improvements

Drawn from the codebase and the project's open issues:

- **Remove debug residue.** Drop the `Debug.Trace` import and `traceShowId`/`DO NOT SUBMIT` marker in `Checker.hs`.
- **Implement recursive-source detection (`SC1093`).** Close the documented `FIXME` so infinite/recursive `source` chains are reported instead of silently ignored.
- **Reduce false positives raised by the community**, e.g. `SC2030`/`SC2031` on `$_` (#3478), `SC2218` false positives (#3421), `SC2183` with `printf %()T` (#3446), and `SC1075`'s severity (#3472).
- **Broaden `set -e` awareness** (#3444) — warn when scripts can fail due to `set -e` interactions.
- **Performance for large generated scripts** — extended analysis is already toggleable; further profiling of `CFGAnalysis` would let it stay on for bigger inputs.

> [!NOTE]
> Because each check carries its own `prop_*` properties, every improvement above should land with property assertions pinning the exact codes produced for representative snippets — that is the project's contribution contract.

---

## 14. Quick Reference

### 14.1 Module map

| Layer | Modules |
|-------|---------|
| CLI | `shellcheck.hs`, `shellcheck-dev.hs` |
| Orchestration | `Checker`, `Analyzer` |
| Parsing | `Parser`, `AST`, `ASTLib` |
| Analysis | `Analytics`, `AnalyzerLib`, `Checks.{Commands,ControlFlow,Custom,ShellSupport}`, `CFG`, `CFGAnalysis` |
| Output | `Formatter.{Format,TTY,GCC,CheckStyle,Diff,JSON,JSON1,Quiet}`, `Fixer` |
| Support | `Interface`, `Data`, `Regex`, `Prelude` |

### 14.2 Exit codes

| Code | Meaning |
|------|---------|
| 0 | No issues |
| 1 | Issues found |
| 2 | File(s) could not be processed |
| 3 | Bad invocation syntax (e.g. unknown flag) |
| 4 | Bad options (e.g. unknown formatter) |

### 14.3 Key commands

```bash
cabal install                                   # build & install (→ ~/.cabal/bin)
stack install ShellCheck                         # build & install (→ ~/.local/bin)
cabal test                                       # run the QuickCheck suite
cabal run -fdev-mode shellcheck-dev -- ast 'cmd' # dump a parse tree (dev)
shellcheck --list-optional                       # list opt-in checks
shellcheck --version                             # version info
```

### 14.4 Configuration surfaces (low → high precedence)

`SHELLCHECK_OPTS` → `.shellcheckrc` → in-file directives → CLI flags
(`--extended-analysis` and `--external-sources` flags override directives; a file directive can only *disable* external sources.)

---

_Generated June 2026 from koalaman/shellcheck at 764802b. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
