use clap::{Parser, Subcommand};
const HELP_TEMPLATE: &str = "\
{name} {version}\n\
{about}\n\
\n\
USAGE:\n\
{usage}\n\
\n\
{all-args}{after-help}\
";
const AFTER_HELP: &str = "\
ENV:\n\
FORBIDDEN_STRINGS_RULES Default rules path; --rules wins if both are set.\n\
If unset, falls back to ./forbidden-strings.local.txt\n\
FORBIDDEN_STRINGS_CACHE_DIR\n\
Absolute per-user cache-root override.\n\
\n\
RUNTIME CACHE:\n\
Runtime rules use read-write caching by default. Cache identity is exact\n\
rules content plus scanner version and platform. Missing or rejected cache\n\
data emits redacted JSON, compiles authoritative text, and attempts atomic\n\
repair. A failed repair never invalidates the in-memory scan.\n\
\n\
BUILT-IN BASELINE:\n\
--builtin-rules appends the embedded betterleaks-ported baseline after\n\
the resolved rules file (user rule numbering is unchanged). When the\n\
implicit default rules file is absent, the baseline alone is used; an\n\
explicitly named missing file (--rules or env) still errors. Without\n\
the flag, the baseline is never read.\n\
\n\
EXIT CODES:\n\
0 No violations.\n\
1 One or more violations (printed to stderr, redacted).\n\
2 Usage error or rule-file error.\n\
\n\
EXAMPLES:\n\
# Scan a few files\n\
forbidden-strings --rules ./rules.txt src/main.ts README.md\n\
\n\
# Scan the whole working tree\n\
FORBIDDEN_STRINGS_RULES=./rules.txt forbidden-strings --all\n\
\n\
# Eagerly compile runtime rules without scanning\n\
forbidden-strings compile-rules --rules ./rules.txt\n\
\n\
RULE FORMAT (autodetected per file, never mixed):\n\
Tail format -> '==> name <==' headers open one-rule sections;\n\
the name ([a-z0-9], then [a-z0-9.-]) is the\n\
rule's identity in findings. One significant\n\
body line classifies as below; several are one\n\
verbatim always-verbose pattern.\n\
Legacy format -> one rule per line, unnamed:\n\
Bare line -> case-sensitive literal; under 8 bytes it is\n\
word-boundary gated at word-byte ends\n\
/PATTERN/FLAGS -> regex in the forbidden-regex dialect\n\
# ... -> comment\n\
Empty line -> skipped\n\
\n\
DIALECT:\n\
Supported: literals, classes [a-z] and \\d \\w \\s, '.', (?:a|b),\n\
bounded repetition a{3,6}, anchors ^ $ \\b, and set algebra A & B\n\
and ~(A). Flags 'm' and 'x' are accepted no-ops (multiline and\n\
verbose are always on); any other flag letter is a hard load error.\n\
Rejected at compile time (fail-closed): '*', '+', unbounded {n,},\n\
capturing '(', lookaround and inline flags, backreferences, and any\n\
pattern matching the empty string.\n\
\n\
OUTPUT:\n\
PATH:LINE rule=<token> (columnless; matched substring is NEVER\n\
printed). The token is the rule's section name (baseline rules use\n\
their betterleaks id); unnamed legacy rules use the 0-based index.\n\
\n\
See README.md for the full dialect, set-algebra examples, and CI integration.\n\
";
#[derive(Subcommand, Debug, PartialEq)]
pub enum CliCommand {
#[command(name = "compile-rules")]
CompileRules {
#[arg(
long = "rules",
value_name = "PATH",
allow_hyphen_values = true,
help = "Path to authoritative runtime rules file"
)]
rules_path: String,
},
}
impl CliCommand {
pub fn rules_path(&self) -> &str {
let Self::CompileRules { rules_path } = self;
return rules_path
}
}
#[derive(Parser, Debug, PartialEq)]
#[command(
name = "forbidden-strings",
version,
about = "Linear-time deny-list scanner for Git repos.",
help_template = HELP_TEMPLATE,
after_help = AFTER_HELP,
args_override_self = true,
)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<CliCommand>,
#[arg(
long = "rules",
value_name = "PATH",
allow_hyphen_values = true,
help = "Path to the rule file (tail-format sections or legacy lines)"
)]
pub rules_path: Option<String>,
#[arg(long = "all", help = "Scan every git-tracked file under cwd")]
pub all: bool,
#[arg(
long = "builtin-rules",
help = "Also scan with the embedded betterleaks-ported baseline rules"
)]
pub builtin_rules: bool,
#[arg(value_name = "FILE", num_args = 0.., help = "File path to scan")]
pub files: Vec<String>,
}