1pub fn assert_sorted(cmd: &clap::Command) {
39 assert_sorted_with_path(cmd, vec![]);
40}
41
42fn assert_sorted_with_path(cmd: &clap::Command, parent_path: Vec<&str>) {
43 let mut current_path = parent_path.clone();
44 current_path.push(cmd.get_name());
45
46 let subcommands: Vec<_> = cmd.get_subcommands().map(|s| s.get_name()).collect();
48
49 if !subcommands.is_empty() {
50 let mut sorted = subcommands.clone();
51 sorted.sort();
52
53 if subcommands != sorted {
54 panic!(
55 "Subcommands in '{}' are not sorted alphabetically!\nActual order: {:?}\nExpected order: {:?}",
56 current_path.join(" "),
57 subcommands,
58 sorted
59 );
60 }
61 }
62
63 assert_arguments_sorted_with_path(cmd, ¤t_path);
65
66 for subcmd in cmd.get_subcommands() {
68 assert_sorted_with_path(subcmd, current_path.clone());
69 }
70}
71
72pub fn is_sorted(cmd: &clap::Command) -> Result<(), String> {
92 is_sorted_with_path(cmd, vec![])
93}
94
95fn is_sorted_with_path(cmd: &clap::Command, parent_path: Vec<&str>) -> Result<(), String> {
96 let mut current_path = parent_path.clone();
97 current_path.push(cmd.get_name());
98
99 let subcommands: Vec<_> = cmd.get_subcommands().map(|s| s.get_name()).collect();
101
102 if !subcommands.is_empty() {
103 let mut sorted = subcommands.clone();
104 sorted.sort();
105
106 if subcommands != sorted {
107 return Err(format!(
108 "Subcommands in '{}' are not sorted alphabetically!\nActual order: {:?}\nExpected order: {:?}",
109 current_path.join(" "),
110 subcommands,
111 sorted
112 ));
113 }
114 }
115
116 is_arguments_sorted_with_path(cmd, ¤t_path)?;
118
119 for subcmd in cmd.get_subcommands() {
121 is_sorted_with_path(subcmd, current_path.clone())?;
122 }
123
124 Ok(())
125}
126
127fn assert_arguments_sorted_with_path(cmd: &clap::Command, path: &[&str]) {
129 if let Err(msg) = is_arguments_sorted_with_path(cmd, path) {
130 panic!("{}", msg);
131 }
132}
133
134fn is_arguments_sorted_with_path(cmd: &clap::Command, path: &[&str]) -> Result<(), String> {
136 let args: Vec<_> = cmd.get_arguments().collect();
137
138 let mut positional = Vec::new();
139 let mut with_short = Vec::new();
140 let mut long_only = Vec::new();
141
142 for arg in &args {
143 if arg.is_positional() {
144 positional.push(*arg);
145 } else if arg.get_short().is_some() {
146 with_short.push(*arg);
147 } else if arg.get_long().is_some() {
148 long_only.push(*arg);
149 }
150 }
151
152 let with_short_shorts: Vec<char> = with_short.iter().filter_map(|a| a.get_short()).collect();
156 let mut sorted_shorts = with_short_shorts.clone();
157 sorted_shorts.sort_by(|a, b| {
158 let a_lower = a.to_ascii_lowercase();
159 let b_lower = b.to_ascii_lowercase();
160 match a_lower.cmp(&b_lower) {
161 std::cmp::Ordering::Equal => {
162 if a.is_lowercase() && b.is_uppercase() {
164 std::cmp::Ordering::Less
165 } else if a.is_uppercase() && b.is_lowercase() {
166 std::cmp::Ordering::Greater
167 } else {
168 std::cmp::Ordering::Equal
169 }
170 }
171 other => other,
172 }
173 });
174
175 if with_short_shorts != sorted_shorts {
176 let current: Vec<String> = with_short
177 .iter()
178 .map(|a| format!("-{}", a.get_short().unwrap()))
179 .collect();
180 let mut sorted_args = with_short.clone();
181 sorted_args.sort_by(|a, b| {
182 let a_char = a.get_short().unwrap();
183 let b_char = b.get_short().unwrap();
184 let a_lower = a_char.to_ascii_lowercase();
185 let b_lower = b_char.to_ascii_lowercase();
186 match a_lower.cmp(&b_lower) {
187 std::cmp::Ordering::Equal => {
188 if a_char.is_lowercase() && b_char.is_uppercase() {
189 std::cmp::Ordering::Less
190 } else if a_char.is_uppercase() && b_char.is_lowercase() {
191 std::cmp::Ordering::Greater
192 } else {
193 std::cmp::Ordering::Equal
194 }
195 }
196 other => other,
197 }
198 });
199 let expected: Vec<String> = sorted_args
200 .iter()
201 .map(|a| format!("-{}", a.get_short().unwrap()))
202 .collect();
203
204 return Err(format!(
205 "Flags with short options in '{}' are not sorted!\nActual: {:?}\nExpected: {:?}",
206 path.join(" "),
207 current,
208 expected
209 ));
210 }
211
212 let long_only_longs: Vec<&str> = long_only.iter().filter_map(|a| a.get_long()).collect();
214 let mut sorted_longs = long_only_longs.clone();
215 sorted_longs.sort_unstable();
216
217 if long_only_longs != sorted_longs {
218 let current: Vec<String> = long_only_longs.iter().map(|l| format!("--{}", l)).collect();
219 let expected: Vec<String> = sorted_longs.iter().map(|l| format!("--{}", l)).collect();
220
221 return Err(format!(
222 "Long-only flags in '{}' are not sorted!\nActual: {:?}\nExpected: {:?}",
223 path.join(" "),
224 current,
225 expected
226 ));
227 }
228
229 Ok(())
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use clap::{Command, CommandFactory, Parser, Subcommand};
242
243 #[test]
244 fn test_sorted_subcommands() {
245 let cmd = Command::new("test")
246 .subcommand(Command::new("add"))
247 .subcommand(Command::new("delete"))
248 .subcommand(Command::new("list"));
249
250 assert_sorted(&cmd);
251 }
252
253 #[test]
254 #[should_panic(expected = "are not sorted alphabetically")]
255 fn test_unsorted_subcommands() {
256 let cmd = Command::new("test")
257 .subcommand(Command::new("list"))
258 .subcommand(Command::new("add"))
259 .subcommand(Command::new("delete"));
260
261 assert_sorted(&cmd);
262 }
263
264 #[test]
265 fn test_is_sorted_ok() {
266 let cmd = Command::new("test")
267 .subcommand(Command::new("add"))
268 .subcommand(Command::new("delete"))
269 .subcommand(Command::new("list"));
270
271 assert!(is_sorted(&cmd).is_ok());
272 }
273
274 #[test]
275 fn test_is_sorted_err() {
276 let cmd = Command::new("test")
277 .subcommand(Command::new("list"))
278 .subcommand(Command::new("add"));
279
280 assert!(is_sorted(&cmd).is_err());
281 }
282
283 #[test]
284 fn test_no_subcommands() {
285 let cmd = Command::new("test");
286 assert_sorted(&cmd);
287 assert!(is_sorted(&cmd).is_ok());
288 }
289
290 #[test]
291 fn test_with_derive_sorted() {
292 #[derive(Parser)]
293 struct Cli {
294 #[command(subcommand)]
295 command: Commands,
296 }
297
298 #[derive(Subcommand)]
299 enum Commands {
300 Add,
301 Delete,
302 List,
303 }
304
305 let cmd = Cli::command();
306 assert_sorted(&cmd);
307 }
308
309 #[test]
310 #[should_panic(expected = "are not sorted alphabetically")]
311 fn test_with_derive_unsorted() {
312 #[derive(Parser)]
313 struct Cli {
314 #[command(subcommand)]
315 command: Commands,
316 }
317
318 #[derive(Subcommand)]
319 enum Commands {
320 List,
321 Add,
322 Delete,
323 }
324
325 let cmd = Cli::command();
326 assert_sorted(&cmd);
327 }
328
329 #[test]
332 fn test_arguments_correctly_sorted() {
333 use clap::{Arg, ArgAction};
334
335 let cmd = Command::new("test")
336 .arg(Arg::new("file")) .arg(
338 Arg::new("debug")
339 .short('d')
340 .long("debug")
341 .action(ArgAction::SetTrue),
342 )
343 .arg(Arg::new("output").short('o').long("output"))
344 .arg(
345 Arg::new("verbose")
346 .short('v')
347 .long("verbose")
348 .action(ArgAction::SetTrue),
349 )
350 .arg(Arg::new("config").long("config"))
351 .arg(
352 Arg::new("no-color")
353 .long("no-color")
354 .action(ArgAction::SetTrue),
355 );
356
357 assert_sorted(&cmd);
358 }
359
360 #[test]
361 #[should_panic(expected = "Flags with short options")]
362 fn test_short_flags_unsorted() {
363 use clap::Arg;
364
365 let cmd = Command::new("test")
366 .arg(Arg::new("verbose").short('v').long("verbose"))
367 .arg(Arg::new("debug").short('d').long("debug"));
368
369 assert_sorted(&cmd);
370 }
371
372 #[test]
373 #[should_panic(expected = "Long-only flags")]
374 fn test_long_only_unsorted() {
375 use clap::{Arg, ArgAction};
376
377 let cmd = Command::new("test")
378 .arg(Arg::new("zebra").long("zebra").action(ArgAction::SetTrue))
379 .arg(Arg::new("alpha").long("alpha").action(ArgAction::SetTrue));
380
381 assert_sorted(&cmd);
382 }
383
384 #[test]
385 fn test_positional_order_not_enforced() {
386 let cmd = Command::new("test")
388 .arg(clap::Arg::new("second"))
389 .arg(clap::Arg::new("first"));
390
391 assert_sorted(&cmd);
392 }
393
394 #[test]
395 fn test_is_sorted_ok_with_args() {
396 use clap::Arg;
397
398 let cmd = Command::new("test")
399 .arg(Arg::new("file"))
400 .arg(Arg::new("output").short('o').long("output"))
401 .arg(Arg::new("config").long("config"))
402 .subcommand(Command::new("add"))
403 .subcommand(Command::new("delete"));
404
405 assert!(is_sorted(&cmd).is_ok());
406 }
407
408 #[test]
409 fn test_is_sorted_err_args() {
410 use clap::Arg;
411
412 let cmd = Command::new("test")
413 .arg(Arg::new("zebra").short('z').long("zebra"))
414 .arg(Arg::new("alpha").short('a').long("alpha"));
415
416 assert!(is_sorted(&cmd).is_err());
417 }
418
419 #[test]
420 fn test_recursive_subcommand_args() {
421 use clap::{Arg, ArgAction};
422
423 let cmd = Command::new("test")
424 .arg(
425 Arg::new("verbose")
426 .short('v')
427 .long("verbose")
428 .action(ArgAction::SetTrue),
429 )
430 .subcommand(
431 Command::new("sub")
432 .arg(
433 Arg::new("debug")
434 .short('d')
435 .long("debug")
436 .action(ArgAction::SetTrue),
437 )
438 .arg(Arg::new("output").short('o').long("output")),
439 );
440
441 assert_sorted(&cmd);
442 }
443
444 #[test]
445 #[should_panic(expected = "Flags with short options")]
446 fn test_recursive_subcommand_args_fails() {
447 use clap::Arg;
448
449 let cmd = Command::new("test").subcommand(
450 Command::new("sub")
451 .arg(Arg::new("output").short('o').long("output"))
452 .arg(Arg::new("debug").short('d').long("debug")),
453 );
454
455 assert_sorted(&cmd);
456 }
457
458 #[test]
459 fn test_global_flags_not_checked_in_subcommands() {
460 use clap::{Arg, ArgAction};
461
462 let cmd = Command::new("test")
464 .arg(
465 Arg::new("verbose")
466 .short('v')
467 .long("verbose")
468 .global(true)
469 .action(ArgAction::SetTrue),
470 )
471 .subcommand(
472 Command::new("sub")
473 .arg(
474 Arg::new("debug")
475 .short('d')
476 .long("debug")
477 .action(ArgAction::SetTrue),
478 )
479 .arg(Arg::new("output").short('o').long("output")),
480 );
481
482 assert_sorted(&cmd);
484 }
485
486 #[test]
487 fn test_global_flags_dont_appear_in_subcommand_args() {
488 use clap::{Arg, ArgAction};
489
490 let cmd = Command::new("test")
493 .arg(
494 Arg::new("verbose")
495 .short('v')
496 .long("verbose")
497 .global(true)
498 .action(ArgAction::SetTrue),
499 )
500 .subcommand(
501 Command::new("sub")
502 .arg(
503 Arg::new("debug")
504 .short('d')
505 .long("debug")
506 .action(ArgAction::SetTrue),
507 )
508 .arg(Arg::new("output").short('o').long("output")),
509 );
510
511 let subcmd = cmd.find_subcommand("sub").unwrap();
512 let args: Vec<_> = subcmd.get_arguments().collect();
513
514 assert_eq!(args.len(), 2);
516
517 for arg in &args {
519 assert!(
520 !arg.is_global_set(),
521 "Subcommand arg {} should not be global",
522 arg.get_id()
523 );
524 }
525
526 assert_sorted(&cmd);
528 }
529
530 #[test]
531 #[should_panic(expected = "Flags with short options")]
532 fn test_uppercase_before_lowercase_same_letter() {
533 use clap::Arg;
534
535 let cmd = Command::new("test")
537 .arg(Arg::new("index").short('I').long("index"))
538 .arg(Arg::new("inject").short('i').long("inject"));
539
540 assert_sorted(&cmd);
541 }
542
543 #[test]
544 fn test_lowercase_before_uppercase_same_letter() {
545 use clap::Arg;
546
547 let cmd = Command::new("test")
549 .arg(Arg::new("inject").short('i').long("inject"))
550 .arg(Arg::new("index").short('I').long("index"));
551
552 assert_sorted(&cmd);
553 }
554
555 #[test]
556 #[should_panic(expected = "Flags with short options")]
557 fn test_task_docs_flags_unsorted() {
558 use clap::Arg;
559
560 let cmd = Command::new("generate").subcommand(
562 Command::new("task-docs")
563 .arg(Arg::new("index").short('I').long("index"))
564 .arg(Arg::new("inject").short('i').long("inject"))
565 .arg(Arg::new("multi").short('m').long("multi"))
566 .arg(Arg::new("output").short('o').long("output"))
567 .arg(Arg::new("root").short('r').long("root"))
568 .arg(Arg::new("style").short('s').long("style")),
569 );
570
571 assert_sorted(&cmd);
572 }
573
574 #[test]
575 fn test_error_message_shows_full_command_path() {
576 use clap::Arg;
577
578 let cmd = Command::new("parent-has-no-flags").subcommand(
580 Command::new("child-has-unsorted-flags")
581 .arg(Arg::new("zebra").short('z').long("zebra"))
582 .arg(Arg::new("alpha").short('a').long("alpha")),
583 );
584
585 let result = is_sorted(&cmd);
586 assert!(result.is_err());
587 let err = result.unwrap_err();
588
589 assert!(
591 err.contains("parent-has-no-flags child-has-unsorted-flags"),
592 "Error message should contain full path, got: {}",
593 err
594 );
595 }
596
597 #[test]
598 fn test_error_with_derive_api_nested_subcommands() {
599 use clap::{Args, Parser, Subcommand};
600
601 #[derive(Parser)]
602 struct Cli {
603 #[command(subcommand)]
604 command: Commands,
605 }
606
607 #[derive(Subcommand)]
608 enum Commands {
609 Generate(GenerateArgs),
611 }
612
613 #[derive(Args)]
614 struct GenerateArgs {
615 #[command(subcommand)]
616 command: GenerateCommands,
617 }
618
619 #[derive(Subcommand)]
620 enum GenerateCommands {
621 TaskDocs(TaskDocsArgs),
623 }
624
625 #[derive(Args)]
626 struct TaskDocsArgs {
627 #[arg(short, long)]
629 task: Option<String>,
630
631 #[arg(short, long)]
633 output: Option<String>,
634 }
635
636 let cmd = Cli::command();
637 let result = is_sorted(&cmd);
638
639 if let Err(e) = result {
640 eprintln!("Error message: {}", e);
642 assert!(
647 e.contains("task-docs"),
648 "Error should mention 'task-docs'. Got: {}",
649 e
650 );
651 assert!(
652 e.contains("[\"-t\", \"-o\"]"),
653 "Error should show the actual unsorted flags. Got: {}",
654 e
655 );
656 } else {
657 panic!("Expected error for unsorted flags");
658 }
659 }
660}