dsfb_gpu_debug_demo/cli/mod.rs
1//! Hand-rolled CLI argument parsing.
2//!
3//! `clap` would add a multi-crate dependency tree. The demo binary only
4//! takes a handful of flags per subcommand, so a small, declarative parser
5//! keeps the surface area observable.
6//!
7//! Each subcommand module exposes `parse_and_run(args)` which receives the
8//! tail of the argv (after the subcommand name) and returns an
9//! `std::process::ExitCode`. The parser itself is `parse_flags`, which
10//! walks `--key value` and `--key=value` forms and populates a tiny
11//! HashMap.
12
13use std::collections::BTreeMap;
14use std::process::ExitCode;
15
16pub mod audit_report;
17pub mod bench;
18pub mod bench_gpu_scale;
19pub mod compare;
20pub mod generate_fixture;
21pub mod ingest;
22pub mod run_cpu;
23pub mod run_gpu;
24pub mod s_real_audit;
25
26/// Parse `--key value`, `--key=value`, and bare-`--key` flag forms into
27/// a sorted map.
28///
29/// A flag with no value (either no following arg or the next arg starts
30/// with `--`) is recorded with the literal value `"true"`. This is how
31/// boolean toggles like `--detail` are surfaced to subcommands without
32/// requiring the caller to type `--detail true`.
33///
34/// Sorted because deterministic flag handling makes the parsed structure
35/// independent of argv order, which in turn makes error messages stable
36/// across runs.
37///
38/// # Errors
39///
40/// Returns an error message string if a flag appears more than once or
41/// a positional argument is encountered.
42pub fn parse_flags(args: &[String]) -> Result<BTreeMap<String, String>, String> {
43 let mut flags: BTreeMap<String, String> = BTreeMap::new();
44 let mut i = 0;
45 while i < args.len() {
46 let arg = &args[i];
47 if let Some(rest) = arg.strip_prefix("--") {
48 let (key, value) = if let Some((k, v)) = rest.split_once('=') {
49 (k.to_string(), v.to_string())
50 } else {
51 // Peek at the next argument. If it exists and is not
52 // another `--flag`, consume it as the value. Otherwise
53 // treat this as a boolean toggle with value `"true"`.
54 let next_is_value = args.get(i + 1).is_some_and(|next| !next.starts_with("--"));
55 if next_is_value {
56 let value = args[i + 1].clone();
57 i += 1;
58 (rest.to_string(), value)
59 } else {
60 (rest.to_string(), "true".to_string())
61 }
62 };
63 if flags.insert(key.clone(), value).is_some() {
64 return Err(format!("flag --{key} given more than once"));
65 }
66 i += 1;
67 } else {
68 return Err(format!("unexpected positional argument: {arg}"));
69 }
70 }
71 Ok(flags)
72}
73
74/// Look up a required flag; emit a helpful error if it's missing.
75///
76/// # Errors
77///
78/// Returns an error string if `name` is not present in `flags`.
79pub fn require_flag<'a>(
80 flags: &'a BTreeMap<String, String>,
81 name: &str,
82) -> Result<&'a str, String> {
83 flags
84 .get(name)
85 .map(String::as_str)
86 .ok_or_else(|| format!("missing required flag --{name}"))
87}
88
89/// Print a usage error to stderr and return the standard usage exit code.
90pub fn usage_error(message: &str) -> ExitCode {
91 eprintln!("dsfb-gpu-debug: {message}");
92 ExitCode::from(1)
93}