1pub fn assert_sorted(cmd: &clap::Command) {
39 let subcommands: Vec<_> = cmd.get_subcommands().map(|s| s.get_name()).collect();
41
42 if !subcommands.is_empty() {
43 let mut sorted = subcommands.clone();
44 sorted.sort();
45
46 if subcommands != sorted {
47 panic!(
48 "Subcommands in '{}' are not sorted alphabetically!\nActual order: {:?}\nExpected order: {:?}",
49 cmd.get_name(),
50 subcommands,
51 sorted
52 );
53 }
54 }
55
56 assert_arguments_sorted(cmd);
58
59 for subcmd in cmd.get_subcommands() {
61 assert_sorted(subcmd);
62 }
63}
64
65pub fn is_sorted(cmd: &clap::Command) -> Result<(), String> {
85 let subcommands: Vec<_> = cmd.get_subcommands().map(|s| s.get_name()).collect();
87
88 if !subcommands.is_empty() {
89 let mut sorted = subcommands.clone();
90 sorted.sort();
91
92 if subcommands != sorted {
93 return Err(format!(
94 "Subcommands in '{}' are not sorted alphabetically!\nActual order: {:?}\nExpected order: {:?}",
95 cmd.get_name(),
96 subcommands,
97 sorted
98 ));
99 }
100 }
101
102 is_arguments_sorted(cmd)?;
104
105 for subcmd in cmd.get_subcommands() {
107 is_sorted(subcmd)?;
108 }
109
110 Ok(())
111}
112
113fn assert_arguments_sorted(cmd: &clap::Command) {
115 if let Err(msg) = is_arguments_sorted(cmd) {
116 panic!("{}", msg);
117 }
118}
119
120fn is_arguments_sorted(cmd: &clap::Command) -> Result<(), String> {
122 let args: Vec<_> = cmd.get_arguments().collect();
123
124 let mut positional = Vec::new();
125 let mut with_short = Vec::new();
126 let mut long_only = Vec::new();
127
128 for arg in &args {
129 if arg.is_positional() {
130 positional.push(*arg);
131 } else if arg.get_short().is_some() {
132 with_short.push(*arg);
133 } else if arg.get_long().is_some() {
134 long_only.push(*arg);
135 }
136 }
137
138 let with_short_shorts: Vec<char> = with_short
142 .iter()
143 .filter_map(|a| a.get_short())
144 .collect();
145 let mut sorted_shorts = with_short_shorts.clone();
146 sorted_shorts.sort_by(|a, b| {
147 let a_lower = a.to_ascii_lowercase();
148 let b_lower = b.to_ascii_lowercase();
149 match a_lower.cmp(&b_lower) {
150 std::cmp::Ordering::Equal => {
151 if a.is_lowercase() && b.is_uppercase() {
153 std::cmp::Ordering::Less
154 } else if a.is_uppercase() && b.is_lowercase() {
155 std::cmp::Ordering::Greater
156 } else {
157 std::cmp::Ordering::Equal
158 }
159 }
160 other => other,
161 }
162 });
163
164 if with_short_shorts != sorted_shorts {
165 let current: Vec<String> = with_short
166 .iter()
167 .map(|a| format!("-{}", a.get_short().unwrap()))
168 .collect();
169 let mut sorted_args = with_short.clone();
170 sorted_args.sort_by(|a, b| {
171 let a_char = a.get_short().unwrap();
172 let b_char = b.get_short().unwrap();
173 let a_lower = a_char.to_ascii_lowercase();
174 let b_lower = b_char.to_ascii_lowercase();
175 match a_lower.cmp(&b_lower) {
176 std::cmp::Ordering::Equal => {
177 if a_char.is_lowercase() && b_char.is_uppercase() {
178 std::cmp::Ordering::Less
179 } else if a_char.is_uppercase() && b_char.is_lowercase() {
180 std::cmp::Ordering::Greater
181 } else {
182 std::cmp::Ordering::Equal
183 }
184 }
185 other => other,
186 }
187 });
188 let expected: Vec<String> = sorted_args
189 .iter()
190 .map(|a| format!("-{}", a.get_short().unwrap()))
191 .collect();
192
193 return Err(format!(
194 "Flags with short options in '{}' are not sorted!\nActual: {:?}\nExpected: {:?}",
195 cmd.get_name(),
196 current,
197 expected
198 ));
199 }
200
201 let long_only_longs: Vec<&str> = long_only
203 .iter()
204 .filter_map(|a| a.get_long())
205 .collect();
206 let mut sorted_longs = long_only_longs.clone();
207 sorted_longs.sort_unstable();
208
209 if long_only_longs != sorted_longs {
210 let current: Vec<String> = long_only_longs
211 .iter()
212 .map(|l| format!("--{}", l))
213 .collect();
214 let expected: Vec<String> = sorted_longs.iter().map(|l| format!("--{}", l)).collect();
215
216 return Err(format!(
217 "Long-only flags in '{}' are not sorted!\nActual: {:?}\nExpected: {:?}",
218 cmd.get_name(),
219 current,
220 expected
221 ));
222 }
223
224 let arg_ids: Vec<&str> = args.iter().map(|a| a.get_id().as_str()).collect();
226
227 let mut expected_order = Vec::new();
228 expected_order.extend(positional.iter().map(|a| a.get_id().as_str()));
229 expected_order.extend(with_short.iter().map(|a| a.get_id().as_str()));
230 expected_order.extend(long_only.iter().map(|a| a.get_id().as_str()));
231
232 if arg_ids != expected_order {
233 let full_path = get_command_path(cmd);
235 return Err(format!(
236 "Arguments in '{}' are not in correct group order!\nExpected: [positional, short flags, long-only flags]\nActual: {:?}\nExpected: {:?}",
237 full_path,
238 arg_ids,
239 expected_order
240 ));
241 }
242
243 Ok(())
244}
245
246fn get_command_path(cmd: &clap::Command) -> String {
248 let mut parts = vec![cmd.get_name()];
249 let mut current = cmd;
250
251 parts.into_iter().rev().collect::<Vec<_>>().join(" ")
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use clap::{Command, CommandFactory, Parser, Subcommand};
261
262 #[test]
263 fn test_sorted_subcommands() {
264 let cmd = Command::new("test")
265 .subcommand(Command::new("add"))
266 .subcommand(Command::new("delete"))
267 .subcommand(Command::new("list"));
268
269 assert_sorted(&cmd);
270 }
271
272 #[test]
273 #[should_panic(expected = "are not sorted alphabetically")]
274 fn test_unsorted_subcommands() {
275 let cmd = Command::new("test")
276 .subcommand(Command::new("list"))
277 .subcommand(Command::new("add"))
278 .subcommand(Command::new("delete"));
279
280 assert_sorted(&cmd);
281 }
282
283 #[test]
284 fn test_is_sorted_ok() {
285 let cmd = Command::new("test")
286 .subcommand(Command::new("add"))
287 .subcommand(Command::new("delete"))
288 .subcommand(Command::new("list"));
289
290 assert!(is_sorted(&cmd).is_ok());
291 }
292
293 #[test]
294 fn test_is_sorted_err() {
295 let cmd = Command::new("test")
296 .subcommand(Command::new("list"))
297 .subcommand(Command::new("add"));
298
299 assert!(is_sorted(&cmd).is_err());
300 }
301
302 #[test]
303 fn test_no_subcommands() {
304 let cmd = Command::new("test");
305 assert_sorted(&cmd);
306 assert!(is_sorted(&cmd).is_ok());
307 }
308
309 #[test]
310 fn test_with_derive_sorted() {
311 #[derive(Parser)]
312 struct Cli {
313 #[command(subcommand)]
314 command: Commands,
315 }
316
317 #[derive(Subcommand)]
318 enum Commands {
319 Add,
320 Delete,
321 List,
322 }
323
324 let cmd = Cli::command();
325 assert_sorted(&cmd);
326 }
327
328 #[test]
329 #[should_panic(expected = "are not sorted alphabetically")]
330 fn test_with_derive_unsorted() {
331 #[derive(Parser)]
332 struct Cli {
333 #[command(subcommand)]
334 command: Commands,
335 }
336
337 #[derive(Subcommand)]
338 enum Commands {
339 List,
340 Add,
341 Delete,
342 }
343
344 let cmd = Cli::command();
345 assert_sorted(&cmd);
346 }
347
348 #[test]
351 fn test_arguments_correctly_sorted() {
352 use clap::{Arg, ArgAction};
353
354 let cmd = Command::new("test")
355 .arg(Arg::new("file")) .arg(Arg::new("debug").short('d').long("debug").action(ArgAction::SetTrue))
357 .arg(Arg::new("output").short('o').long("output"))
358 .arg(Arg::new("verbose").short('v').long("verbose").action(ArgAction::SetTrue))
359 .arg(Arg::new("config").long("config"))
360 .arg(Arg::new("no-color").long("no-color").action(ArgAction::SetTrue));
361
362 assert_sorted(&cmd);
363 }
364
365 #[test]
366 #[should_panic(expected = "Flags with short options")]
367 fn test_short_flags_unsorted() {
368 use clap::Arg;
369
370 let cmd = Command::new("test")
371 .arg(Arg::new("verbose").short('v').long("verbose"))
372 .arg(Arg::new("debug").short('d').long("debug"));
373
374 assert_sorted(&cmd);
375 }
376
377 #[test]
378 #[should_panic(expected = "Long-only flags")]
379 fn test_long_only_unsorted() {
380 use clap::{Arg, ArgAction};
381
382 let cmd = Command::new("test")
383 .arg(Arg::new("zebra").long("zebra").action(ArgAction::SetTrue))
384 .arg(Arg::new("alpha").long("alpha").action(ArgAction::SetTrue));
385
386 assert_sorted(&cmd);
387 }
388
389 #[test]
390 #[should_panic(expected = "not in correct group order")]
391 fn test_wrong_group_order() {
392 use clap::Arg;
393
394 let cmd = Command::new("test")
396 .arg(Arg::new("config").long("config"))
397 .arg(Arg::new("verbose").short('v').long("verbose"));
398
399 assert_sorted(&cmd);
400 }
401
402 #[test]
403 fn test_positional_order_not_enforced() {
404 let cmd = Command::new("test")
406 .arg(clap::Arg::new("second"))
407 .arg(clap::Arg::new("first"));
408
409 assert_sorted(&cmd);
410 }
411
412 #[test]
413 fn test_is_sorted_ok_with_args() {
414 use clap::{Arg, ArgAction};
415
416 let cmd = Command::new("test")
417 .arg(Arg::new("file"))
418 .arg(Arg::new("output").short('o').long("output"))
419 .arg(Arg::new("config").long("config"))
420 .subcommand(Command::new("add"))
421 .subcommand(Command::new("delete"));
422
423 assert!(is_sorted(&cmd).is_ok());
424 }
425
426 #[test]
427 fn test_is_sorted_err_args() {
428 use clap::Arg;
429
430 let cmd = Command::new("test")
431 .arg(Arg::new("zebra").short('z').long("zebra"))
432 .arg(Arg::new("alpha").short('a').long("alpha"));
433
434 assert!(is_sorted(&cmd).is_err());
435 }
436
437 #[test]
438 fn test_recursive_subcommand_args() {
439 use clap::{Arg, ArgAction};
440
441 let cmd = Command::new("test")
442 .arg(Arg::new("verbose").short('v').long("verbose").action(ArgAction::SetTrue))
443 .subcommand(
444 Command::new("sub")
445 .arg(Arg::new("debug").short('d').long("debug").action(ArgAction::SetTrue))
446 .arg(Arg::new("output").short('o').long("output")),
447 );
448
449 assert_sorted(&cmd);
450 }
451
452 #[test]
453 #[should_panic(expected = "Flags with short options")]
454 fn test_recursive_subcommand_args_fails() {
455 use clap::Arg;
456
457 let cmd = Command::new("test")
458 .subcommand(
459 Command::new("sub")
460 .arg(Arg::new("output").short('o').long("output"))
461 .arg(Arg::new("debug").short('d').long("debug")),
462 );
463
464 assert_sorted(&cmd);
465 }
466
467 #[test]
468 fn test_global_flags_not_checked_in_subcommands() {
469 use clap::{Arg, ArgAction};
470
471 let cmd = Command::new("test")
473 .arg(Arg::new("verbose").short('v').long("verbose").global(true).action(ArgAction::SetTrue))
474 .subcommand(
475 Command::new("sub")
476 .arg(Arg::new("debug").short('d').long("debug").action(ArgAction::SetTrue))
477 .arg(Arg::new("output").short('o').long("output")),
478 );
479
480 assert_sorted(&cmd);
482 }
483
484 #[test]
485 fn test_global_flags_dont_appear_in_subcommand_args() {
486 use clap::{Arg, ArgAction};
487
488 let cmd = Command::new("test")
491 .arg(Arg::new("verbose").short('v').long("verbose").global(true).action(ArgAction::SetTrue))
492 .subcommand(
493 Command::new("sub")
494 .arg(Arg::new("debug").short('d').long("debug").action(ArgAction::SetTrue))
495 .arg(Arg::new("output").short('o').long("output")),
496 );
497
498 let subcmd = cmd.find_subcommand("sub").unwrap();
499 let args: Vec<_> = subcmd.get_arguments().collect();
500
501 assert_eq!(args.len(), 2);
503
504 for arg in &args {
506 assert!(!arg.is_global_set(), "Subcommand arg {} should not be global", arg.get_id());
507 }
508
509 assert_sorted(&cmd);
511 }
512}