1use 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
9pub type ParseOptionsStatus = i32;
11
12#[derive(Debug)]
13pub enum ParseOptionsToolError {
14 Help,
16 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
214fn 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
258pub 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 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
608const 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
830fn 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
838fn 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
859fn 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}