Skip to main content

grit_lib/parse_options_test_tool/
sub_cmd.rs

1//! `test-tool parse-subcommand` — mirrors `cmd__parse_subcommand` / `parse_subcommand__cmd`.
2
3use super::parse_options_cmd::ParseOptionsToolError;
4
5const KEEP_DASHDASH: u32 = 1 << 0;
6const STOP_AT_NON_OPTION: u32 = 1 << 1;
7const KEEP_ARGV0: u32 = 1 << 2;
8const KEEP_UNKNOWN_OPT: u32 = 1 << 3;
9const NO_INTERNAL_HELP: u32 = 1 << 4;
10const SUBCOMMAND_OPTIONAL: u32 = 1 << 7;
11
12#[derive(Clone, Copy)]
13enum Subcmd {
14    One,
15    Two,
16}
17
18/// `test-tool parse-subcommand` — mirrors `cmd__parse_subcommand`.
19pub fn run_parse_subcommand(args: &[String]) -> Result<i32, ParseOptionsToolError> {
20    if args.len() < 2 {
21        return Err(fatal_need_subcommand());
22    }
23    let mut i = 1usize;
24    let mut test_flags: u32 = 0;
25    while i < args.len() {
26        let a = &args[i];
27        if !a.starts_with("--") {
28            break;
29        }
30        match a.as_str() {
31            "--keep-dashdash" => test_flags |= KEEP_DASHDASH,
32            "--stop-at-non-option" => test_flags |= STOP_AT_NON_OPTION,
33            "--keep-argv0" => test_flags |= KEEP_ARGV0,
34            "--keep-unknown-opt" => test_flags |= KEEP_UNKNOWN_OPT,
35            "--no-internal-help" => test_flags |= NO_INTERNAL_HELP,
36            "--subcommand-optional" => test_flags |= SUBCOMMAND_OPTIONAL,
37            _ => {
38                return Err(ParseOptionsToolError::Fatal(format!(
39                    "error: unknown option `{a}'\n"
40                )));
41            }
42        }
43        i += 1;
44    }
45    if args.get(i).map(|s| s.as_str()) != Some("cmd") {
46        return Err(fatal_need_subcommand());
47    }
48
49    if (test_flags & STOP_AT_NON_OPTION) != 0 {
50        return Err(ParseOptionsToolError::Bug(
51            "BUG: parse-options.c:767: subcommands are incompatible with PARSE_OPT_STOP_AT_NON_OPTION\n"
52                .to_string(),
53        ));
54    }
55    if (test_flags & KEEP_UNKNOWN_OPT) != 0 && (test_flags & SUBCOMMAND_OPTIONAL) == 0 {
56        return Err(ParseOptionsToolError::Bug(
57            "BUG: parse-options.c:770: subcommands are incompatible with PARSE_OPT_KEEP_UNKNOWN_OPT unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\n"
58                .to_string(),
59        ));
60    }
61    if (test_flags & KEEP_DASHDASH) != 0 && (test_flags & SUBCOMMAND_OPTIONAL) == 0 {
62        return Err(ParseOptionsToolError::Bug(
63            "BUG: parse-options.c:772: subcommands are incompatible with PARSE_OPT_KEEP_DASHDASH unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\n"
64                .to_string(),
65        ));
66    }
67
68    parse_subcommand_inner(&args[i..], test_flags)
69}
70
71fn fatal_need_subcommand() -> ParseOptionsToolError {
72    ParseOptionsToolError::Fatal(
73        "error: need a subcommand\nusage: test-tool parse-subcommand [flag-options] cmd <subcommand>\n"
74            .to_string(),
75    )
76}
77
78fn parse_int(s: &str) -> Result<i32, ParseOptionsToolError> {
79    s.parse().map_err(|_| {
80        ParseOptionsToolError::Fatal("error: option `opt' expects a numerical value\n".to_string())
81    })
82}
83
84fn parse_subcommand_inner(argv: &[String], flags: u32) -> Result<i32, ParseOptionsToolError> {
85    if argv.is_empty() || argv[0] != "cmd" {
86        return Err(fatal_need_subcommand());
87    }
88
89    let keep_dashdash = flags & KEEP_DASHDASH != 0;
90    let keep_argv0 = flags & KEEP_ARGV0 != 0;
91    let keep_unknown = flags & KEEP_UNKNOWN_OPT != 0;
92    let subcommand_optional = flags & SUBCOMMAND_OPTIONAL != 0;
93    let internal_help = flags & NO_INTERNAL_HELP == 0;
94
95    let total_after_cmd = argv.len().saturating_sub(1);
96    let mut opt: i32 = 0;
97    let mut i = 1usize;
98
99    while i < argv.len() {
100        let arg = &argv[i];
101
102        if internal_help && total_after_cmd == 1 && arg == "-h" {
103            return Err(ParseOptionsToolError::Help);
104        }
105
106        if total_after_cmd == 1 && arg == "--git-completion-helper" {
107            println!("subcmd-one subcmd-two --opt= --no-opt");
108            return Ok(0);
109        }
110
111        if arg == "-" || !arg.starts_with('-') {
112            return match_dashless(arg, opt, &argv[i..], subcommand_optional, keep_argv0);
113        }
114
115        if arg == "--" {
116            if keep_dashdash {
117                return dispatch_subcmd_one(opt, &argv[i..], keep_argv0);
118            }
119            i += 1;
120            return finish_after_double_dash(opt, &argv[i..], subcommand_optional, keep_argv0);
121        }
122
123        if arg == "--end-of-options" {
124            if !keep_unknown {
125                i += 1;
126                return finish_after_double_dash(opt, &argv[i..], subcommand_optional, keep_argv0);
127            }
128            return dispatch_subcmd_one(opt, &argv[i..], keep_argv0);
129        }
130
131        if arg.starts_with("--") {
132            let name = arg.strip_prefix("--").unwrap_or(arg.as_str());
133            if internal_help && (name == "help" || name == "help-all") {
134                return Err(ParseOptionsToolError::Help);
135            }
136            if let Some(rest) = name.strip_prefix("opt=") {
137                opt = parse_int(rest)?;
138                i += 1;
139                continue;
140            }
141            if name == "opt" {
142                i += 1;
143                let v = argv.get(i).ok_or_else(|| {
144                    ParseOptionsToolError::Fatal("error: option `opt' requires a value\n".to_string())
145                })?;
146                opt = parse_int(v)?;
147                i += 1;
148                continue;
149            }
150            if subcommand_optional && keep_unknown {
151                return dispatch_subcmd_one(opt, &argv[i..], keep_argv0);
152            }
153            return Err(ParseOptionsToolError::Fatal(format!(
154                "error: unknown option `{name}'\nusage: test-tool parse-subcommand [flag-options] cmd <subcommand>\n"
155            )));
156        }
157
158        let body = &arg[1..];
159        if let Some(rest) = body.strip_prefix('o') {
160            if rest.is_empty() {
161                i += 1;
162                let v = argv.get(i).ok_or_else(|| {
163                    ParseOptionsToolError::Fatal("error: switch `o' requires a value\n".to_string())
164                })?;
165                opt = parse_int(v)?;
166                i += 1;
167            } else {
168                opt = parse_int(rest)?;
169                i += 1;
170            }
171            continue;
172        }
173        if internal_help && (body == "h" || body.starts_with('h')) {
174            return Err(ParseOptionsToolError::Help);
175        }
176        if subcommand_optional && keep_unknown {
177            return dispatch_subcmd_one(opt, &argv[i..], keep_argv0);
178        }
179        let c = body.chars().next().unwrap_or('?');
180        return Err(ParseOptionsToolError::Fatal(format!(
181            "error: unknown switch `{c}'\nusage: test-tool parse-subcommand [flag-options] cmd <subcommand>\n"
182        )));
183    }
184
185    if !subcommand_optional {
186        return Err(fatal_need_subcommand());
187    }
188    dispatch_subcmd_one(opt, &[], keep_argv0)
189}
190
191fn finish_after_double_dash(
192    opt: i32,
193    rest: &[String],
194    subcommand_optional: bool,
195    keep_argv0: bool,
196) -> Result<i32, ParseOptionsToolError> {
197    if subcommand_optional {
198        dispatch_subcmd_one(opt, rest, keep_argv0)
199    } else {
200        Err(fatal_need_subcommand())
201    }
202}
203
204fn match_dashless(
205    arg: &str,
206    opt: i32,
207    rest: &[String],
208    subcommand_optional: bool,
209    keep_argv0: bool,
210) -> Result<i32, ParseOptionsToolError> {
211    match arg {
212        "subcmd-one" => dispatch_subcmd(Subcmd::One, opt, rest, keep_argv0),
213        "subcmd-two" => dispatch_subcmd(Subcmd::Two, opt, rest, keep_argv0),
214        _ => {
215            if subcommand_optional {
216                dispatch_subcmd_one(opt, rest, keep_argv0)
217            } else {
218                Err(ParseOptionsToolError::Fatal(format!(
219                    "error: unknown subcommand: `{arg}'\nusage: test-tool parse-subcommand [flag-options] cmd <subcommand>\n"
220                )))
221            }
222        }
223    }
224}
225
226fn dispatch_subcmd(
227    sub: Subcmd,
228    opt: i32,
229    rest: &[String],
230    keep_argv0: bool,
231) -> Result<i32, ParseOptionsToolError> {
232    println!("opt: {opt}");
233    match sub {
234        Subcmd::One => {
235            println!("fn: subcmd_one");
236            print_args_maybe_keep_argv0(rest, keep_argv0);
237        }
238        Subcmd::Two => {
239            println!("fn: subcmd_two");
240            print_args_maybe_keep_argv0(rest, keep_argv0);
241        }
242    }
243    Ok(0)
244}
245
246fn dispatch_subcmd_one(
247    opt: i32,
248    rest: &[String],
249    keep_argv0: bool,
250) -> Result<i32, ParseOptionsToolError> {
251    println!("opt: {opt}");
252    println!("fn: subcmd_one");
253    print_args_maybe_keep_argv0(rest, keep_argv0);
254    Ok(0)
255}
256
257fn print_args_maybe_keep_argv0(rest: &[String], keep_argv0: bool) {
258    if keep_argv0 {
259        println!("arg {:02}: cmd", 0);
260        for (idx, a) in rest.iter().enumerate() {
261            println!("arg {:02}: {a}", idx + 1);
262        }
263    } else {
264        for (idx, a) in rest.iter().enumerate() {
265            println!("arg {:02}: {a}", idx);
266        }
267    }
268}