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