Skip to main content

amont_runtime/hooks/
shellcheck.rs

1//! pre-commit-shellcheck โ€” the shell linter, on the shell this repo stages.
2
3use super::common::run as run_tool;
4use super::common::{fail, hl, ok, repo_root, staged_files, warn, which};
5use crate::check::Outcome;
6
7/// The extensions this check consumes. Exported so `registry.rs` declares the
8/// scope from the same constant โ€” see `lint_json_yaml::EXTS` for the drift this
9/// prevents.
10///
11/// Extensions only, deliberately: a `#!/bin/sh` file with no extension is not
12/// matched. Reading shebangs is its own change with its own risks, and it is
13/// already written down as one โ€” see `docs/index-fidelity-and-run-modes.md` ยง5.
14pub const EXTS: &[&str] = &[".sh", ".bash"];
15
16pub fn run(settings: &crate::config::Settings, _args: &[std::ffi::OsString]) -> Outcome {
17    let files = staged_files(EXTS);
18    if files.is_empty() {
19        return Outcome::Passed;
20    }
21    let root = repo_root();
22    // NO opt-in file, unlike `yamllint`. That check gates on a config because
23    // its stock rules are too noisy to enforce generically; shellcheck's
24    // defaults are the reason people run it at all. Gating on a `.shellcheckrc`
25    // would leave this inert in every repository that has not written one,
26    // which is a check that does nothing dressed as a check that is careful.
27    //
28    // The RESOLVED path, not the bare name: `Command::new` does no PATHEXT
29    // resolution, so a bare `shellcheck` cannot execute `shellcheck.exe` on
30    // Windows and a `Severity::Block` check would report an installed tool as
31    // broken. Same incident `common::program` documents for `npm.cmd`.
32    let Some(bin) = which("shellcheck") else {
33        warn(&format!(
34            "Shell files are staged but shellcheck is not installed. Install {}",
35            hl("shellcheck")
36        ));
37        return Outcome::Unavailable;
38    };
39    // `--` before the file list: a staged file named e.g. `-x.sh` would
40    // otherwise be read as a flag by shellcheck's own parser.
41    let argv = vec![bin];
42    let mut with_files = vec!["--".to_string()];
43    with_files.extend(files);
44    if !run_tool(settings, &root, &argv, &with_files) {
45        fail("shellcheck found issues. Please fix");
46        return Outcome::Failed;
47    }
48    ok(settings, "shellcheck passed");
49    Outcome::Passed
50}