1use std::collections::HashSet;
2use std::ffi::OsString;
3
4use anyhow::{anyhow, bail, Context, Result};
5use clap::{CommandFactory, Parser};
6
7pub struct IgnoredUndefOkFlag {
8 pub long: String,
9}
10
11pub struct ParsedArgs<T> {
12 pub opts: T,
13 pub ignored_undefok_flags: Vec<IgnoredUndefOkFlag>,
14}
15
16fn normalize_undefok_name(name: &str) -> Result<String> {
17 let normalized = name.trim();
18 if normalized.is_empty() {
19 bail!("--undefok entries must not be empty");
20 }
21 if normalized.starts_with('-') {
22 bail!("--undefok entries must not include leading dashes");
23 }
24 if normalized.contains('=') {
25 bail!("--undefok entry {name:?} must name a flag, not include a value");
26 }
27 Ok(normalized.to_owned())
28}
29
30fn collect_configured_undefok_flags(args: &[OsString]) -> Result<HashSet<String>> {
31 let mut configured = HashSet::new();
32 let mut iter = args.iter().peekable();
33
34 while let Some(arg) = iter.next() {
35 let Some(arg_str) = arg.to_str() else {
36 continue;
37 };
38
39 let value = if let Some(value) = arg_str.strip_prefix("--undefok=") {
40 value
41 } else if arg_str == "--undefok" {
42 let next = iter
43 .next()
44 .ok_or_else(|| anyhow!("--undefok requires a comma-separated value"))?;
45 next.to_str()
46 .ok_or_else(|| anyhow!("--undefok value must be valid UTF-8"))?
47 } else {
48 continue;
49 };
50
51 for raw_name in value.split(',') {
52 configured.insert(normalize_undefok_name(raw_name)?);
53 }
54 }
55
56 Ok(configured)
57}
58
59fn filter_undefok_args(
60 args: Vec<OsString>,
61 configured_undefok_flags: &HashSet<String>,
62 active_longs: &HashSet<String>,
63) -> (Vec<OsString>, Vec<IgnoredUndefOkFlag>) {
64 let mut filtered = Vec::with_capacity(args.len());
65 let mut ignored_undefok_flags = Vec::new();
66 let mut iter = args.into_iter().peekable();
67
68 while let Some(arg) = iter.next() {
69 let Some(arg_str) = arg.to_str() else {
70 filtered.push(arg);
71 continue;
72 };
73
74 if arg_str == "--undefok" {
75 filtered.push(arg);
76 if let Some(next) = iter.next() {
77 filtered.push(next);
78 }
79 continue;
80 }
81
82 if arg_str.starts_with("--undefok=") {
83 filtered.push(arg);
84 continue;
85 }
86
87 if !arg_str.starts_with("--") || arg_str == "--" {
88 filtered.push(arg);
89 continue;
90 }
91
92 let body = &arg_str[2..];
93 let (name, has_inline_value) = match body.split_once('=') {
94 Some((name, _)) => (name, true),
95 None => (body, false),
96 };
97
98 if !configured_undefok_flags.contains(name) || active_longs.contains(name) {
99 filtered.push(arg);
100 continue;
101 }
102
103 ignored_undefok_flags.push(IgnoredUndefOkFlag {
104 long: name.to_owned(),
105 });
106
107 if !has_inline_value {
108 let should_consume_value = iter
109 .peek()
110 .and_then(|next| next.to_str())
111 .is_some_and(|next| !next.starts_with('-'));
112 if should_consume_value {
113 let _ = iter.next();
114 }
115 }
116 }
117
118 (filtered, ignored_undefok_flags)
119}
120
121fn parse_args_from<T>(raw_args: Vec<OsString>) -> Result<ParsedArgs<T>>
122where
123 T: Parser + CommandFactory,
124{
125 let configured_undefok_flags = collect_configured_undefok_flags(&raw_args)?;
126 let active_longs: HashSet<String> = T::command()
127 .get_arguments()
128 .filter_map(|arg| arg.get_long().map(str::to_owned))
129 .collect();
130
131 let (filtered_args, ignored_undefok_flags) =
132 filter_undefok_args(raw_args, &configured_undefok_flags, &active_longs);
133 let opts = T::try_parse_from(filtered_args).context("parsing command-line arguments")?;
134
135 Ok(ParsedArgs {
136 opts,
137 ignored_undefok_flags,
138 })
139}
140
141pub fn parse_args<T>() -> Result<ParsedArgs<T>>
142where
143 T: Parser + CommandFactory,
144{
145 parse_args_from(std::env::args_os().collect())
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use clap::Parser;
152
153 fn os_vec(args: &[&str]) -> Vec<OsString> {
154 args.iter().map(OsString::from).collect()
155 }
156
157 fn strings(args: Vec<OsString>) -> Vec<String> {
158 args.into_iter()
159 .map(|arg| arg.into_string().expect("test args must be valid UTF-8"))
160 .collect()
161 }
162
163 #[derive(Debug, Parser)]
164 struct NoOverlapOpts {
165 #[clap(long, value_delimiter = ',')]
166 undefok: Vec<String>,
167 #[clap(long)]
168 monitor_interval_s: Option<u64>,
169 }
170
171 #[test]
172 fn collects_inline_undefok_entries() {
173 let configured = collect_configured_undefok_flags(&os_vec(&[
174 "scx_nitosis",
175 "--undefok=reconfiguration-interval-s,rebalance-cpus-interval-s",
176 ]))
177 .expect("inline undefok should parse");
178
179 assert!(configured.contains("reconfiguration-interval-s"));
180 assert!(configured.contains("rebalance-cpus-interval-s"));
181 }
182
183 #[test]
184 fn collects_separate_undefok_value() {
185 let configured = collect_configured_undefok_flags(&os_vec(&[
186 "scx_nitosis",
187 "--undefok",
188 "reconfiguration-interval-s",
189 ]))
190 .expect("separate undefok should parse");
191
192 assert!(configured.contains("reconfiguration-interval-s"));
193 }
194
195 #[test]
196 fn filters_configured_undefok_flag_with_inline_value() {
197 let configured = collect_configured_undefok_flags(&os_vec(&[
198 "scx_nitosis",
199 "--undefok=reconfiguration-interval-s",
200 ]))
201 .expect("undefok should parse");
202
203 let (filtered, ignored) = filter_undefok_args(
204 os_vec(&[
205 "scx_nitosis",
206 "--undefok=reconfiguration-interval-s",
207 "--reconfiguration-interval-s=10",
208 "--monitor-interval-s",
209 "2",
210 ]),
211 &configured,
212 &HashSet::from([String::from("monitor-interval-s")]),
213 );
214
215 assert_eq!(
216 strings(filtered),
217 vec![
218 "scx_nitosis",
219 "--undefok=reconfiguration-interval-s",
220 "--monitor-interval-s",
221 "2"
222 ]
223 );
224 assert_eq!(ignored.len(), 1);
225 assert_eq!(ignored[0].long, "reconfiguration-interval-s");
226 }
227
228 #[test]
229 fn filters_configured_undefok_flag_with_separate_value() {
230 let configured = collect_configured_undefok_flags(&os_vec(&[
231 "scx_nitosis",
232 "--undefok=rebalance-cpus-interval-s",
233 ]))
234 .expect("undefok should parse");
235
236 let (filtered, ignored) = filter_undefok_args(
237 os_vec(&[
238 "scx_nitosis",
239 "--undefok=rebalance-cpus-interval-s",
240 "--rebalance-cpus-interval-s",
241 "5",
242 "--monitor-interval-s",
243 "2",
244 ]),
245 &configured,
246 &HashSet::from([String::from("monitor-interval-s")]),
247 );
248
249 assert_eq!(
250 strings(filtered),
251 vec![
252 "scx_nitosis",
253 "--undefok=rebalance-cpus-interval-s",
254 "--monitor-interval-s",
255 "2"
256 ]
257 );
258 assert_eq!(ignored.len(), 1);
259 assert_eq!(ignored[0].long, "rebalance-cpus-interval-s");
260 }
261
262 #[test]
263 fn missing_undefok_value_does_not_consume_next_flag() {
264 let configured = collect_configured_undefok_flags(&os_vec(&[
265 "scx_nitosis",
266 "--undefok=rebalance-cpus-interval-s",
267 ]))
268 .expect("undefok should parse");
269
270 let (filtered, ignored) = filter_undefok_args(
271 os_vec(&[
272 "scx_nitosis",
273 "--undefok=rebalance-cpus-interval-s",
274 "--rebalance-cpus-interval-s",
275 "--monitor-interval-s",
276 "2",
277 ]),
278 &configured,
279 &HashSet::from([String::from("monitor-interval-s")]),
280 );
281
282 assert_eq!(
283 strings(filtered),
284 vec![
285 "scx_nitosis",
286 "--undefok=rebalance-cpus-interval-s",
287 "--monitor-interval-s",
288 "2"
289 ]
290 );
291 assert_eq!(ignored.len(), 1);
292 }
293
294 #[test]
295 fn unknown_flags_are_left_for_clap_when_not_listed_in_undefok() {
296 let configured = collect_configured_undefok_flags(&os_vec(&[
297 "scx_nitosis",
298 "--undefok=reconfiguration-interval-s",
299 ]))
300 .expect("undefok should parse");
301
302 let (filtered, ignored) = filter_undefok_args(
303 os_vec(&[
304 "scx_nitosis",
305 "--undefok=reconfiguration-interval-s",
306 "--unknown-flag",
307 "value",
308 "--monitor-interval-s",
309 "2",
310 ]),
311 &configured,
312 &HashSet::from([String::from("monitor-interval-s")]),
313 );
314
315 assert_eq!(
316 strings(filtered),
317 vec![
318 "scx_nitosis",
319 "--undefok=reconfiguration-interval-s",
320 "--unknown-flag",
321 "value",
322 "--monitor-interval-s",
323 "2"
324 ]
325 );
326 assert!(ignored.is_empty());
327 }
328
329 #[test]
330 fn listed_flag_may_be_absent() {
331 let configured = collect_configured_undefok_flags(&os_vec(&[
332 "scx_nitosis",
333 "--undefok=reconfiguration-interval-s",
334 ]))
335 .expect("undefok should parse");
336
337 let (filtered, ignored) = filter_undefok_args(
338 os_vec(&[
339 "scx_nitosis",
340 "--undefok=reconfiguration-interval-s",
341 "--monitor-interval-s",
342 "2",
343 ]),
344 &configured,
345 &HashSet::from([String::from("monitor-interval-s")]),
346 );
347
348 assert_eq!(
349 strings(filtered),
350 vec![
351 "scx_nitosis",
352 "--undefok=reconfiguration-interval-s",
353 "--monitor-interval-s",
354 "2"
355 ]
356 );
357 assert!(ignored.is_empty());
358 }
359
360 #[test]
361 fn active_flag_may_be_listed_in_undefok_without_being_filtered() {
362 let configured = collect_configured_undefok_flags(&os_vec(&[
363 "scx_nitosis",
364 "--undefok=monitor-interval-s",
365 ]))
366 .expect("undefok should parse");
367
368 let (filtered, ignored) = filter_undefok_args(
369 os_vec(&[
370 "scx_nitosis",
371 "--undefok=monitor-interval-s",
372 "--monitor-interval-s",
373 "2",
374 ]),
375 &configured,
376 &HashSet::from([String::from("monitor-interval-s")]),
377 );
378
379 assert_eq!(
380 strings(filtered),
381 vec![
382 "scx_nitosis",
383 "--undefok=monitor-interval-s",
384 "--monitor-interval-s",
385 "2"
386 ]
387 );
388 assert!(ignored.is_empty());
389 }
390
391 #[test]
392 fn rejects_empty_undefok_entry() {
393 let err = collect_configured_undefok_flags(&os_vec(&[
394 "scx_nitosis",
395 "--undefok=reconfiguration-interval-s,",
396 ]))
397 .expect_err("empty undefok entry should fail");
398
399 assert!(
400 err.to_string()
401 .contains("--undefok entries must not be empty"),
402 "unexpected error: {err}"
403 );
404 }
405
406 #[test]
407 fn rejects_undefok_entry_with_leading_dashes() {
408 let err = collect_configured_undefok_flags(&os_vec(&[
409 "scx_nitosis",
410 "--undefok=--reconfiguration-interval-s",
411 ]))
412 .expect_err("leading dashes in undefok entries should fail");
413
414 assert!(
415 err.to_string()
416 .contains("--undefok entries must not include leading dashes"),
417 "unexpected error: {err}"
418 );
419 }
420
421 #[test]
422 fn parse_from_keeps_active_flag_functional_when_listed_in_undefok() {
423 let parsed = parse_args_from::<NoOverlapOpts>(os_vec(&[
424 "scx_nitosis",
425 "--undefok=monitor-interval-s",
426 "--monitor-interval-s",
427 "2",
428 ]))
429 .expect("active flags listed in undefok should still parse");
430
431 assert_eq!(parsed.opts.undefok, vec!["monitor-interval-s"]);
432 assert_eq!(parsed.opts.monitor_interval_s, Some(2));
433 assert!(parsed.ignored_undefok_flags.is_empty());
434 }
435
436 #[test]
437 fn parse_from_ignores_and_records_unknown_flag_listed_in_undefok() {
438 let parsed = parse_args_from::<NoOverlapOpts>(os_vec(&[
439 "scx_nitosis",
440 "--undefok=reconfiguration-interval-s",
441 "--reconfiguration-interval-s",
442 "10",
443 "--monitor-interval-s",
444 "2",
445 ]))
446 .expect("unknown flags listed in undefok should be ignored");
447
448 assert_eq!(parsed.opts.undefok, vec!["reconfiguration-interval-s"]);
449 assert_eq!(parsed.opts.monitor_interval_s, Some(2));
450 assert_eq!(parsed.ignored_undefok_flags.len(), 1);
451 assert_eq!(
452 parsed.ignored_undefok_flags[0].long,
453 "reconfiguration-interval-s"
454 );
455 }
456
457 #[test]
458 fn parse_from_rejects_unknown_flag_not_listed_in_undefok() {
459 let err = match parse_args_from::<NoOverlapOpts>(os_vec(&[
460 "scx_nitosis",
461 "--unknown-flag",
462 "10",
463 ])) {
464 Ok(_) => panic!("unknown flags not listed in undefok should still fail"),
465 Err(err) => err,
466 };
467
468 assert!(
469 err.chain().any(|cause| cause
470 .to_string()
471 .contains("unexpected argument '--unknown-flag'")),
472 "unexpected error: {err}"
473 );
474 }
475}