Ishellcheck Processor
Purpose
Lints shell scripts with ShellCheck’s analysis, in-process (no shellcheck program, no Haskell runtime to install). The Rust alternative to the shellcheck processor: a port of ShellCheck 0.11.0, so a script gets the same findings with the same codes, positions and messages, and passes or fails the way shellcheck would exit.
How It Works
Each file goes through what ShellCheck does to it: its parser (sh, bash, dash, ksh and busybox dialects, here-documents, arithmetic, [[ ]] conditions, the parse errors and their notes), the control flow graph and its dataflow analysis (which assignments reach which uses, through functions and subshells), and every check of ShellCheck’s Analytics, Commands and ShellSupport modules, optional checks included. The dialect comes from shell, a # shellcheck shell= directive, the shebang or the file extension, in ShellCheck’s order. # shellcheck directives (disable, enable, source, source-path, external-sources, extended-analysis) and .shellcheckrc files (looked up from the script’s directory upwards, then in ~/.shellcheckrc and the XDG config directory) apply as in ShellCheck. Findings are printed as shellcheck --format=gcc prints them, and any finding fails the product:
src/bad.sh:2:1: warning: foo appears unused. Verify use (or export if used externally). [SC2034]
src/bad.sh:3:6: note: Double quote to prevent globbing and word splitting. [SC2086]
Each file is its own product, checked as shellcheck FILE checks it: sourced files are read only with external_sources, and their findings are reported only with check_sourced.
[processor.checker.ishellcheck]
src_dirs = ["scripts"]
exclude = ["SC1091"]
severity = "warning"
Fidelity
Checked against shellcheck 0.11.0 over 4494 real scripts (1850 from the fleet, 2644 from a development machine’s /usr/bin and friends), the 2194 snippets of ShellCheck’s own unit tests, and 28000 mutated copies (broken quotes, brackets and keywords, swapped operators, deleted and duplicated lines): the same findings, line for line (about 360,000 compared), under default options, each shell override, external_sources and check_sourced. Neither side crashes on any of them.
It is also faster and smaller than shellcheck: 2781 real scripts (the fleet’s and a development machine’s) take 2.3 seconds against shellcheck’s 15.7 on 16 threads (18 against 137 seconds of CPU), and the largest script, a 3500-line dkms, takes 1.0 second and 400 MB against 6.4 seconds and 1.9 GB.
The engine’s unit tests (src/engines/shellcheck/tests.rs) keep it that way: they run shellcheck 0.11.0 itself (a missing or different version fails them) and the engine over the scripts in src/engines/shellcheck/testdata/ and deterministic mutants of them, under each option, and require the same output line for line. The processor’s tests check that each TOML field matches the shellcheck option of the same name.
Not supported: output formats other than gcc, auto-fixes (--format=diff), and the --wiki-link-count/--color presentation options.
Source Files
- Input:
.shand.bashfiles undersrc_dirs, or the files insrc_files - Output: none (checker)
The default src_dirs is empty, so a stanza with no src_dirs or src_files checks nothing: name the directories ([""] for the whole project) or files to check.
Configuration
| Key | Type | Default | Description |
|---|---|---|---|
shell | string | "" | The dialect, as --shell (sh, bash, dash, ksh, busybox); empty: from directives, the shebang or the extension |
external_sources | bool | false | Follow sourced files that are not inputs, as --external-sources |
source_path | string[] | [] | Where to look for sourced files, as --source-path (SCRIPTDIR is the script’s directory) |
severity | string | "style" | The minimum severity reported, as --severity: error, warning, info, style |
exclude | string[] | [] | Codes to exclude, as --exclude (SC2034 or 2034) |
include | string[] | [] | Codes to report exclusively, as --include; empty: all codes |
enable | string[] | [] | Optional checks to enable, as --enable (all for every one) |
rcfile | string | "" | A .shellcheckrc to use instead of searching for one, as --rcfile |
norc | bool | false | Ignore .shellcheckrc files, as --norc |
check_sourced | bool | false | Report findings in sourced files too, as --check-sourced |
extended_analysis | string | "" | Dataflow analysis, as --extended-analysis: true, false, or empty for the default (directives decide) |
dep_auto | string[] | [".shellcheckrc"] | Config files that are inputs when they exist |
Switching a repo from shellcheck: [processor.checker.shellcheck] → [processor.checker.ishellcheck]; a .shellcheckrc stays as it is, and options in args become the fields of the same name (args = ["-e", "SC1091", "-S", "warning"] becomes exclude = ["SC1091"], severity = "warning").
Batch support
Not used: checking in-process has no start-up cost for a batch to share, so files are checked in parallel as separate products.
Clean behavior
This processor is a Checker — rsconstruct clean outputs is a no-op for it (checkers produce no outputs). See Clean behavior and rsconstruct clean.