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
156 .iter()
157 .filter_map(|a| a.get_short())
158 .collect();
159 let mut sorted_shorts = with_short_shorts.clone();
160 sorted_shorts.sort_by(|a, b| {
161 let a_lower = a.to_ascii_lowercase();
162 let b_lower = b.to_ascii_lowercase();
163 match a_lower.cmp(&b_lower) {
164 std::cmp::Ordering::Equal => {
165 if a.is_lowercase() && b.is_uppercase() {
167 std::cmp::Ordering::Less
168 } else if a.is_uppercase() && b.is_lowercase() {
169 std::cmp::Ordering::Greater
170 } else {
171 std::cmp::Ordering::Equal
172 }
173 }
174 other => other,
175 }
176 });
177
178 if with_short_shorts != sorted_shorts {
179 let current: Vec<String> = with_short
180 .iter()
181 .map(|a| format!("-{}", a.get_short().unwrap()))
182 .collect();
183 let mut sorted_args = with_short.clone();
184 sorted_args.sort_by(|a, b| {
185 let a_char = a.get_short().unwrap();
186 let b_char = b.get_short().unwrap();
187 let a_lower = a_char.to_ascii_lowercase();
188 let b_lower = b_char.to_ascii_lowercase();
189 match a_lower.cmp(&b_lower) {
190 std::cmp::Ordering::Equal => {
191 if a_char.is_lowercase() && b_char.is_uppercase() {
192 std::cmp::Ordering::Less
193 } else if a_char.is_uppercase() && b_char.is_lowercase() {
194 std::cmp::Ordering::Greater
195 } else {
196 std::cmp::Ordering::Equal
197 }
198 }
199 other => other,
200 }
201 });
202 let expected: Vec<String> = sorted_args
203 .iter()
204 .map(|a| format!("-{}", a.get_short().unwrap()))
205 .collect();
206
207 return Err(format!(
208 "Flags with short options in '{}' are not sorted!\nActual: {:?}\nExpected: {:?}",
209 path.join(" "),
210 current,
211 expected
212 ));
213 }
214
215 let long_only_longs: Vec<&str> = long_only
217 .iter()
218 .filter_map(|a| a.get_long())
219 .collect();
220 let mut sorted_longs = long_only_longs.clone();
221 sorted_longs.sort_unstable();
222
223 if long_only_longs != sorted_longs {
224 let current: Vec<String> = long_only_longs
225 .iter()
226 .map(|l| format!("--{}", l))
227 .collect();
228 let expected: Vec<String> = sorted_longs.iter().map(|l| format!("--{}", l)).collect();
229
230 return Err(format!(
231 "Long-only flags in '{}' are not sorted!\nActual: {:?}\nExpected: {:?}",
232 path.join(" "),
233 current,
234 expected
235 ));
236 }
237
238 let arg_ids: Vec<&str> = args.iter().map(|a| a.get_id().as_str()).collect();
240
241 let mut expected_order = Vec::new();
242 expected_order.extend(positional.iter().map(|a| a.get_id().as_str()));
243 expected_order.extend(with_short.iter().map(|a| a.get_id().as_str()));
244 expected_order.extend(long_only.iter().map(|a| a.get_id().as_str()));
245
246 if arg_ids != expected_order {
247 return Err(format!(
248 "Arguments in '{}' are not in correct group order!\nExpected: [positional, short flags, long-only flags]\nActual: {:?}\nExpected: {:?}",
249 path.join(" "),
250 arg_ids,
251 expected_order
252 ));
253 }
254
255 Ok(())
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use clap::{Command, CommandFactory, Parser, Subcommand};
262
263 #[test]
264 fn test_sorted_subcommands() {
265 let cmd = Command::new("test")
266 .subcommand(Command::new("add"))
267 .subcommand(Command::new("delete"))
268 .subcommand(Command::new("list"));
269
270 assert_sorted(&cmd);
271 }
272
273 #[test]
274 #[should_panic(expected = "are not sorted alphabetically")]
275 fn test_unsorted_subcommands() {
276 let cmd = Command::new("test")
277 .subcommand(Command::new("list"))
278 .subcommand(Command::new("add"))
279 .subcommand(Command::new("delete"));
280
281 assert_sorted(&cmd);
282 }
283
284 #[test]
285 fn test_is_sorted_ok() {
286 let cmd = Command::new("test")
287 .subcommand(Command::new("add"))
288 .subcommand(Command::new("delete"))
289 .subcommand(Command::new("list"));
290
291 assert!(is_sorted(&cmd).is_ok());
292 }
293
294 #[test]
295 fn test_is_sorted_err() {
296 let cmd = Command::new("test")
297 .subcommand(Command::new("list"))
298 .subcommand(Command::new("add"));
299
300 assert!(is_sorted(&cmd).is_err());
301 }
302
303 #[test]
304 fn test_no_subcommands() {
305 let cmd = Command::new("test");
306 assert_sorted(&cmd);
307 assert!(is_sorted(&cmd).is_ok());
308 }
309
310 #[test]
311 fn test_with_derive_sorted() {
312 #[derive(Parser)]
313 struct Cli {
314 #[command(subcommand)]
315 command: Commands,
316 }
317
318 #[derive(Subcommand)]
319 enum Commands {
320 Add,
321 Delete,
322 List,
323 }
324
325 let cmd = Cli::command();
326 assert_sorted(&cmd);
327 }
328
329 #[test]
330 #[should_panic(expected = "are not sorted alphabetically")]
331 fn test_with_derive_unsorted() {
332 #[derive(Parser)]
333 struct Cli {
334 #[command(subcommand)]
335 command: Commands,
336 }
337
338 #[derive(Subcommand)]
339 enum Commands {
340 List,
341 Add,
342 Delete,
343 }
344
345 let cmd = Cli::command();
346 assert_sorted(&cmd);
347 }
348
349 #[test]
352 fn test_arguments_correctly_sorted() {
353 use clap::{Arg, ArgAction};
354
355 let cmd = Command::new("test")
356 .arg(Arg::new("file")) .arg(Arg::new("debug").short('d').long("debug").action(ArgAction::SetTrue))
358 .arg(Arg::new("output").short('o').long("output"))
359 .arg(Arg::new("verbose").short('v').long("verbose").action(ArgAction::SetTrue))
360 .arg(Arg::new("config").long("config"))
361 .arg(Arg::new("no-color").long("no-color").action(ArgAction::SetTrue));
362
363 assert_sorted(&cmd);
364 }
365
366 #[test]
367 #[should_panic(expected = "Flags with short options")]
368 fn test_short_flags_unsorted() {
369 use clap::Arg;
370
371 let cmd = Command::new("test")
372 .arg(Arg::new("verbose").short('v').long("verbose"))
373 .arg(Arg::new("debug").short('d').long("debug"));
374
375 assert_sorted(&cmd);
376 }
377
378 #[test]
379 #[should_panic(expected = "Long-only flags")]
380 fn test_long_only_unsorted() {
381 use clap::{Arg, ArgAction};
382
383 let cmd = Command::new("test")
384 .arg(Arg::new("zebra").long("zebra").action(ArgAction::SetTrue))
385 .arg(Arg::new("alpha").long("alpha").action(ArgAction::SetTrue));
386
387 assert_sorted(&cmd);
388 }
389
390 #[test]
391 #[should_panic(expected = "not in correct group order")]
392 fn test_wrong_group_order() {
393 use clap::Arg;
394
395 let cmd = Command::new("test")
397 .arg(Arg::new("config").long("config"))
398 .arg(Arg::new("verbose").short('v').long("verbose"));
399
400 assert_sorted(&cmd);
401 }
402
403 #[test]
404 fn test_positional_order_not_enforced() {
405 let cmd = Command::new("test")
407 .arg(clap::Arg::new("second"))
408 .arg(clap::Arg::new("first"));
409
410 assert_sorted(&cmd);
411 }
412
413 #[test]
414 fn test_is_sorted_ok_with_args() {
415 use clap::Arg;
416
417 let cmd = Command::new("test")
418 .arg(Arg::new("file"))
419 .arg(Arg::new("output").short('o').long("output"))
420 .arg(Arg::new("config").long("config"))
421 .subcommand(Command::new("add"))
422 .subcommand(Command::new("delete"));
423
424 assert!(is_sorted(&cmd).is_ok());
425 }
426
427 #[test]
428 fn test_is_sorted_err_args() {
429 use clap::Arg;
430
431 let cmd = Command::new("test")
432 .arg(Arg::new("zebra").short('z').long("zebra"))
433 .arg(Arg::new("alpha").short('a').long("alpha"));
434
435 assert!(is_sorted(&cmd).is_err());
436 }
437
438 #[test]
439 fn test_recursive_subcommand_args() {
440 use clap::{Arg, ArgAction};
441
442 let cmd = Command::new("test")
443 .arg(Arg::new("verbose").short('v').long("verbose").action(ArgAction::SetTrue))
444 .subcommand(
445 Command::new("sub")
446 .arg(Arg::new("debug").short('d').long("debug").action(ArgAction::SetTrue))
447 .arg(Arg::new("output").short('o').long("output")),
448 );
449
450 assert_sorted(&cmd);
451 }
452
453 #[test]
454 #[should_panic(expected = "Flags with short options")]
455 fn test_recursive_subcommand_args_fails() {
456 use clap::Arg;
457
458 let cmd = Command::new("test")
459 .subcommand(
460 Command::new("sub")
461 .arg(Arg::new("output").short('o').long("output"))
462 .arg(Arg::new("debug").short('d').long("debug")),
463 );
464
465 assert_sorted(&cmd);
466 }
467
468 #[test]
469 fn test_global_flags_not_checked_in_subcommands() {
470 use clap::{Arg, ArgAction};
471
472 let cmd = Command::new("test")
474 .arg(Arg::new("verbose").short('v').long("verbose").global(true).action(ArgAction::SetTrue))
475 .subcommand(
476 Command::new("sub")
477 .arg(Arg::new("debug").short('d').long("debug").action(ArgAction::SetTrue))
478 .arg(Arg::new("output").short('o').long("output")),
479 );
480
481 assert_sorted(&cmd);
483 }
484
485 #[test]
486 fn test_global_flags_dont_appear_in_subcommand_args() {
487 use clap::{Arg, ArgAction};
488
489 let cmd = Command::new("test")
492 .arg(Arg::new("verbose").short('v').long("verbose").global(true).action(ArgAction::SetTrue))
493 .subcommand(
494 Command::new("sub")
495 .arg(Arg::new("debug").short('d').long("debug").action(ArgAction::SetTrue))
496 .arg(Arg::new("output").short('o').long("output")),
497 );
498
499 let subcmd = cmd.find_subcommand("sub").unwrap();
500 let args: Vec<_> = subcmd.get_arguments().collect();
501
502 assert_eq!(args.len(), 2);
504
505 for arg in &args {
507 assert!(!arg.is_global_set(), "Subcommand arg {} should not be global", arg.get_id());
508 }
509
510 assert_sorted(&cmd);
512 }
513
514 #[test]
515 #[should_panic(expected = "Flags with short options")]
516 fn test_uppercase_before_lowercase_same_letter() {
517 use clap::Arg;
518
519 let cmd = Command::new("test")
521 .arg(Arg::new("index").short('I').long("index"))
522 .arg(Arg::new("inject").short('i').long("inject"));
523
524 assert_sorted(&cmd);
525 }
526
527 #[test]
528 fn test_lowercase_before_uppercase_same_letter() {
529 use clap::Arg;
530
531 let cmd = Command::new("test")
533 .arg(Arg::new("inject").short('i').long("inject"))
534 .arg(Arg::new("index").short('I').long("index"));
535
536 assert_sorted(&cmd);
537 }
538
539 #[test]
540 #[should_panic(expected = "Flags with short options")]
541 fn test_task_docs_flags_unsorted() {
542 use clap::Arg;
543
544 let cmd = Command::new("generate")
546 .subcommand(
547 Command::new("task-docs")
548 .arg(Arg::new("index").short('I').long("index"))
549 .arg(Arg::new("inject").short('i').long("inject"))
550 .arg(Arg::new("multi").short('m').long("multi"))
551 .arg(Arg::new("output").short('o').long("output"))
552 .arg(Arg::new("root").short('r').long("root"))
553 .arg(Arg::new("style").short('s').long("style")),
554 );
555
556 assert_sorted(&cmd);
557 }
558
559 #[test]
560 fn test_error_message_shows_full_command_path() {
561 use clap::Arg;
562
563 let cmd = Command::new("parent-has-no-flags")
565 .subcommand(
566 Command::new("child-has-unsorted-flags")
567 .arg(Arg::new("zebra").short('z').long("zebra"))
568 .arg(Arg::new("alpha").short('a').long("alpha")),
569 );
570
571 let result = is_sorted(&cmd);
572 assert!(result.is_err());
573 let err = result.unwrap_err();
574
575 assert!(err.contains("parent-has-no-flags child-has-unsorted-flags"),
577 "Error message should contain full path, got: {}", err);
578 }
579
580 #[test]
581 fn test_error_with_derive_api_nested_subcommands() {
582 use clap::{Args, Parser, Subcommand};
583
584 #[derive(Parser)]
585 struct Cli {
586 #[command(subcommand)]
587 command: Commands,
588 }
589
590 #[derive(Subcommand)]
591 enum Commands {
592 Generate(GenerateArgs),
594 }
595
596 #[derive(Args)]
597 struct GenerateArgs {
598 #[command(subcommand)]
599 command: GenerateCommands,
600 }
601
602 #[derive(Subcommand)]
603 enum GenerateCommands {
604 TaskDocs(TaskDocsArgs),
606 }
607
608 #[derive(Args)]
609 struct TaskDocsArgs {
610 #[arg(short, long)]
612 task: Option<String>,
613
614 #[arg(short, long)]
616 output: Option<String>,
617 }
618
619 let cmd = Cli::command();
620 let result = is_sorted(&cmd);
621
622 if let Err(e) = result {
623 eprintln!("Error message: {}", e);
625 assert!(e.contains("task-docs"),
630 "Error should mention 'task-docs'. Got: {}", e);
631 assert!(e.contains("[\"-t\", \"-o\"]"),
632 "Error should show the actual unsorted flags. Got: {}", e);
633 } else {
634 panic!("Expected error for unsorted flags");
635 }
636 }
637}