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
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        o => o,
235    }
236}
237
238fn collect_expect(
239    map: &mut HashMap<String, String>,
240    arg: &str,
241) -> Result<(), ParseOptionsToolError> {
242    let Some(colon) = arg.find(':') else {
243        return Err(ParseOptionsToolError::Fatal(
244            "malformed --expect option\n".to_string() + &usage_append(),
245        ));
246    };
247    let key = arg[..colon].to_string();
248    if map.insert(key, arg.to_string()).is_some() {
249        return Err(ParseOptionsToolError::Fatal(format!(
250            "malformed --expect option, duplicate {}\n",
251            &arg[..colon]
252        )));
253    }
254    Ok(())
255}
256
257/// `test-tool parse-options` — mirrors `cmd__parse_options`.
258pub fn run_parse_options(args: &[String]) -> Result<ParseOptionsStatus, ParseOptionsToolError> {
259    let disallow_abbrev = env_disallow_abbrev();
260    let mut st = PoState::default();
261    let argv = args;
262    if argv.is_empty() {
263        return Err(ParseOptionsToolError::Fatal(
264            "usage: test-tool parse-options <options>\n".to_string() + &usage_append(),
265        ));
266    }
267    let mut i = 1usize;
268    let prefix = "prefix/";
269    let mut rest: Vec<String> = Vec::new();
270
271    while i < argv.len() {
272        let arg = &argv[i];
273        if arg == "-h" || arg == "--help" {
274            print!("{PARSE_OPTIONS_HELP}");
275            return Err(ParseOptionsToolError::Help);
276        }
277        if arg == "--help-all" {
278            print!("{PARSE_OPTIONS_HELP}");
279            return Err(ParseOptionsToolError::Help);
280        }
281        if arg == "--" {
282            i += 1;
283            rest.extend(argv[i..].iter().cloned());
284            return st.dump(&st.expect.clone(), &rest);
285        }
286        if let Some(rest_arg) = arg.strip_prefix("--") {
287            if rest_arg == "end-of-options" {
288                i += 1;
289                rest.extend(argv[i..].iter().cloned());
290                return st.dump(&st.expect.clone(), &rest);
291            }
292            let (name, eq_val) = if let Some(p) = rest_arg.find('=') {
293                (&rest_arg[..p], Some(rest_arg[p + 1..].to_string()))
294            } else {
295                (rest_arg, None)
296            };
297            if name == "expect" {
298                let v = eq_val.ok_or_else(|| {
299                    ParseOptionsToolError::Fatal(
300                        "error: option `expect' requires a value\n".to_string(),
301                    )
302                })?;
303                collect_expect(&mut st.expect, &v)?;
304                i += 1;
305                continue;
306            }
307            match parse_long(&mut st, name, eq_val, argv, &mut i, prefix, disallow_abbrev) {
308                Ok(()) => {}
309                Err(e) => return Err(map_parse_fatal(e)),
310            }
311            continue;
312        }
313        if arg.starts_with('-') && arg.len() > 1 {
314            if arg == "-" {
315                i += 1;
316                rest.extend(argv[i..].iter().cloned());
317                return st.dump(&st.expect.clone(), &rest);
318            }
319            // -NUM (OPTION_NUMBER): entire argv is `-` followed by decimal digits
320            let tail = &arg[1..];
321            if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) {
322                let n: i32 = -tail.parse::<i32>().map_err(|_| {
323                    ParseOptionsToolError::Fatal("error: invalid number\n".to_string())
324                })?;
325                st.touch_integer(
326                    n,
327                    OptMeta {
328                        name: "NUM",
329                        is_cmdmode: false,
330                    },
331                    None,
332                    false,
333                )?;
334                i += 1;
335                continue;
336            }
337            i = match parse_short(&mut st, argv, i, prefix, disallow_abbrev) {
338                Ok(n) => n,
339                Err(e) => return Err(map_parse_fatal(e)),
340            };
341            continue;
342        }
343        // Dashless argv (Git `parse_options_step`): only `+` is a NODASH option here.
344        if arg == "+" {
345            st.boolean = st.boolean.saturating_add(1);
346            i += 1;
347            continue;
348        }
349        rest.push(arg.clone());
350        i += 1;
351    }
352
353    st.dump(&st.expect.clone(), &rest)
354}
355
356fn parse_long(
357    st: &mut PoState,
358    name: &str,
359    eq_val: Option<String>,
360    argv: &[String],
361    i: &mut usize,
362    prefix: &str,
363    disallow_abbrev: bool,
364) -> Result<(), ParseOptionsToolError> {
365    let arg_end = name.find('=').unwrap_or(name.len());
366    let original_key = name;
367    let mut flags_unset = false;
368    let mut arg_starts_with_no_no = false;
369    let mut s = name;
370    if let Some(x) = s.strip_prefix("no-") {
371        if let Some(x2) = x.strip_prefix("no-") {
372            arg_starts_with_no_no = true;
373            s = x2;
374        } else {
375            flags_unset = true;
376            s = x;
377        }
378    }
379
380    let _ = arg_starts_with_no_no;
381
382    let matched = long_exact(st, s, flags_unset, eq_val.clone(), argv, i, prefix)?;
383    if matched {
384        return Ok(());
385    }
386
387    if !disallow_abbrev {
388        let m = long_abbrev(st, s, arg_end, flags_unset, eq_val.clone(), argv, i, prefix)?;
389        if m {
390            return Ok(());
391        }
392    }
393
394    Err(unknown_long(original_key))
395}
396
397fn unknown_long(name: &str) -> ParseOptionsToolError {
398    ParseOptionsToolError::Fatal(format!("error: unknown option `{name}'\n"))
399}
400
401fn long_exact(
402    st: &mut PoState,
403    full: &str,
404    flags_unset: bool,
405    eq_val: Option<String>,
406    argv: &[String],
407    i: &mut usize,
408    prefix: &str,
409) -> Result<bool, ParseOptionsToolError> {
410    let u = flags_unset;
411    let mut hit = false;
412    match full {
413        "yes" => {
414            let err_name = if u { "no-yes" } else { "yes" };
415            no_eq(eq_val.as_deref(), err_name, u)?;
416            let unset = u ^ false;
417            st.boolean = if unset { 0 } else { 1 };
418            hit = true;
419        }
420        "doubt" => {
421            let err_name = if u { "no-doubt" } else { "doubt" };
422            no_eq(eq_val.as_deref(), err_name, u)?;
423            let unset = u ^ true;
424            st.boolean = if unset { 0 } else { 1 };
425            hit = true;
426        }
427        "no-fear" => {
428            no_eq(eq_val.as_deref(), "no-fear", u)?;
429            st.boolean = 1;
430            hit = true;
431        }
432        "boolean" => {
433            no_eq(eq_val.as_deref(), "boolean", u)?;
434            if u {
435                st.boolean = 0;
436            } else {
437                st.boolean = st.boolean.saturating_add(1);
438            }
439            hit = true;
440        }
441        "or4" => {
442            no_eq(eq_val.as_deref(), "or4", u)?;
443            if u {
444                st.boolean &= !4;
445            } else {
446                st.boolean |= 4;
447            }
448            hit = true;
449        }
450        "neg-or4" => {
451            no_eq(eq_val.as_deref(), "neg-or4", u)?;
452            if u {
453                st.boolean |= 4;
454            } else {
455                st.boolean &= !4;
456            }
457            hit = true;
458        }
459        "integer" => {
460            let v = take_val(eq_val, argv, i, "integer")?;
461            set_int(st, &v, "integer")?;
462            hit = true;
463        }
464        "i16" => {
465            let v = take_val(eq_val, argv, i, "i16")?;
466            set_i16(st, &v)?;
467            hit = true;
468        }
469        "unsigned" => {
470            let v = take_val(eq_val, argv, i, "unsigned")?;
471            set_unsigned(st, &v)?;
472            hit = true;
473        }
474        "u16" => {
475            let v = take_val(eq_val, argv, i, "u16")?;
476            set_u16(st, &v)?;
477            hit = true;
478        }
479        "set23" => {
480            no_eq(eq_val.as_deref(), "set23", u)?;
481            st.touch_integer(if u { 0 } else { 23 }, opt("set23", false), None, u)?;
482            hit = true;
483        }
484        "mode1" => {
485            no_eq(eq_val.as_deref(), "mode1", u)?;
486            st.touch_integer(if u { 0 } else { 1 }, opt("mode1", true), None, u)?;
487            hit = true;
488        }
489        "mode2" => {
490            no_eq(eq_val.as_deref(), "mode2", u)?;
491            st.touch_integer(if u { 0 } else { 2 }, opt("mode2", true), None, u)?;
492            hit = true;
493        }
494        "mode34" => {
495            let v = take_val(eq_val, argv, i, "mode34")?;
496            if u {
497                st.touch_integer(0, opt("mode34", true), Some("0"), true)?;
498            } else if v == "3" {
499                st.touch_integer(3, opt("mode34", true), Some("3"), false)?;
500            } else if v == "4" {
501                st.touch_integer(4, opt("mode34", true), Some("4"), false)?;
502            } else {
503                return Err(ParseOptionsToolError::Fatal(format!(
504                    "error: invalid value for '--mode34': '{v}'\n"
505                )));
506            }
507            hit = true;
508        }
509        "length" => {
510            let v = take_val(eq_val, argv, i, "length")?;
511            if u {
512                return Err(ParseOptionsToolError::Fatal("error: option `no-length' isn't available\n".to_string()));
513            }
514            st.length_cb_called = true;
515            st.length_cb_arg = Some(v.clone());
516            st.length_cb_unset = false;
517            st.touch_integer(v.len() as i32, opt("length", false), None, false)?;
518            hit = true;
519        }
520        "file" => {
521            let v = take_val(eq_val, argv, i, "file")?;
522            if u {
523                st.file = None;
524            } else {
525                st.file = Some(format!("{prefix}{v}"));
526            }
527            hit = true;
528        }
529        "string" | "string2" | "st" => {
530            let v = take_val(eq_val, argv, i, "string")?;
531            if u {
532                st.string = None;
533            } else {
534                st.string = Some(v);
535            }
536            hit = true;
537        }
538        "obsolete" => {
539            no_eq(eq_val.as_deref(), "obsolete", false)?;
540            hit = true;
541        }
542        "longhelp" => {
543            no_eq(eq_val.as_deref(), "longhelp", u)?;
544            st.touch_integer(0, opt("longhelp", false), None, u)?;
545            hit = true;
546        }
547        "list" => {
548            let v = take_val(eq_val, argv, i, "list")?;
549            if u {
550                st.list.clear();
551            } else {
552                st.list.push(v);
553            }
554            hit = true;
555        }
556        "ambiguous" => {
557            no_eq(eq_val.as_deref(), "ambiguous", false)?;
558            st.ambiguous = st.ambiguous.saturating_add(1);
559            hit = true;
560        }
561        "no-ambiguous" => {
562            no_eq(eq_val.as_deref(), "no-ambiguous", false)?;
563            st.ambiguous = 0;
564            hit = true;
565        }
566        "abbrev" => {
567            if u {
568                st.abbrev = 0;
569            } else if let Some(ev) = eq_val {
570                parse_abbrev(&ev, &mut st.abbrev)?;
571            } else if *i + 1 < argv.len() {
572                let v = take_val(None, argv, i, "abbrev")?;
573                parse_abbrev(&v, &mut st.abbrev)?;
574            } else {
575                st.abbrev = 7;
576            }
577            hit = true;
578        }
579        "verbose" => {
580            no_eq(eq_val.as_deref(), "verbose", u)?;
581            if u {
582                st.verbose = 0;
583            } else {
584                st.verbose = if st.verbose < 0 { 1 } else { st.verbose + 1 };
585            }
586            hit = true;
587        }
588        "quiet" => {
589            no_eq(eq_val.as_deref(), "quiet", u)?;
590            if u {
591                st.quiet = 0;
592            } else if st.quiet <= 0 {
593                st.quiet -= 1;
594            } else {
595                st.quiet = -1;
596            }
597            hit = true;
598        }
599        "dry-run" => {
600            no_eq(eq_val.as_deref(), "dry-run", u)?;
601            st.dry_run = if u { 0 } else { 1 };
602            hit = true;
603        }
604        "alias-source" | "alias-target" => {
605            let v = take_val(eq_val, argv, i, "alias-source")?;
606            if u {
607                st.string = None;
608            } else {
609                st.string = Some(v);
610            }
611            hit = true;
612        }
613        _ => {}
614    }
615    if hit {
616        *i += 1;
617    }
618    Ok(hit)
619}
620
621fn opt(name: &'static str, is_cmdmode: bool) -> OptMeta {
622    OptMeta { name, is_cmdmode }
623}
624
625/// Long option names in `parse_long_opt` table order (for abbreviation + ambiguity).
626const LONG_NAMES: &[&str] = &[
627    "yes",
628    "no-doubt",
629    "doubt",
630    "no-fear",
631    "boolean",
632    "or4",
633    "neg-or4",
634    "integer",
635    "i16",
636    "unsigned",
637    "u16",
638    "set23",
639    "mode1",
640    "mode2",
641    "mode34",
642    "length",
643    "file",
644    "string",
645    "string2",
646    "st",
647    "obsolete",
648    "longhelp",
649    "list",
650    "ambiguous",
651    "no-ambiguous",
652    "abbrev",
653    "verbose",
654    "quiet",
655    "dry-run",
656    "expect",
657    "alias-source",
658    "alias-target",
659];
660
661fn is_alias_pair(a: &str, b: &str) -> bool {
662    (a == "alias-source" && b == "alias-target") || (a == "alias-target" && b == "alias-source")
663}
664
665fn long_abbrev(
666    st: &mut PoState,
667    s: &str,
668    _arg_end: usize,
669    flags_unset: bool,
670    eq_val: Option<String>,
671    argv: &[String],
672    i: &mut usize,
673    prefix: &str,
674) -> Result<bool, ParseOptionsToolError> {
675    let user_len = s.len();
676    if user_len == 0 {
677        return Ok(false);
678    }
679    let mut matches: Vec<&'static str> = Vec::new();
680    for &ln in LONG_NAMES {
681        let mut long_name = ln;
682        let mut opt_unset = false;
683        if let Some(x) = long_name.strip_prefix("no-") {
684            long_name = x;
685            opt_unset = true;
686        }
687        let allow_unset = !matches!(
688            ln,
689            "no-fear" | "obsolete" | "longhelp" | "ambiguous" | "no-ambiguous"
690        );
691        if (flags_unset ^ opt_unset) && !allow_unset {
692            continue;
693        }
694        if long_name.len() >= user_len && long_name.as_bytes().get(..user_len) == Some(s.as_bytes())
695        {
696            matches.push(ln);
697        }
698    }
699    matches.sort_unstable();
700    matches.dedup();
701    if matches.is_empty() {
702        return Ok(false);
703    }
704    let mut abbrev: Option<&str> = None;
705    let mut ambiguous: Option<(&str, &str)> = None;
706    for m in &matches {
707        match abbrev {
708            None => abbrev = Some(m),
709            Some(a) => {
710                if !is_alias_pair(a, m) {
711                    ambiguous = Some((a, m));
712                    break;
713                }
714                abbrev = Some(m);
715            }
716        }
717    }
718    if let Some((a, b)) = ambiguous {
719        return Err(ParseOptionsToolError::Fatal(format!(
720            "ambiguous option: {s} (could be --{a} or --{b})\n"
721        )));
722    }
723    let Some(only) = abbrev else {
724        return Ok(false);
725    };
726    let key = match only {
727        "no-doubt" => "doubt",
728        o => o,
729    };
730    long_exact(st, key, flags_unset, eq_val, argv, i, prefix)?;
731    Ok(true)
732}
733
734fn no_eq(eq: Option<&str>, name: &str, unset: bool) -> Result<(), ParseOptionsToolError> {
735    if let Some(x) = eq {
736        if !x.is_empty() || unset {
737            return Err(ParseOptionsToolError::Fatal(format!(
738                "error: option `{name}' takes no value\n"
739            )));
740        }
741    }
742    Ok(())
743}
744
745fn take_val(
746    eq_val: Option<String>,
747    argv: &[String],
748    i: &mut usize,
749    optname: &str,
750) -> Result<String, ParseOptionsToolError> {
751    if let Some(v) = eq_val {
752        return Ok(v);
753    }
754    if *i + 1 >= argv.len() {
755        return Err(ParseOptionsToolError::Fatal(format!(
756            "error: option `{optname}' requires a value\n"
757        )));
758    }
759    *i += 1;
760    Ok(argv[*i].clone())
761}
762
763fn parse_abbrev(s: &str, out: &mut i32) -> Result<(), ParseOptionsToolError> {
764    if s.is_empty() {
765        return Err(ParseOptionsToolError::Fatal("error: option `abbrev' expects a numerical value\n".to_string()));
766    }
767    let v: i32 = s.parse().map_err(|_| {
768        ParseOptionsToolError::Fatal("error: option `abbrev' expects a numerical value\n".to_string())
769    })?;
770    *out = if v != 0 && v < 4 { 4 } else { v };
771    Ok(())
772}
773
774fn set_int(st: &mut PoState, raw: &str, optname: &str) -> Result<(), ParseOptionsToolError> {
775    let (lo, hi) = int_bounds_32();
776    let opt_meta = match optname {
777        "integer" | "j" => opt("integer", false),
778        _ => {
779            return Err(ParseOptionsToolError::Fatal(format!(
780                "internal error: unknown option name for set_int: {optname}\n"
781            )));
782        }
783    };
784    match git_parse_signed(raw, hi) {
785        Ok(v) if v >= lo && v <= hi => {
786            st.touch_integer(v as i32, opt_meta, None, false)?;
787            Ok(())
788        }
789        Err(std::io::ErrorKind::InvalidData) => Err(ParseOptionsToolError::Fatal(format!(
790            "error: value {raw} for option `{optname}' not in range [{lo},{hi}]\n"
791        ))),
792        _ => Err(ParseOptionsToolError::Fatal(format!(
793            "error: option `{optname}' expects an integer value with an optional k/m/g suffix\n"
794        ))),
795    }
796}
797
798fn set_i16(st: &mut PoState, raw: &str) -> Result<(), ParseOptionsToolError> {
799    match git_parse_signed(raw, i16::MAX as i128) {
800        Ok(v) if v >= i16::MIN as i128 && v <= i16::MAX as i128 => {
801            st.i16 = v as i16;
802            Ok(())
803        }
804        Err(std::io::ErrorKind::InvalidData) | Ok(_) => Err(ParseOptionsToolError::Fatal(format!(
805            "error: value {raw} for option `i16' not in range [-32768,32767]\n"
806        ))),
807        _ => Err(ParseOptionsToolError::Fatal(
808            "error: option `i16' expects an integer value with an optional k/m/g suffix\n"
809                .to_string(),
810        )),
811    }
812}
813
814fn set_unsigned(st: &mut PoState, raw: &str) -> Result<(), ParseOptionsToolError> {
815    match git_parse_unsigned(raw, u64::MAX as u128) {
816        Ok(v) => {
817            st.unsigned_integer = v as u64;
818            Ok(())
819        }
820        Err(std::io::ErrorKind::InvalidData) => Err(ParseOptionsToolError::Fatal(format!(
821            "error: value {raw} for option `unsigned' not in range [0,{}]\n",
822            u64::MAX
823        ))),
824        _ => Err(ParseOptionsToolError::Fatal(
825            "error: option `unsigned' expects a non-negative integer value with an optional k/m/g suffix\n"
826                .to_string(),
827        )),
828    }
829}
830
831fn set_u16(st: &mut PoState, raw: &str) -> Result<(), ParseOptionsToolError> {
832    match git_parse_unsigned(raw, u16::MAX as u128) {
833        Ok(v) if v <= u16::MAX as u128 => {
834            st.u16 = v as u16;
835            Ok(())
836        }
837        _ => Err(ParseOptionsToolError::Fatal(format!(
838            "error: value {raw} for option `u16' not in range [0,65535]\n"
839        ))),
840    }
841}
842
843/// Mirrors `check_typos` in `parse-options.c` for single-dash typos (`-boolean`, …).
844fn check_typos(full_suffix: &str) -> Result<(), ParseOptionsToolError> {
845    if full_suffix.len() < 3 {
846        return Ok(());
847    }
848    if full_suffix.starts_with("no-") {
849        return Err(ParseOptionsToolError::Fatal(format!(
850            "error: did you mean `--{full_suffix}` (with two dashes)?\n"
851        )));
852    }
853    for ln in LONG_NAMES {
854        if ln.starts_with(full_suffix) {
855            return Err(ParseOptionsToolError::Fatal(format!(
856                "error: did you mean `--{full_suffix}` (with two dashes)?\n"
857            )));
858        }
859    }
860    Ok(())
861}
862
863fn parse_short(
864    st: &mut PoState,
865    argv: &[String],
866    i: usize,
867    prefix: &str,
868    _disallow_abbrev: bool,
869) -> Result<usize, ParseOptionsToolError> {
870    let arg = &argv[i];
871    let full_suffix = &arg[1..];
872    if full_suffix.is_empty() {
873        return Err(ParseOptionsToolError::Fatal(
874            "error: unknown switch\n".to_string(),
875        ));
876    }
877
878    let mut o = 0usize;
879    let mut local_i = i;
880
881    // First short option (Git calls `check_typos(arg+1)` when remainder after first match)
882    let first = full_suffix.chars().next().unwrap();
883    let flen = first.len_utf8();
884    match first {
885        'h' => {
886            print!("{PARSE_OPTIONS_HELP}");
887            return Err(ParseOptionsToolError::Help);
888        }
889        's' => {
890            let v = if full_suffix.len() > flen {
891                full_suffix[flen..].to_string()
892            } else if local_i + 1 < argv.len() {
893                local_i += 1;
894                argv[local_i].clone()
895            } else {
896                return Err(ParseOptionsToolError::Fatal("error: switch `s' requires a value\n".to_string()));
897            };
898            o = full_suffix.len();
899            st.string = Some(v);
900        }
901        'b' => {
902            st.boolean = st.boolean.saturating_add(1);
903            o += flen;
904        }
905        'i' => {
906            let tail = &full_suffix[flen..];
907            let v = if !tail.is_empty() {
908                tail.to_string()
909            } else if local_i + 1 < argv.len() {
910                local_i += 1;
911                argv[local_i].clone()
912            } else {
913                return Err(ParseOptionsToolError::Fatal("error: switch `i' requires a value\n".to_string()));
914            };
915            set_int(st, &v, "integer")?;
916            o = full_suffix.len();
917        }
918        'j' => {
919            let tail = &full_suffix[flen..];
920            let v = if !tail.is_empty() {
921                tail.to_string()
922            } else if local_i + 1 < argv.len() {
923                local_i += 1;
924                argv[local_i].clone()
925            } else {
926                return Err(ParseOptionsToolError::Fatal("error: switch `j' requires a value\n".to_string()));
927            };
928            set_int(st, &v, "j")?;
929            o = full_suffix.len();
930        }
931        'u' => {
932            let tail = &full_suffix[flen..];
933            let v = if !tail.is_empty() {
934                tail.to_string()
935            } else if local_i + 1 < argv.len() {
936                local_i += 1;
937                argv[local_i].clone()
938            } else {
939                return Err(ParseOptionsToolError::Fatal("error: switch `u' requires a value\n".to_string()));
940            };
941            set_unsigned(st, &v)?;
942            o = full_suffix.len();
943        }
944        'v' => {
945            st.verbose = if st.verbose < 0 { 1 } else { st.verbose + 1 };
946            o += flen;
947        }
948        'n' => {
949            st.dry_run = 1;
950            o += flen;
951        }
952        'q' => {
953            if st.quiet <= 0 {
954                st.quiet -= 1;
955            } else {
956                st.quiet = -1;
957            }
958            o += flen;
959        }
960        'D' => {
961            st.boolean = 1;
962            o += flen;
963        }
964        'B' => {
965            st.boolean = 1;
966            o += flen;
967        }
968        '4' => {
969            st.boolean |= 4;
970            o += flen;
971        }
972        'L' => {
973            let v = if full_suffix.len() > flen {
974                full_suffix[flen..].to_string()
975            } else if local_i + 1 < argv.len() {
976                local_i += 1;
977                argv[local_i].clone()
978            } else {
979                return Err(ParseOptionsToolError::Fatal("error: switch `L' requires a value\n".to_string()));
980            };
981            o = full_suffix.len();
982            st.length_cb_called = true;
983            st.length_cb_arg = Some(v.clone());
984            st.length_cb_unset = false;
985            st.touch_integer(v.len() as i32, opt("length", false), None, false)?;
986        }
987        'F' => {
988            let v = if full_suffix.len() > flen {
989                full_suffix[flen..].to_string()
990            } else if local_i + 1 < argv.len() {
991                local_i += 1;
992                argv[local_i].clone()
993            } else {
994                return Err(ParseOptionsToolError::Fatal("error: switch `F' requires a value\n".to_string()));
995            };
996            o = full_suffix.len();
997            st.file = Some(format!("{prefix}{v}"));
998        }
999        'A' => {
1000            let v = if full_suffix.len() > flen {
1001                full_suffix[flen..].to_string()
1002            } else if local_i + 1 < argv.len() {
1003                local_i += 1;
1004                argv[local_i].clone()
1005            } else {
1006                return Err(ParseOptionsToolError::Fatal("error: switch `A' requires a value\n".to_string()));
1007            };
1008            o = full_suffix.len();
1009            st.string = Some(v);
1010        }
1011        'Z' => {
1012            let v = if full_suffix.len() > flen {
1013                full_suffix[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("error: switch `Z' requires a value\n".to_string()));
1019            };
1020            o = full_suffix.len();
1021            st.string = Some(v);
1022        }
1023        'o' => {
1024            let v = if full_suffix.len() > flen {
1025                full_suffix[flen..].to_string()
1026            } else if local_i + 1 < argv.len() {
1027                local_i += 1;
1028                argv[local_i].clone()
1029            } else {
1030                return Err(ParseOptionsToolError::Fatal("error: switch `o' requires a value\n".to_string()));
1031            };
1032            o = full_suffix.len();
1033            st.string = Some(v);
1034        }
1035        '+' => {
1036            st.boolean = st.boolean.saturating_add(1);
1037            o += flen;
1038        }
1039        _ => {
1040            return Err(ParseOptionsToolError::Fatal(format!(
1041                "error: unknown switch `{first}'\n"
1042            )));
1043        }
1044    }
1045
1046    if o < full_suffix.len() {
1047        check_typos(full_suffix)?;
1048    }
1049
1050    while o < full_suffix.len() {
1051        let c = full_suffix[o..].chars().next().unwrap();
1052        let clen = c.len_utf8();
1053        match c {
1054            'b' => {
1055                st.boolean = st.boolean.saturating_add(1);
1056                o += clen;
1057            }
1058            'v' => {
1059                st.verbose = if st.verbose < 0 { 1 } else { st.verbose + 1 };
1060                o += clen;
1061            }
1062            'n' => {
1063                st.dry_run = 1;
1064                o += clen;
1065            }
1066            'q' => {
1067                if st.quiet <= 0 {
1068                    st.quiet -= 1;
1069                } else {
1070                    st.quiet = -1;
1071                }
1072                o += clen;
1073            }
1074            '4' => {
1075                st.boolean |= 4;
1076                o += clen;
1077            }
1078            '+' => {
1079                st.boolean = st.boolean.saturating_add(1);
1080                o += clen;
1081            }
1082            _ => {
1083                return Err(ParseOptionsToolError::Fatal(format!(
1084                    "error: unknown switch `{c}'\n"
1085                )));
1086            }
1087        }
1088    }
1089
1090    Ok(local_i + 1)
1091}