Skip to main content

xyz_rust/cli/
parse.rs

1// flag 定义与解析:长短名、= 形式、bool 无值形式、-- 之后全部转
2// 位置参数。未知 flag 报错(用法错误 → 退出码 2)。
3
4use crate::errors;
5use crate::spec::field::FieldMeta;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum FlagKind {
9    Str,
10    Bool,
11    Slice,
12}
13
14/// 描述一个长/短 flag 的定义与取值方式。
15#[derive(Debug, Clone)]
16pub struct FlagDef {
17    pub long: String,
18    pub short: Option<char>,
19    pub kind: FlagKind,
20    pub field: FieldMeta,
21}
22
23/// 一次解析中某个 flag 的取值状态。
24#[derive(Debug, Clone, Default)]
25pub struct FlagVal {
26    pub seen: bool,
27    pub str: String,
28    pub list: Vec<String>,
29    pub boolean: bool,
30}
31
32/// 解析 args 中的 flag,返回每个 flag 的取值与剩余位置参数。
33pub fn parse_flags(
34    defs: &[FlagDef],
35    args: &[String],
36) -> errors::Result<(Vec<FlagVal>, Vec<String>)> {
37    let mut long_idx: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
38    let mut short_idx: std::collections::HashMap<char, usize> = std::collections::HashMap::new();
39    for (i, d) in defs.iter().enumerate() {
40        long_idx.insert(d.long.as_str(), i);
41        if let Some(sh) = d.short {
42            short_idx.insert(sh, i);
43        }
44    }
45    let mut fvals: Vec<FlagVal> = defs.iter().map(|_| FlagVal::default()).collect();
46    let mut pos: Vec<String> = Vec::new();
47    let mut i = 0;
48    while i < args.len() {
49        let a = &args[i];
50        if a == "--" {
51            pos.extend_from_slice(&args[i + 1..]);
52            break;
53        }
54        if let Some(rest) = a.strip_prefix("--") {
55            let (name, has_val, val) = match rest.split_once('=') {
56                Some((n, v)) => (n, true, v.to_string()),
57                None => (rest, false, String::new()),
58            };
59            let Some(&di) = long_idx.get(name) else {
60                return Err(errors::Error::new(
61                    errors::Kind::InvalidInput,
62                    format!("unknown flag: --{name}"),
63                ));
64            };
65            let fv = &mut fvals[di];
66            if defs[di].kind == FlagKind::Bool {
67                let mut b = true;
68                if has_val {
69                    b = crate::spec::scalar::parse_bool(&val).map_err(|_| {
70                        errors::Error::new(
71                            errors::Kind::InvalidInput,
72                            format!("invalid boolean value {val:?} for --{name}"),
73                        )
74                    })?;
75                }
76                fv.seen = true;
77                fv.boolean = b;
78                i += 1;
79                continue;
80            }
81            let val = if !has_val {
82                if i + 1 >= args.len() {
83                    return Err(errors::Error::new(
84                        errors::Kind::InvalidInput,
85                        format!("flag needs an argument: --{name}"),
86                    ));
87                }
88                i += 1;
89                args[i].clone()
90            } else {
91                val
92            };
93            fv.seen = true;
94            if defs[di].kind == FlagKind::Slice {
95                fv.list.push(val);
96            } else {
97                fv.str = val;
98            }
99            i += 1;
100            continue;
101        }
102        if a.starts_with('-') && a.len() > 1 {
103            let chars: Vec<char> = a.chars().collect();
104            let sh = chars[1];
105            let Some(&di) = short_idx.get(&sh) else {
106                return Err(errors::Error::new(
107                    errors::Kind::InvalidInput,
108                    format!("unknown shorthand flag: -{sh}"),
109                ));
110            };
111            let fv = &mut fvals[di];
112            let mut rest: String = chars.iter().skip(2).collect();
113            if let Some(stripped) = rest.strip_prefix('=') {
114                rest = stripped.to_string();
115            }
116            if defs[di].kind == FlagKind::Bool {
117                if rest.is_empty() {
118                    fv.seen = true;
119                    fv.boolean = true;
120                    i += 1;
121                    continue;
122                }
123                let b = crate::spec::scalar::parse_bool(&rest).map_err(|_| {
124                    errors::Error::new(
125                        errors::Kind::InvalidInput,
126                        format!("invalid boolean value {rest:?} for -{sh}"),
127                    )
128                })?;
129                fv.seen = true;
130                fv.boolean = b;
131                i += 1;
132                continue;
133            }
134            let rest = if rest.is_empty() {
135                if i + 1 >= args.len() {
136                    return Err(errors::Error::new(
137                        errors::Kind::InvalidInput,
138                        format!("flag needs an argument: -{sh}"),
139                    ));
140                }
141                i += 1;
142                args[i].clone()
143            } else {
144                rest
145            };
146            fv.seen = true;
147            if defs[di].kind == FlagKind::Slice {
148                fv.list.push(rest);
149            } else {
150                fv.str = rest;
151            }
152            i += 1;
153            continue;
154        }
155        pos.push(a.clone());
156        i += 1;
157    }
158    Ok((fvals, pos))
159}
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::spec::field::{FieldKind, FieldMeta};
164
165    fn str_def(name: &str, short: Option<char>) -> FlagDef {
166        FlagDef {
167            long: name.to_string(),
168            short,
169            kind: FlagKind::Str,
170            field: fm(name),
171        }
172    }
173    fn bool_def(name: &str, short: Option<char>) -> FlagDef {
174        FlagDef {
175            long: name.to_string(),
176            short,
177            kind: FlagKind::Bool,
178            field: fm(name),
179        }
180    }
181    fn slice_def(name: &str, short: Option<char>) -> FlagDef {
182        FlagDef {
183            long: name.to_string(),
184            short,
185            kind: FlagKind::Slice,
186            field: fm(name),
187        }
188    }
189    fn fm(name: &str) -> FieldMeta {
190        FieldMeta {
191            name: name.to_string(),
192            json_name: name.to_string(),
193            kind: FieldKind::String,
194            ..Default::default()
195        }
196    }
197    fn args(v: &[&str]) -> Vec<String> {
198        v.iter().map(|s| s.to_string()).collect()
199    }
200
201    #[test]
202    fn long_short_and_eq_forms() {
203        let defs = vec![str_def("query", Some('q')), bool_def("verbose", Some('v'))];
204        let (vals, pos) =
205            parse_flags(&defs, &args(&["--query", "x", "-v", "--query=y", "p"])).unwrap();
206        assert!(vals[0].seen);
207        assert_eq!(vals[0].str, "y"); // 后值覆盖
208        assert!(vals[1].boolean);
209        assert_eq!(pos, vec!["p".to_string()]);
210    }
211
212    #[test]
213    fn slice_accumulates() {
214        let defs = vec![slice_def("tags", Some('t'))];
215        let (vals, _) = parse_flags(&defs, &args(&["--tags", "a", "-t", "b"])).unwrap();
216        assert_eq!(vals[0].list, vec!["a", "b"]);
217    }
218
219    #[test]
220    fn bool_with_value() {
221        let defs = vec![bool_def("v", None)];
222        let (vals, _) = parse_flags(&defs, &args(&["--v=false"])).unwrap();
223        assert!(!vals[0].boolean);
224        let (vals, _) = parse_flags(&defs, &args(&["--v"])).unwrap();
225        assert!(vals[0].boolean);
226    }
227
228    #[test]
229    fn double_dash_stops_parsing() {
230        let defs = vec![str_def("a", None)];
231        let (vals, pos) = parse_flags(&defs, &args(&["--a", "1", "--", "--a", "2"])).unwrap();
232        assert_eq!(vals[0].str, "1");
233        assert_eq!(pos, vec!["--a".to_string(), "2".to_string()]);
234    }
235
236    #[test]
237    fn unknown_flags_error() {
238        let defs = vec![str_def("a", Some('a'))];
239        let err = parse_flags(&defs, &args(&["--nope"])).unwrap_err();
240        assert!(err.to_string().contains("unknown flag: --nope"), "{err}");
241        let err = parse_flags(&defs, &args(&["-z"])).unwrap_err();
242        assert!(
243            err.to_string().contains("unknown shorthand flag: -z"),
244            "{err}"
245        );
246        let err = parse_flags(&defs, &args(&["--a"])).unwrap_err();
247        assert!(
248            err.to_string().contains("flag needs an argument: --a"),
249            "{err}"
250        );
251    }
252
253    #[test]
254    fn shorthand_with_attached_value() {
255        let defs = vec![str_def("name", Some('n'))];
256        let (vals, _) = parse_flags(&defs, &args(&["-nalice"])).unwrap();
257        assert_eq!(vals[0].str, "alice");
258        let (vals, _) = parse_flags(&defs, &args(&["-n=alice"])).unwrap();
259        assert_eq!(vals[0].str, "alice");
260    }
261}