shuck-cli 0.0.17

A fast shell script linter
Documentation
use shuck_linter::Rule;
use shuck_linter::RuleSet;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OptionalCheckBehavior {
    None,
    ReportEnvironmentStyleNames,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OptionalCheck {
    pub name: &'static str,
    pub description: &'static str,
    pub example: &'static str,
    pub guidance: &'static str,
    pub enable_rules: &'static [Rule],
    pub default_disabled_rules: &'static [Rule],
    pub behavior: OptionalCheckBehavior,
    pub supported: bool,
}

impl OptionalCheck {
    pub fn enabled_rule_set(self) -> RuleSet {
        self.enable_rules.iter().copied().collect()
    }
}

pub const OPTIONAL_CHECKS: &[OptionalCheck] = &[
    OptionalCheck {
        name: "add-default-case",
        description: "Reports case statements that omit a fallback branch.",
        example: "case $? in 0) echo ok ;; esac",
        guidance: "Add a catch-all branch when the script should handle unexpected values.",
        enable_rules: &[],
        default_disabled_rules: &[],
        behavior: OptionalCheckBehavior::None,
        supported: false,
    },
    OptionalCheck {
        name: "avoid-negated-conditions",
        description: "Prefers direct comparison operators over leading negation in tests.",
        example: "[ ! \"$value\" -eq 1 ]",
        guidance: "Rewrite the operator so the intent stays positive without a leading !.",
        enable_rules: &[],
        default_disabled_rules: &[],
        behavior: OptionalCheckBehavior::None,
        supported: false,
    },
    OptionalCheck {
        name: "avoid-nullary-conditions",
        description: "Flags bare [ \"$var\" ] checks that rely on implicit non-empty semantics.",
        example: "[ \"$var\" ]",
        guidance: "Use an explicit string or numeric operator for the condition you mean.",
        enable_rules: &[],
        default_disabled_rules: &[],
        behavior: OptionalCheckBehavior::None,
        supported: false,
    },
    OptionalCheck {
        name: "check-extra-masked-returns",
        description: "Looks for command substitutions whose failures are easy to miss.",
        example: "rm -r \"$(helper)/home\"",
        guidance: "Split the substitution into a checked step before using the result.",
        enable_rules: &[],
        default_disabled_rules: &[],
        behavior: OptionalCheckBehavior::None,
        supported: false,
    },
    OptionalCheck {
        name: "check-set-e-suppressed",
        description: "Notes call sites where set -e is neutralized by the surrounding construct.",
        example: "set -e; build && echo ok",
        guidance: "Run the function as its own command when failures should still abort the script.",
        enable_rules: &[],
        default_disabled_rules: &[],
        behavior: OptionalCheckBehavior::None,
        supported: false,
    },
    OptionalCheck {
        name: "check-unassigned-uppercase",
        description: "Adds uppercase-variable coverage to unset-variable style checks.",
        example: "echo $VAR",
        guidance: "Initialize the uppercase name before reading it when it is not inherited from the environment.",
        enable_rules: &[],
        default_disabled_rules: &[],
        behavior: OptionalCheckBehavior::ReportEnvironmentStyleNames,
        supported: true,
    },
    OptionalCheck {
        name: "deprecate-which",
        description: "Discourages the non-portable which utility in favor of shell builtins.",
        example: "which javac",
        guidance: "Prefer command -v when you only need command lookup.",
        enable_rules: &[],
        default_disabled_rules: &[],
        behavior: OptionalCheckBehavior::None,
        supported: false,
    },
    OptionalCheck {
        name: "quote-safe-variables",
        description: "Requests quotes even for scalar variables that look safe today.",
        example: "name=hello; echo $name",
        guidance: "Wrap the expansion in double quotes when consistency matters more than brevity.",
        enable_rules: &[],
        default_disabled_rules: &[],
        behavior: OptionalCheckBehavior::None,
        supported: false,
    },
    OptionalCheck {
        name: "require-double-brackets",
        description: "Requires [[ ... ]] in shells where that test form is available.",
        example: "[ -e /etc/issue ]",
        guidance: "Use the double-bracket form only when the selected shell actually supports it.",
        enable_rules: &[],
        default_disabled_rules: &[],
        behavior: OptionalCheckBehavior::None,
        supported: false,
    },
    OptionalCheck {
        name: "require-variable-braces",
        description: "Prefers ${name} over bare $name references.",
        example: "name=hello; echo $name",
        guidance: "Add braces when you want every variable reference to follow the same house style.",
        enable_rules: &[],
        default_disabled_rules: &[],
        behavior: OptionalCheckBehavior::None,
        supported: false,
    },
    OptionalCheck {
        name: "useless-use-of-cat",
        description: "Looks for cat pipelines that can be replaced by a direct file operand.",
        example: "cat foo | grep bar",
        guidance: "Pass the file to the downstream command directly when it reads files itself.",
        enable_rules: &[],
        default_disabled_rules: &[],
        behavior: OptionalCheckBehavior::None,
        supported: false,
    },
];

pub fn compat_default_disabled_rules() -> RuleSet {
    OPTIONAL_CHECKS
        .iter()
        .filter(|check| check.supported)
        .flat_map(|check| check.default_disabled_rules.iter().copied())
        .collect()
}

pub fn supported_optional_checks() -> impl Iterator<Item = &'static OptionalCheck> {
    OPTIONAL_CHECKS.iter().filter(|check| check.supported)
}

pub fn find_optional_check(name: &str) -> Option<&'static OptionalCheck> {
    OPTIONAL_CHECKS.iter().find(|check| check.name == name)
}