stern4rust/args.rs
1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use std::path::PathBuf;
6
7use clap::Parser;
8
9use crate::offence_threshold::OffenceThreshold;
10use crate::output_format::OutputFormat;
11
12#[derive(Debug, Parser)]
13#[command(name = "cargo-stern4rust")]
14#[command(bin_name = "cargo stern4rust")]
15#[command(version)]
16#[command(about = "Check Rust packages and fail the build when the rule is broken")]
17pub struct Args {
18 #[arg(long)]
19 pub manifest_path: Option<PathBuf>,
20
21 #[arg(long = "package")]
22 pub packages: Vec<String>,
23
24 /// File holding the header every .rs file must open with. It is data rather
25 /// than a built-in constant because it is never the same twice: MIT here,
26 /// Apache 2.0 in a sibling repository, and a different year again next year.
27 #[arg(long)]
28 pub header_file: Option<PathBuf>,
29
30 /// How to report. The table is for a person; `json` is the same run as a
31 /// document, for a gate script or an agent that would otherwise have to
32 /// guess where one column of the table ends and the next begins.
33 #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
34 pub format: OutputFormat,
35
36 /// How many offences the report prints. A first run against a large
37 /// codebase can find a thousand, and a thousand rows is a wall rather than
38 /// a report. The cap is on what is shown and never on what is counted:
39 /// the summary, the omitted count and the exit code all see every offence.
40 /// Use 0 for no limit.
41 #[arg(long, default_value_t = OffenceThreshold::DEFAULT)]
42 pub offence_threshold: usize,
43}
44
45impl Args {
46 pub fn parse_args() -> Self {
47 Self::parse_from(Self::without_cargo_subcommand(std::env::args()))
48 }
49
50 /// Cargo invokes `cargo stern4rust` as `cargo-stern4rust stern4rust ...`, so
51 /// the subcommand name arrives as an extra leading argument that clap would
52 /// otherwise reject. Running the binary directly does not repeat it, which
53 /// is why the strip is conditional rather than unconditional.
54 pub fn without_cargo_subcommand<I>(args: I) -> Vec<String>
55 where
56 I: IntoIterator<Item = String>,
57 {
58 let args: Vec<String> = args.into_iter().collect();
59 if args.get(1).map(String::as_str) != Some("stern4rust") {
60 return args;
61 }
62 let mut forwarded = Vec::with_capacity(args.len() - 1);
63 forwarded.extend(args.iter().take(1).cloned());
64 forwarded.extend(args.into_iter().skip(2));
65 forwarded
66 }
67}