1use crate::ast::Arg;
2use crate::command::{CommandMeta, FlagValueType};
3use crate::error::{ParseError, SpanContext};
4
5pub type StrippedArgs = (Vec<(String, Arg)>, Vec<Arg>);
7
8pub fn strip_flags(args: Vec<Arg>, meta: &CommandMeta) -> Result<StrippedArgs, ParseError> {
16 if meta.flags.is_empty() {
17 return Ok((Vec::new(), args));
18 }
19
20 let mut flags = Vec::new();
21 let mut positional = Vec::new();
22 let mut iter = args.into_iter();
23 while let Some(arg) = iter.next() {
24 match &arg {
25 Arg::String(_, true) => {
27 positional.push(arg);
28 }
29 Arg::Expr(_) | Arg::Parts(_) => {
31 positional.push(arg);
32 }
33 Arg::String(s, false) if s == "--" => {
34 positional.extend(iter);
35 break;
36 }
37 Arg::String(s, false) if s.starts_with("--") => {
38 let matched = meta.flags.iter().find(|f| {
39 s == f.long
40 || s.starts_with(f.long) && s.as_bytes().get(f.long.len()) == Some(&b'=')
41 });
42 match matched {
43 Some(flag_meta) => {
44 let value = if let Some(eq_pos) = s.find('=') {
45 Arg::String(s[eq_pos + 1..].to_string(), false)
46 } else if matches!(flag_meta.value_type, FlagValueType::Flag) {
47 Arg::String("true".into(), false)
48 } else {
49 iter.next().ok_or_else(|| {
50 ParseError::validation(
51 meta.name,
52 format!(
53 "invalid syntax for command {}: {} requires a value",
54 meta.name, flag_meta.long
55 ),
56 &SpanContext::line_only(0),
57 )
58 })?
59 };
60 flags.push((flag_meta.name.to_string(), value));
61 }
62 None => {
63 return Err(ParseError::validation(
64 meta.name,
65 format!(
66 "invalid syntax for command {}: unknown flag {}",
67 meta.name, s
68 ),
69 &SpanContext::line_only(0),
70 ));
71 }
72 }
73 }
74 _ => positional.push(arg),
75 }
76 }
77 Ok((flags, positional))
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use crate::command::FlagValueType;
84
85 fn test_meta(flags: &'static [crate::command::FlagSpec]) -> CommandMeta {
86 CommandMeta {
87 name: "TEST_CMD",
88 syntax: "TEST_CMD",
89 summary: "test",
90 description: "test",
91 args: &[],
92 flags,
93 default_output: None,
94 examples: &[],
95 }
96 }
97
98 #[test]
99 fn no_flags_passes_through() {
100 let meta = test_meta(&[]);
101 let args = vec![
102 Arg::String("hello".into(), false),
103 Arg::String("world".into(), false),
104 ];
105 let (flags, pos) = strip_flags(args, &meta).unwrap();
106 assert!(flags.is_empty());
107 assert_eq!(pos.len(), 2);
108 }
109
110 #[test]
111 fn posix_terminator_stops_scanning() {
112 let meta = test_meta(&[crate::command::FlagSpec {
113 name: "hash",
114 long: "--hash",
115 value_type: FlagValueType::String,
116 required: false,
117 description: "",
118 }]);
119 let args = vec![
120 Arg::String("--".into(), false),
121 Arg::String("--hash".into(), false),
122 Arg::String("abc123".into(), false),
123 ];
124 let (flags, pos) = strip_flags(args, &meta).unwrap();
125 assert!(flags.is_empty());
126 assert_eq!(pos.len(), 2);
127 assert_eq!(pos[0].as_str(), "--hash");
128 assert_eq!(pos[1].as_str(), "abc123");
129 }
130
131 #[test]
132 fn flag_with_separate_value() {
133 let meta = test_meta(&[crate::command::FlagSpec {
134 name: "hash",
135 long: "--hash",
136 value_type: FlagValueType::String,
137 required: false,
138 description: "",
139 }]);
140 let args = vec![
141 Arg::String("--hash".into(), false),
142 Arg::String("abc123".into(), false),
143 Arg::String("path.txt".into(), false),
144 ];
145 let (flags, pos) = strip_flags(args, &meta).unwrap();
146 assert_eq!(flags.len(), 1);
147 assert_eq!(flags[0].0, "hash");
148 assert_eq!(flags[0].1.as_str(), "abc123");
149 assert_eq!(pos.len(), 1);
150 assert_eq!(pos[0].as_str(), "path.txt");
151 }
152
153 #[test]
154 fn flag_with_attached_value() {
155 let meta = test_meta(&[crate::command::FlagSpec {
156 name: "hash",
157 long: "--hash",
158 value_type: FlagValueType::String,
159 required: false,
160 description: "",
161 }]);
162 let args = vec![
163 Arg::String("--hash=abc123".into(), false),
164 Arg::String("path.txt".into(), false),
165 ];
166 let (flags, pos) = strip_flags(args, &meta).unwrap();
167 assert_eq!(flags.len(), 1);
168 assert_eq!(flags[0].0, "hash");
169 assert_eq!(flags[0].1.as_str(), "abc123");
170 assert_eq!(pos.len(), 1);
171 }
172
173 #[test]
174 fn boolean_flag() {
175 let meta = test_meta(&[crate::command::FlagSpec {
176 name: "dirty",
177 long: "--include-dirty",
178 value_type: FlagValueType::Flag,
179 required: false,
180 description: "",
181 }]);
182 let args = vec![
183 Arg::String("--include-dirty".into(), false),
184 Arg::String("rev".into(), false),
185 ];
186 let (flags, pos) = strip_flags(args, &meta).unwrap();
187 assert_eq!(flags.len(), 1);
188 assert_eq!(flags[0].0, "dirty");
189 assert_eq!(flags[0].1.as_str(), "true");
190 assert_eq!(pos.len(), 1);
191 }
192
193 #[test]
194 fn unknown_flag_rejected() {
195 let meta = test_meta(&[crate::command::FlagSpec {
196 name: "hash",
197 long: "--hash",
198 value_type: FlagValueType::String,
199 required: false,
200 description: "",
201 }]);
202 let args = vec![
203 Arg::String("--hsh".into(), false),
204 Arg::String("val".into(), false),
205 ];
206 let result = strip_flags(args, &meta);
207 assert!(result.is_err());
208 assert!(result.unwrap_err().to_string().contains("unknown flag"));
209 }
210
211 #[test]
212 fn flag_missing_value_is_error() {
213 let meta = test_meta(&[crate::command::FlagSpec {
214 name: "hash",
215 long: "--hash",
216 value_type: FlagValueType::String,
217 required: false,
218 description: "",
219 }]);
220 let args = vec![Arg::String("--hash".into(), false)];
221 let result = strip_flags(args, &meta);
222 assert!(result.is_err());
223 }
224}