Skip to main content

grit_lib/parse_options_test_tool/
parse_options_cmd.rs

1//! `test-tool parse-options` — mirrors `cmd__parse_options` in `git/t/helper/test-parse-options.c`.
2
3use std::collections::HashMap;
4
5use super::git_number::{git_parse_signed, git_parse_unsigned};
6
7const PARSE_OPTIONS_HELP: &str = include_str!("parse_options_help.txt");
8
9/// Exit status from `cmd__parse_options` (0 ok, 1 expect mismatch).
10pub type ParseOptionsStatus = i32;
11
12#[derive(Debug)]
13pub enum ParseOptionsToolError {
14    /// `-h` / `--help`: help already printed to stdout, exit 129, empty stderr.
15    Help,
16    /// Callback returned an error without printing (Git `parse_options` exit 1, empty stderr).
17    Silent,
18    Fatal(String),
19    Bug(String),
20}
21
22impl std::fmt::Display for ParseOptionsToolError {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            ParseOptionsToolError::Help => f.write_str("(help)"),
26            ParseOptionsToolError::Silent => f.write_str("(silent)"),
27            ParseOptionsToolError::Fatal(s) | ParseOptionsToolError::Bug(s) => f.write_str(s),
28        }
29    }
30}
31
32impl std::error::Error for ParseOptionsToolError {}
33
34fn env_disallow_abbrev() -> bool {
35    std::env::var("GIT_TEST_DISALLOW_ABBREVIATED_OPTIONS")
36        .ok()
37        .as_deref()
38        .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
39        .unwrap_or(false)
40}
41
42fn int_bounds_32() -> (i128, i128) {
43    (i32::MIN as i128, i32::MAX as i128)
44}
45
46struct CmdElem {
47    value: i32,
48    opt: Option<(OptMeta, Option<String>, bool)>,
49}
50
51#[derive(Clone, Copy)]
52struct OptMeta {
53    name: &'static str,
54    is_cmdmode: bool,
55}
56
57struct PoState {
58    boolean: i32,
59    integer: i32,
60    unsigned_integer: u64,
61    timestamp: i64,
62    i16: i16,
63    u16: u16,
64    abbrev: i32,
65    verbose: i32,
66    dry_run: i32,
67    quiet: i32,
68    string: Option<String>,
69    file: Option<String>,
70    ambiguous: i32,
71    list: Vec<String>,
72    length_cb_called: bool,
73    length_cb_arg: Option<String>,
74    length_cb_unset: bool,
75    expect: HashMap<String, String>,
76    cmd: CmdElem,
77}
78
79impl Default for PoState {
80    fn default() -> Self {
81        Self {
82            boolean: 0,
83            integer: 0,
84            unsigned_integer: 0,
85            timestamp: 0,
86            i16: 0,
87            u16: 0,
88            abbrev: 7,
89            verbose: -1,
90            dry_run: 0,
91            quiet: 0,
92            string: None,
93            file: None,
94            ambiguous: 0,
95            list: Vec::new(),
96            length_cb_called: false,
97            length_cb_arg: None,
98            length_cb_unset: false,
99            expect: HashMap::new(),
100            cmd: CmdElem {
101                value: 0,
102                opt: None,
103            },
104        }
105    }
106}
107
108impl PoState {
109    fn touch_integer(
110        &mut self,
111        new: i32,
112        meta: OptMeta,
113        arg: Option<&str>,
114        unset: bool,
115    ) -> Result<(), ParseOptionsToolError> {
116        self.integer = new;
117        let new_val = self.integer;
118        let e = &mut self.cmd;
119        if new_val == e.value {
120            return Ok(());
121        }
122        if let Some((prev, prev_arg, prev_unset)) = &e.opt {
123            if prev.is_cmdmode || meta.is_cmdmode {
124                let o1 = format_opt_display(meta.name, arg, unset);
125                let o2 = format_opt_display(prev.name, prev_arg.as_deref(), *prev_unset);
126                return Err(ParseOptionsToolError::Fatal(format!(
127                    "error: options '{o1}' and '{o2}' cannot be used together\n"
128                )));
129            }
130        }
131        e.opt = Some((meta, arg.map(|s| s.to_string()), unset));
132        e.value = new_val;
133        Ok(())
134    }
135
136    fn show_line(&self, expect: &HashMap<String, String>, line: &str, bad: &mut bool) {
137        if expect.is_empty() {
138            println!("{line}");
139            return;
140        }
141        let Some(colon) = line.find(':') else {
142            println!("{line}");
143            return;
144        };
145        let key = &line[..colon];
146        let Some(expected_full) = expect.get(key) else {
147            println!("{line}");
148            return;
149        };
150        if expected_full != line {
151            println!("-{expected_full}");
152            println!("+{line}");
153            *bad = true;
154        }
155    }
156
157    fn dump(
158        &self,
159        expect: &HashMap<String, String>,
160        rest: &[String],
161    ) -> Result<ParseOptionsStatus, ParseOptionsToolError> {
162        let mut bad = false;
163        if self.length_cb_called {
164            let arg = self.length_cb_arg.as_deref().unwrap_or("not set");
165            let u = if self.length_cb_unset { 1 } else { 0 };
166            let line = format!("Callback: \"{arg}\", {u}");
167            self.show_line(expect, &line, &mut bad);
168        }
169        self.show_line(expect, &format!("boolean: {}", self.boolean), &mut bad);
170        self.show_line(expect, &format!("integer: {}", self.integer), &mut bad);
171        self.show_line(expect, &format!("i16: {}", self.i16), &mut bad);
172        self.show_line(
173            expect,
174            &format!("unsigned: {}", self.unsigned_integer),
175            &mut bad,
176        );
177        self.show_line(expect, &format!("u16: {}", self.u16), &mut bad);
178        self.show_line(expect, &format!("timestamp: {}", self.timestamp), &mut bad);
179        let s = self.string.as_deref().unwrap_or("(not set)");
180        self.show_line(expect, &format!("string: {s}"), &mut bad);
181        self.show_line(expect, &format!("abbrev: {}", self.abbrev), &mut bad);
182        self.show_line(expect, &format!("verbose: {}", self.verbose), &mut bad);
183        self.show_line(expect, &format!("quiet: {}", self.quiet), &mut bad);
184        self.show_line(
185            expect,
186            &format!("dry run: {}", if self.dry_run != 0 { "yes" } else { "no" }),
187            &mut bad,
188        );
189        let f = self.file.as_deref().unwrap_or("(not set)");
190        self.show_line(expect, &format!("file: {f}"), &mut bad);
191        for item in &self.list {
192            self.show_line(expect, &format!("list: {item}"), &mut bad);
193        }
194        for (i, a) in rest.iter().enumerate() {
195            self.show_line(expect, &format!("arg {i:02}: {a}"), &mut bad);
196        }
197        Ok(if bad { 1 } else { 0 })
198    }
199}
200
201fn format_opt_display(name: &'static str, arg: Option<&str>, unset: bool) -> String {
202    if name == "mode34" && !unset {
203        if let Some(a) = arg {
204            return format!("--mode34={a}");
205        }
206    }
207    format!("--{name}")
208}
209
210fn usage_append() -> String {
211    PARSE_OPTIONS_HELP.to_string()
212}
213
214/// Git's `usage_with_options` is attached only for some parse errors; t0040 expects a bare line
215/// for missing values, superfluous `=`, range errors, etc., but appends help for unknown/ambiguous
216/// options (see `check_unknown_i18n` and ambiguous-abbrev tests).
217fn append_usage_if_unknown(msg: &str) -> String {
218    let with_usage = msg.to_string() + &usage_append();
219    if msg.starts_with("error: unknown option `")
220        || msg.starts_with("error: unknown switch `")
221        || msg.starts_with("ambiguous option:")
222    {
223        with_usage
224    } else {
225        msg.to_string()
226    }
227}
228
229fn map_parse_fatal(e: ParseOptionsToolError) -> ParseOptionsToolError {
230    match e {
231        ParseOptionsToolError::Fatal(m) => {
232            ParseOptionsToolError::Fatal(append_usage_if_unknown(&m))
233        }
234        ParseOptionsToolError::Silent => ParseOptionsToolError::Silent,
235        o => o,
236    }
237}
238
239fn collect_expect(
240    map: &mut HashMap<String, String>,
241    arg: &str,
242) -> Result<(), ParseOptionsToolError> {
243    let Some(colon) = arg.find(':') else {
244        return Err(ParseOptionsToolError::Fatal(
245            "malformed --expect option\n".to_string() + &usage_append(),
246        ));
247    };
248    let key = arg[..colon].to_string();
249    if map.insert(key, arg.to_string()).is_some() {
250        return Err(ParseOptionsToolError::Fatal(format!(
251            "malformed --expect option, duplicate {}\n",
252            &arg[..colon]
253        )));
254    }
255    Ok(())
256}
257
258/// `test-tool parse-options` — mirrors `cmd__parse_options`.
259pub fn run_parse_options(args: &[String]) -> Result<ParseOptionsStatus, ParseOptionsToolError> {
260    let disallow_abbrev = env_disallow_abbrev();
261    let mut st = PoState::default();
262    let argv = args;
263    if argv.is_empty() {
264        return Err(ParseOptionsToolError::Fatal(
265            "usage: test-tool parse-options <options>\n".to_string() + &usage_append(),
266        ));
267    }
268    let mut i = 1usize;
269    let prefix = "prefix/";
270    let mut rest: Vec<String> = Vec::new();
271
272    while i < argv.len() {
273        let arg = &argv[i];
274        if arg == "-h" || arg == "--help" {
275            print!("{PARSE_OPTIONS_HELP}");
276            return Err(ParseOptionsToolError::Help);
277        }
278        if arg == "--help-all" {
279            print!("{PARSE_OPTIONS_HELP}");
280            return Err(ParseOptionsToolError::Help);
281        }
282        if arg == "--" {
283            i += 1;
284            rest.extend(argv[i..].iter().cloned());
285            return st.dump(&st.expect.clone(), &rest);
286        }
287        if let Some(rest_arg) = arg.strip_prefix("--") {
288            if rest_arg == "end-of-options" {
289                i += 1;
290                rest.extend(argv[i..].iter().cloned());
291                return st.dump(&st.expect.clone(), &rest);
292            }
293            let (name, eq_val) = if let Some(p) = rest_arg.find('=') {
294                (&rest_arg[..p], Some(rest_arg[p + 1..].to_string()))
295            } else {
296                (rest_arg, None)
297            };
298            if name == "expect" {
299                let v = eq_val.ok_or_else(|| {
300                    ParseOptionsToolError::Fatal(
301                        "error: option `expect' requires a value\n".to_string(),
302                    )
303                })?;
304                collect_expect(&mut st.expect, &v)?;
305                i += 1;
306                continue;
307            }
308            match parse_long(&mut st, name, eq_val, argv, &mut i, prefix, disallow_abbrev) {
309                Ok(()) => {}
310                Err(e) => return Err(map_parse_fatal(e)),
311            }
312            continue;
313        }
314        if arg.starts_with('-') && arg.len() > 1 {
315            if arg == "-" {
316                i += 1;
317                rest.extend(argv[i..].iter().cloned());
318                return st.dump(&st.expect.clone(), &rest);
319            }
320            i = match parse_short(&mut st, argv, i, prefix, disallow_abbrev) {
321                Ok(n) => n,
322                Err(e) => return Err(map_parse_fatal(e)),
323            };
324            continue;
325        }
326        // Dashless argv (Git `parse_options_step`): only `+` is a NODASH option here.
327        if arg == "+" {
328            st.boolean = st.boolean.saturating_add(1);
329            i += 1;
330            continue;
331        }
332        rest.push(arg.clone());
333        i += 1;
334    }
335
336    st.dump(&st.expect.clone(), &rest)
337}
338
339fn parse_long(
340    st: &mut PoState,
341    name: &str,
342    eq_val: Option<String>,
343    argv: &[String],
344    i: &mut usize,
345    prefix: &str,
346    disallow_abbrev: bool,
347) -> Result<(), ParseOptionsToolError> {
348    let arg_end = name.find('=').unwrap_or(name.len());
349    let original_key = name;
350    let mut flags_unset = false;
351    let mut arg_starts_with_no_no = false;
352    let mut s = name;
353    if let Some(x) = s.strip_prefix("no-") {
354        if let Some(x2) = x.strip_prefix("no-") {
355            arg_starts_with_no_no = true;
356            s = x2;
357        } else {
358            flags_unset = true;
359            s = x;
360        }
361    }
362
363    let _ = arg_starts_with_no_no;
364
365    let matched = long_exact(st, s, flags_unset, eq_val.clone(), argv, i, prefix)?;
366    if matched {
367        return Ok(());
368    }
369
370    if !disallow_abbrev {
371        let m = long_abbrev(st, s, arg_end, flags_unset, eq_val.clone(), argv, i, prefix)?;
372        if m {
373            return Ok(());
374        }
375    }
376
377    Err(unknown_long(original_key))
378}
379
380fn unknown_long(name: &str) -> ParseOptionsToolError {
381    ParseOptionsToolError::Fatal(format!("error: unknown option `{name}'\n"))
382}
383
384fn long_exact(
385    st: &mut PoState,
386    full: &str,
387    flags_unset: bool,
388    eq_val: Option<String>,
389    argv: &[String],
390    i: &mut usize,
391    prefix: &str,
392) -> Result<bool, ParseOptionsToolError> {
393    let u = flags_unset;
394    let mut hit = false;
395    match full {
396        "yes" => {
397            let err_name = if u { "no-yes" } else { "yes" };
398            no_eq(eq_val.as_deref(), err_name, u)?;
399            let unset = u ^ false;
400            st.boolean = if unset { 0 } else { 1 };
401            hit = true;
402        }
403        "doubt" => {
404            let err_name = if u { "no-doubt" } else { "doubt" };
405            no_eq(eq_val.as_deref(), err_name, u)?;
406            let unset = u ^ true;
407            st.boolean = if unset { 0 } else { 1 };
408            hit = true;
409        }
410        "no-fear" => {
411            no_eq(eq_val.as_deref(), "no-fear", u)?;
412            st.boolean = 1;
413            hit = true;
414        }
415        "boolean" => {
416            no_eq(eq_val.as_deref(), "boolean", u)?;
417            if u {
418                st.boolean = 0;
419            } else {
420                st.boolean = st.boolean.saturating_add(1);
421            }
422            hit = true;
423        }
424        "or4" => {
425            no_eq(eq_val.as_deref(), "or4", u)?;
426            if u {
427                st.boolean &= !4;
428            } else {
429                st.boolean |= 4;
430            }
431            hit = true;
432        }
433        "neg-or4" => {
434            no_eq(eq_val.as_deref(), "neg-or4", u)?;
435            if u {
436                st.boolean |= 4;
437            } else {
438                st.boolean &= !4;
439            }
440            hit = true;
441        }
442        "integer" => {
443            let v = take_val(eq_val, argv, i, "integer")?;
444            set_int(st, &v, "integer")?;
445            hit = true;
446        }
447        "i16" => {
448            let v = take_val(eq_val, argv, i, "i16")?;
449            set_i16(st, &v)?;
450            hit = true;
451        }
452        "unsigned" => {
453            let v = take_val(eq_val, argv, i, "unsigned")?;
454            set_unsigned(st, &v)?;
455            hit = true;
456        }
457        "u16" => {
458            let v = take_val(eq_val, argv, i, "u16")?;
459            set_u16(st, &v)?;
460            hit = true;
461        }
462        "set23" => {
463            no_eq(eq_val.as_deref(), "set23", u)?;
464            st.touch_integer(if u { 0 } else { 23 }, opt("set23", false), None, u)?;
465            hit = true;
466        }
467        "mode1" => {
468            no_eq(eq_val.as_deref(), "mode1", u)?;
469            st.touch_integer(if u { 0 } else { 1 }, opt("mode1", true), None, u)?;
470            hit = true;
471        }
472        "mode2" => {
473            no_eq(eq_val.as_deref(), "mode2", u)?;
474            st.touch_integer(if u { 0 } else { 2 }, opt("mode2", true), None, u)?;
475            hit = true;
476        }
477        "mode34" => {
478            let v = take_val(eq_val, argv, i, "mode34")?;
479            if u {
480                st.touch_integer(0, opt("mode34", true), Some("0"), true)?;
481            } else if v == "3" {
482                st.touch_integer(3, opt("mode34", true), Some("3"), false)?;
483            } else if v == "4" {
484                st.touch_integer(4, opt("mode34", true), Some("4"), false)?;
485            } else {
486                return Err(ParseOptionsToolError::Fatal(format!(
487                    "error: invalid value for '--mode34': '{v}'\n"
488                )));
489            }
490            hit = true;
491        }
492        "length" => {
493            if u {
494                no_eq(eq_val.as_deref(), "no-length", u)?;
495                return Err(ParseOptionsToolError::Silent);
496            }
497            let v = take_val(eq_val, argv, i, "length")?;
498            st.length_cb_called = true;
499            st.length_cb_arg = Some(v.clone());
500            st.length_cb_unset = false;
501            st.touch_integer(v.len() as i32, opt("length", false), None, false)?;
502            hit = true;
503        }
504        "file" => {
505            let v = take_val(eq_val, argv, i, "file")?;
506            if u {
507                st.file = None;
508            } else {
509                st.file = Some(format!("{prefix}{v}"));
510            }
511            hit = true;
512        }
513        "string" | "string2" | "st" => {
514            let v = take_val(eq_val, argv, i, "string")?;
515            if u {
516                st.string = None;
517            } else {
518                st.string = Some(v);
519            }
520            hit = true;
521        }
522        "obsolete" => {
523            no_eq(eq_val.as_deref(), "obsolete", false)?;
524            hit = true;
525        }
526        "longhelp" => {
527            no_eq(eq_val.as_deref(), "longhelp", u)?;
528            st.touch_integer(0, opt("longhelp", false), None, u)?;
529            hit = true;
530        }
531        "list" => {
532            if u {
533                no_eq(eq_val.as_deref(), "list", u)?;
534                st.list.clear();
535            } else {
536                let v = take_val(eq_val, argv, i, "list")?;
537                st.list.push(v);
538            }
539            hit = true;
540        }
541        "ambiguous" => {
542            no_eq(eq_val.as_deref(), "ambiguous", false)?;
543            st.ambiguous = st.ambiguous.saturating_add(1);
544            hit = true;
545        }
546        "no-ambiguous" => {
547            no_eq(eq_val.as_deref(), "no-ambiguous", false)?;
548            st.ambiguous = 0;
549            hit = true;
550        }
551        "abbrev" => {
552            if u {
553                st.abbrev = 0;
554            } else if let Some(ev) = eq_val {
555                parse_abbrev(&ev, &mut st.abbrev)?;
556            } else if *i + 1 < argv.len() {
557                let v = take_val(None, argv, i, "abbrev")?;
558                parse_abbrev(&v, &mut st.abbrev)?;
559            } else {
560                st.abbrev = 7;
561            }
562            hit = true;
563        }
564        "verbose" => {
565            no_eq(eq_val.as_deref(), "verbose", u)?;
566            if u {
567                st.verbose = 0;
568            } else {
569                st.verbose = if st.verbose < 0 { 1 } else { st.verbose + 1 };
570            }
571            hit = true;
572        }
573        "quiet" => {
574            no_eq(eq_val.as_deref(), "quiet", u)?;
575            if u {
576                st.quiet = 0;
577            } else {
578                st.quiet = st.quiet.saturating_add(1);
579            }
580            hit = true;
581        }
582        "dry-run" => {
583            no_eq(eq_val.as_deref(), "dry-run", u)?;
584            st.dry_run = if u { 0 } else { 1 };
585            hit = true;
586        }
587        "alias-source" | "alias-target" => {
588            let v = take_val(eq_val, argv, i, "alias-source")?;
589            if u {
590                st.string = None;
591            } else {
592                st.string = Some(v);
593            }
594            hit = true;
595        }
596        _ => {}
597    }
598    if hit {
599        *i += 1;
600    }
601    Ok(hit)
602}
603
604fn opt(name: &'static str, is_cmdmode: bool) -> OptMeta {
605    OptMeta { name, is_cmdmode }
606}
607
608/// Long option names in `parse_long_opt` table order (for abbreviation + ambiguity).
609const LONG_NAMES: &[&str] = &[
610    "yes",
611    "no-doubt",
612    "doubt",
613    "no-fear",
614    "boolean",
615    "or4",
616    "neg-or4",
617    "integer",
618    "i16",
619    "unsigned",
620    "u16",
621    "set23",
622    "mode1",
623    "mode2",
624    "mode34",
625    "length",
626    "file",
627    "string",
628    "string2",
629    "st",
630    "obsolete",
631    "longhelp",
632    "list",
633    "ambiguous",
634    "no-ambiguous",
635    "abbrev",
636    "verbose",
637    "quiet",
638    "dry-run",
639    "expect",
640    "alias-source",
641    "alias-target",
642];
643
644fn is_alias_pair(a: &str, b: &str) -> bool {
645    (a == "alias-source" && b == "alias-target") || (a == "alias-target" && b == "alias-source")
646}
647
648fn long_abbrev(
649    st: &mut PoState,
650    s: &str,
651    _arg_end: usize,
652    flags_unset: bool,
653    eq_val: Option<String>,
654    argv: &[String],
655    i: &mut usize,
656    prefix: &str,
657) -> Result<bool, ParseOptionsToolError> {
658    let user_len = s.len();
659    if user_len == 0 {
660        return Ok(false);
661    }
662    let mut matches: Vec<&'static str> = Vec::new();
663    for &ln in LONG_NAMES {
664        let mut long_name = ln;
665        let mut opt_unset = false;
666        if let Some(x) = long_name.strip_prefix("no-") {
667            long_name = x;
668            opt_unset = true;
669        }
670        let allow_unset = !matches!(
671            ln,
672            "no-fear" | "obsolete" | "longhelp" | "ambiguous" | "no-ambiguous"
673        );
674        if (flags_unset ^ opt_unset) && !allow_unset {
675            continue;
676        }
677        if long_name.len() >= user_len && long_name.as_bytes().get(..user_len) == Some(s.as_bytes())
678        {
679            matches.push(ln);
680        }
681    }
682    matches.sort_unstable();
683    matches.dedup();
684    if matches.is_empty() {
685        return Ok(false);
686    }
687    let mut abbrev: Option<&str> = None;
688    let mut ambiguous: Option<(&str, &str)> = None;
689    for m in &matches {
690        match abbrev {
691            None => abbrev = Some(m),
692            Some(a) => {
693                if !is_alias_pair(a, m) {
694                    ambiguous = Some((a, m));
695                    break;
696                }
697                abbrev = Some(m);
698            }
699        }
700    }
701    if let Some((a, b)) = ambiguous {
702        return Err(ParseOptionsToolError::Fatal(format!(
703            "ambiguous option: {s} (could be --{a} or --{b})\n"
704        )));
705    }
706    let Some(only) = abbrev else {
707        return Ok(false);
708    };
709    let key = match only {
710        "no-doubt" => "doubt",
711        o => o,
712    };
713    long_exact(st, key, flags_unset, eq_val, argv, i, prefix)?;
714    Ok(true)
715}
716
717fn no_eq(eq: Option<&str>, name: &str, unset: bool) -> Result<(), ParseOptionsToolError> {
718    if let Some(x) = eq {
719        if !x.is_empty() || unset {
720            return Err(ParseOptionsToolError::Fatal(format!(
721                "error: option `{name}' takes no value\n"
722            )));
723        }
724    }
725    Ok(())
726}
727
728fn take_val(
729    eq_val: Option<String>,
730    argv: &[String],
731    i: &mut usize,
732    optname: &str,
733) -> Result<String, ParseOptionsToolError> {
734    if let Some(v) = eq_val {
735        return Ok(v);
736    }
737    if *i + 1 >= argv.len() {
738        return Err(ParseOptionsToolError::Fatal(format!(
739            "error: option `{optname}' requires a value\n"
740        )));
741    }
742    *i += 1;
743    Ok(argv[*i].clone())
744}
745
746fn parse_abbrev(s: &str, out: &mut i32) -> Result<(), ParseOptionsToolError> {
747    if s.is_empty() {
748        return Err(ParseOptionsToolError::Fatal(
749            "error: option `abbrev' expects a numerical value\n".to_string(),
750        ));
751    }
752    let v: i32 = s.parse().map_err(|_| {
753        ParseOptionsToolError::Fatal(
754            "error: option `abbrev' expects a numerical value\n".to_string(),
755        )
756    })?;
757    *out = if v != 0 && v < 4 { 4 } else { v };
758    Ok(())
759}
760
761fn set_int(st: &mut PoState, raw: &str, optname: &str) -> Result<(), ParseOptionsToolError> {
762    let (lo, hi) = int_bounds_32();
763    let opt_meta = match optname {
764        "integer" | "j" => opt("integer", false),
765        _ => {
766            return Err(ParseOptionsToolError::Fatal(format!(
767                "internal error: unknown option name for set_int: {optname}\n"
768            )));
769        }
770    };
771    match git_parse_signed(raw, hi) {
772        Ok(v) if v >= lo && v <= hi => {
773            st.touch_integer(v as i32, opt_meta, None, false)?;
774            Ok(())
775        }
776        Err(std::io::ErrorKind::InvalidData) => Err(ParseOptionsToolError::Fatal(format!(
777            "error: value {raw} for option `{optname}' not in range [{lo},{hi}]\n"
778        ))),
779        _ => Err(ParseOptionsToolError::Fatal(format!(
780            "error: option `{optname}' expects an integer value with an optional k/m/g suffix\n"
781        ))),
782    }
783}
784
785fn set_i16(st: &mut PoState, raw: &str) -> Result<(), ParseOptionsToolError> {
786    match git_parse_signed(raw, i16::MAX as i128) {
787        Ok(v) if v >= i16::MIN as i128 && v <= i16::MAX as i128 => {
788            st.i16 = v as i16;
789            Ok(())
790        }
791        Err(std::io::ErrorKind::InvalidData) | Ok(_) => Err(ParseOptionsToolError::Fatal(format!(
792            "error: value {raw} for option `i16' not in range [-32768,32767]\n"
793        ))),
794        _ => Err(ParseOptionsToolError::Fatal(
795            "error: option `i16' expects an integer value with an optional k/m/g suffix\n"
796                .to_string(),
797        )),
798    }
799}
800
801fn set_unsigned(st: &mut PoState, raw: &str) -> Result<(), ParseOptionsToolError> {
802    match git_parse_unsigned(raw, u64::MAX as u128) {
803        Ok(v) => {
804            st.unsigned_integer = v as u64;
805            Ok(())
806        }
807        Err(std::io::ErrorKind::InvalidData) => Err(ParseOptionsToolError::Fatal(format!(
808            "error: value {raw} for option `unsigned' not in range [0,{}]\n",
809            u64::MAX
810        ))),
811        _ => Err(ParseOptionsToolError::Fatal(
812            "error: option `unsigned' expects a non-negative integer value with an optional k/m/g suffix\n"
813                .to_string(),
814        )),
815    }
816}
817
818fn set_u16(st: &mut PoState, raw: &str) -> Result<(), ParseOptionsToolError> {
819    match git_parse_unsigned(raw, u16::MAX as u128) {
820        Ok(v) if v <= u16::MAX as u128 => {
821            st.u16 = v as u16;
822            Ok(())
823        }
824        _ => Err(ParseOptionsToolError::Fatal(format!(
825            "error: value {raw} for option `u16' not in range [0,65535]\n"
826        ))),
827    }
828}
829
830/// Git `starts_with(long_name, arg)` for typo detection: byte prefix of `long_name`.
831fn long_name_starts_with_user_typos(user: &str, long_name: &str) -> bool {
832    long_name
833        .as_bytes()
834        .get(..user.len())
835        .is_some_and(|pfx| pfx == user.as_bytes())
836}
837
838/// Git `check_typos(arg + 1, …)` for a short-option cluster: `cluster` is the full argv suffix
839/// after `-` (e.g. `boolean` for `-boolean`), not the unconsumed tail.
840fn check_typos_short_cluster(cluster: &str) -> Result<(), ParseOptionsToolError> {
841    if cluster.len() < 3 {
842        return Ok(());
843    }
844    if cluster.starts_with("no-") {
845        return Err(ParseOptionsToolError::Fatal(format!(
846            "error: did you mean `--{cluster}` (with two dashes)?\n"
847        )));
848    }
849    for ln in LONG_NAMES {
850        if long_name_starts_with_user_typos(cluster, ln) {
851            return Err(ParseOptionsToolError::Fatal(format!(
852                "error: did you mean `--{cluster}` (with two dashes)?\n"
853            )));
854        }
855    }
856    Ok(())
857}
858
859/// One step of Git `parse_short_opt`: explicit short options win; otherwise a digit run is
860/// `OPTION_NUMBER`.
861fn parse_short_opt_step(
862    st: &mut PoState,
863    full_suffix: &str,
864    o: &mut usize,
865    local_i: &mut usize,
866    argv: &[String],
867    prefix: &str,
868) -> Result<(), ParseOptionsToolError> {
869    let Some(first) = full_suffix[*o..].chars().next() else {
870        return Ok(());
871    };
872    let flen = first.len_utf8();
873
874    match first {
875        'h' => {
876            print!("{PARSE_OPTIONS_HELP}");
877            return Err(ParseOptionsToolError::Help);
878        }
879        's' => {
880            let v = if *o + flen < full_suffix.len() {
881                full_suffix[*o + flen..].to_string()
882            } else if *local_i + 1 < argv.len() {
883                *local_i += 1;
884                argv[*local_i].clone()
885            } else {
886                return Err(ParseOptionsToolError::Fatal(
887                    "error: switch `s' requires a value\n".to_string(),
888                ));
889            };
890            *o = full_suffix.len();
891            st.string = Some(v);
892        }
893        'b' => {
894            st.boolean = st.boolean.saturating_add(1);
895            *o += flen;
896        }
897        'i' => {
898            let tail = &full_suffix[*o + flen..];
899            let v = if !tail.is_empty() {
900                tail.to_string()
901            } else if *local_i + 1 < argv.len() {
902                *local_i += 1;
903                argv[*local_i].clone()
904            } else {
905                return Err(ParseOptionsToolError::Fatal(
906                    "error: switch `i' requires a value\n".to_string(),
907                ));
908            };
909            set_int(st, &v, "integer")?;
910            *o = full_suffix.len();
911        }
912        'j' => {
913            let tail = &full_suffix[*o + flen..];
914            let v = if !tail.is_empty() {
915                tail.to_string()
916            } else if *local_i + 1 < argv.len() {
917                *local_i += 1;
918                argv[*local_i].clone()
919            } else {
920                return Err(ParseOptionsToolError::Fatal(
921                    "error: switch `j' requires a value\n".to_string(),
922                ));
923            };
924            set_int(st, &v, "j")?;
925            *o = full_suffix.len();
926        }
927        'u' => {
928            let tail = &full_suffix[*o + flen..];
929            let v = if !tail.is_empty() {
930                tail.to_string()
931            } else if *local_i + 1 < argv.len() {
932                *local_i += 1;
933                argv[*local_i].clone()
934            } else {
935                return Err(ParseOptionsToolError::Fatal(
936                    "error: switch `u' requires a value\n".to_string(),
937                ));
938            };
939            set_unsigned(st, &v)?;
940            *o = full_suffix.len();
941        }
942        'v' => {
943            st.verbose = if st.verbose < 0 { 1 } else { st.verbose + 1 };
944            *o += flen;
945        }
946        'n' => {
947            st.dry_run = 1;
948            *o += flen;
949        }
950        'q' => {
951            st.quiet = st.quiet.saturating_add(1);
952            *o += flen;
953        }
954        'D' => {
955            st.boolean = 1;
956            *o += flen;
957        }
958        'B' => {
959            st.boolean = 1;
960            *o += flen;
961        }
962        '4' => {
963            st.boolean |= 4;
964            *o += flen;
965        }
966        'L' => {
967            let v = if *o + flen < full_suffix.len() {
968                full_suffix[*o + flen..].to_string()
969            } else if *local_i + 1 < argv.len() {
970                *local_i += 1;
971                argv[*local_i].clone()
972            } else {
973                return Err(ParseOptionsToolError::Fatal(
974                    "error: switch `L' requires a value\n".to_string(),
975                ));
976            };
977            *o = full_suffix.len();
978            st.length_cb_called = true;
979            st.length_cb_arg = Some(v.clone());
980            st.length_cb_unset = false;
981            st.touch_integer(v.len() as i32, opt("length", false), None, false)?;
982        }
983        'F' => {
984            let v = if *o + flen < full_suffix.len() {
985                full_suffix[*o + flen..].to_string()
986            } else if *local_i + 1 < argv.len() {
987                *local_i += 1;
988                argv[*local_i].clone()
989            } else {
990                return Err(ParseOptionsToolError::Fatal(
991                    "error: switch `F' requires a value\n".to_string(),
992                ));
993            };
994            *o = full_suffix.len();
995            st.file = Some(format!("{prefix}{v}"));
996        }
997        'A' => {
998            let v = if *o + flen < full_suffix.len() {
999                full_suffix[*o + flen..].to_string()
1000            } else if *local_i + 1 < argv.len() {
1001                *local_i += 1;
1002                argv[*local_i].clone()
1003            } else {
1004                return Err(ParseOptionsToolError::Fatal(
1005                    "error: switch `A' requires a value\n".to_string(),
1006                ));
1007            };
1008            *o = full_suffix.len();
1009            st.string = Some(v);
1010        }
1011        'Z' => {
1012            let v = if *o + flen < full_suffix.len() {
1013                full_suffix[*o + flen..].to_string()
1014            } else if *local_i + 1 < argv.len() {
1015                *local_i += 1;
1016                argv[*local_i].clone()
1017            } else {
1018                return Err(ParseOptionsToolError::Fatal(
1019                    "error: switch `Z' requires a value\n".to_string(),
1020                ));
1021            };
1022            *o = full_suffix.len();
1023            st.string = Some(v);
1024        }
1025        'o' => {
1026            let v = if *o + flen < full_suffix.len() {
1027                full_suffix[*o + flen..].to_string()
1028            } else if *local_i + 1 < argv.len() {
1029                *local_i += 1;
1030                argv[*local_i].clone()
1031            } else {
1032                return Err(ParseOptionsToolError::Fatal(
1033                    "error: switch `o' requires a value\n".to_string(),
1034                ));
1035            };
1036            *o = full_suffix.len();
1037            st.string = Some(v);
1038        }
1039        '+' => {
1040            st.boolean = st.boolean.saturating_add(1);
1041            *o += flen;
1042        }
1043        c if c.is_ascii_digit() => {
1044            let start = *o;
1045            let mut end = start;
1046            while end < full_suffix.len() && full_suffix.as_bytes()[end].is_ascii_digit() {
1047                end += 1;
1048            }
1049            let digits = &full_suffix[start..end];
1050            let n: i32 = digits
1051                .parse()
1052                .map_err(|_| ParseOptionsToolError::Fatal("error: invalid number\n".to_string()))?;
1053            st.touch_integer(
1054                n,
1055                OptMeta {
1056                    name: "NUM",
1057                    is_cmdmode: false,
1058                },
1059                None,
1060                false,
1061            )?;
1062            *o = end;
1063        }
1064        _ => {
1065            if *o == 0 {
1066                check_typos_short_cluster(full_suffix)?;
1067            }
1068            return Err(ParseOptionsToolError::Fatal(format!(
1069                "error: unknown switch `{first}'\n"
1070            )));
1071        }
1072    }
1073    Ok(())
1074}
1075
1076fn parse_short(
1077    st: &mut PoState,
1078    argv: &[String],
1079    i: usize,
1080    prefix: &str,
1081    _disallow_abbrev: bool,
1082) -> Result<usize, ParseOptionsToolError> {
1083    let arg = &argv[i];
1084    let full_suffix = &arg[1..];
1085    if full_suffix.is_empty() {
1086        return Err(ParseOptionsToolError::Fatal(
1087            "error: unknown switch\n".to_string(),
1088        ));
1089    }
1090
1091    let mut o = 0usize;
1092    let mut local_i = i;
1093
1094    parse_short_opt_step(st, full_suffix, &mut o, &mut local_i, argv, prefix)?;
1095    if o < full_suffix.len() {
1096        check_typos_short_cluster(full_suffix)?;
1097    }
1098    while o < full_suffix.len() {
1099        parse_short_opt_step(st, full_suffix, &mut o, &mut local_i, argv, prefix)?;
1100        if o < full_suffix.len() {
1101            check_typos_short_cluster(full_suffix)?;
1102        }
1103    }
1104
1105    Ok(local_i + 1)
1106}