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 positional_ids: Vec<&str> = positional.iter().map(|a| a.get_id().as_str()).collect();
140 let mut sorted_positional = positional_ids.clone();
141 sorted_positional.sort_unstable();
142
143 if positional_ids != sorted_positional {
144 return Err(format!(
145 "Positional arguments in '{}' are not sorted!\nActual: {:?}\nExpected: {:?}",
146 cmd.get_name(),
147 positional_ids,
148 sorted_positional
149 ));
150 }
151
152 let with_short_shorts: Vec<char> = with_short
154 .iter()
155 .filter_map(|a| a.get_short())
156 .collect();
157 let mut sorted_shorts = with_short_shorts.clone();
158 sorted_shorts.sort_by(|a, b| {
159 let a_lower = a.to_ascii_lowercase();
160 let b_lower = b.to_ascii_lowercase();
161 match a_lower.cmp(&b_lower) {
162 std::cmp::Ordering::Equal => {
163 if a.is_lowercase() && b.is_uppercase() {
165 std::cmp::Ordering::Less
166 } else if a.is_uppercase() && b.is_lowercase() {
167 std::cmp::Ordering::Greater
168 } else {
169 std::cmp::Ordering::Equal
170 }
171 }
172 other => other,
173 }
174 });
175
176 if with_short_shorts != sorted_shorts {
177 let current: Vec<String> = with_short
178 .iter()
179 .map(|a| format!("-{}", a.get_short().unwrap()))
180 .collect();
181 let mut sorted_args = with_short.clone();
182 sorted_args.sort_by(|a, b| {
183 let a_char = a.get_short().unwrap();
184 let b_char = b.get_short().unwrap();
185 let a_lower = a_char.to_ascii_lowercase();
186 let b_lower = b_char.to_ascii_lowercase();
187 match a_lower.cmp(&b_lower) {
188 std::cmp::Ordering::Equal => {
189 if a_char.is_lowercase() && b_char.is_uppercase() {
190 std::cmp::Ordering::Less
191 } else if a_char.is_uppercase() && b_char.is_lowercase() {
192 std::cmp::Ordering::Greater
193 } else {
194 std::cmp::Ordering::Equal
195 }
196 }
197 other => other,
198 }
199 });
200 let expected: Vec<String> = sorted_args
201 .iter()
202 .map(|a| format!("-{}", a.get_short().unwrap()))
203 .collect();
204
205 return Err(format!(
206 "Flags with short options in '{}' are not sorted!\nActual: {:?}\nExpected: {:?}",
207 cmd.get_name(),
208 current,
209 expected
210 ));
211 }
212
213 let long_only_longs: Vec<&str> = long_only
215 .iter()
216 .filter_map(|a| a.get_long())
217 .collect();
218 let mut sorted_longs = long_only_longs.clone();
219 sorted_longs.sort_unstable();
220
221 if long_only_longs != sorted_longs {
222 let current: Vec<String> = long_only_longs
223 .iter()
224 .map(|l| format!("--{}", l))
225 .collect();
226 let expected: Vec<String> = sorted_longs.iter().map(|l| format!("--{}", l)).collect();
227
228 return Err(format!(
229 "Long-only flags in '{}' are not sorted!\nActual: {:?}\nExpected: {:?}",
230 cmd.get_name(),
231 current,
232 expected
233 ));
234 }
235
236 let arg_ids: Vec<&str> = args.iter().map(|a| a.get_id().as_str()).collect();
238
239 let mut expected_order = Vec::new();
240 expected_order.extend(positional.iter().map(|a| a.get_id().as_str()));
241 expected_order.extend(with_short.iter().map(|a| a.get_id().as_str()));
242 expected_order.extend(long_only.iter().map(|a| a.get_id().as_str()));
243
244 if arg_ids != expected_order {
245 return Err(format!(
246 "Arguments in '{}' are not in correct group order!\nExpected: [positional, short flags, long-only flags]\nActual: {:?}\nExpected: {:?}",
247 cmd.get_name(),
248 arg_ids,
249 expected_order
250 ));
251 }
252
253 Ok(())
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use clap::{Command, CommandFactory, Parser, Subcommand};
260
261 #[test]
262 fn test_sorted_subcommands() {
263 let cmd = Command::new("test")
264 .subcommand(Command::new("add"))
265 .subcommand(Command::new("delete"))
266 .subcommand(Command::new("list"));
267
268 assert_sorted(&cmd);
269 }
270
271 #[test]
272 #[should_panic(expected = "are not sorted alphabetically")]
273 fn test_unsorted_subcommands() {
274 let cmd = Command::new("test")
275 .subcommand(Command::new("list"))
276 .subcommand(Command::new("add"))
277 .subcommand(Command::new("delete"));
278
279 assert_sorted(&cmd);
280 }
281
282 #[test]
283 fn test_is_sorted_ok() {
284 let cmd = Command::new("test")
285 .subcommand(Command::new("add"))
286 .subcommand(Command::new("delete"))
287 .subcommand(Command::new("list"));
288
289 assert!(is_sorted(&cmd).is_ok());
290 }
291
292 #[test]
293 fn test_is_sorted_err() {
294 let cmd = Command::new("test")
295 .subcommand(Command::new("list"))
296 .subcommand(Command::new("add"));
297
298 assert!(is_sorted(&cmd).is_err());
299 }
300
301 #[test]
302 fn test_no_subcommands() {
303 let cmd = Command::new("test");
304 assert_sorted(&cmd);
305 assert!(is_sorted(&cmd).is_ok());
306 }
307
308 #[test]
309 fn test_with_derive_sorted() {
310 #[derive(Parser)]
311 struct Cli {
312 #[command(subcommand)]
313 command: Commands,
314 }
315
316 #[derive(Subcommand)]
317 enum Commands {
318 Add,
319 Delete,
320 List,
321 }
322
323 let cmd = Cli::command();
324 assert_sorted(&cmd);
325 }
326
327 #[test]
328 #[should_panic(expected = "are not sorted alphabetically")]
329 fn test_with_derive_unsorted() {
330 #[derive(Parser)]
331 struct Cli {
332 #[command(subcommand)]
333 command: Commands,
334 }
335
336 #[derive(Subcommand)]
337 enum Commands {
338 List,
339 Add,
340 Delete,
341 }
342
343 let cmd = Cli::command();
344 assert_sorted(&cmd);
345 }
346
347 #[test]
350 fn test_arguments_correctly_sorted() {
351 use clap::{Arg, ArgAction};
352
353 let cmd = Command::new("test")
354 .arg(Arg::new("file")) .arg(Arg::new("debug").short('d').long("debug").action(ArgAction::SetTrue))
356 .arg(Arg::new("output").short('o').long("output"))
357 .arg(Arg::new("verbose").short('v').long("verbose").action(ArgAction::SetTrue))
358 .arg(Arg::new("config").long("config"))
359 .arg(Arg::new("no-color").long("no-color").action(ArgAction::SetTrue));
360
361 assert_sorted(&cmd);
362 }
363
364 #[test]
365 #[should_panic(expected = "Flags with short options")]
366 fn test_short_flags_unsorted() {
367 use clap::Arg;
368
369 let cmd = Command::new("test")
370 .arg(Arg::new("verbose").short('v').long("verbose"))
371 .arg(Arg::new("debug").short('d').long("debug"));
372
373 assert_sorted(&cmd);
374 }
375
376 #[test]
377 #[should_panic(expected = "Long-only flags")]
378 fn test_long_only_unsorted() {
379 use clap::{Arg, ArgAction};
380
381 let cmd = Command::new("test")
382 .arg(Arg::new("zebra").long("zebra").action(ArgAction::SetTrue))
383 .arg(Arg::new("alpha").long("alpha").action(ArgAction::SetTrue));
384
385 assert_sorted(&cmd);
386 }
387
388 #[test]
389 #[should_panic(expected = "not in correct group order")]
390 fn test_wrong_group_order() {
391 use clap::Arg;
392
393 let cmd = Command::new("test")
395 .arg(Arg::new("config").long("config"))
396 .arg(Arg::new("verbose").short('v').long("verbose"));
397
398 assert_sorted(&cmd);
399 }
400
401 #[test]
402 #[should_panic(expected = "Positional arguments")]
403 fn test_positional_unsorted() {
404 let cmd = Command::new("test")
405 .arg(clap::Arg::new("second"))
406 .arg(clap::Arg::new("first"));
407
408 assert_sorted(&cmd);
409 }
410
411 #[test]
412 fn test_is_sorted_ok_with_args() {
413 use clap::{Arg, ArgAction};
414
415 let cmd = Command::new("test")
416 .arg(Arg::new("file"))
417 .arg(Arg::new("output").short('o').long("output"))
418 .arg(Arg::new("config").long("config"))
419 .subcommand(Command::new("add"))
420 .subcommand(Command::new("delete"));
421
422 assert!(is_sorted(&cmd).is_ok());
423 }
424
425 #[test]
426 fn test_is_sorted_err_args() {
427 use clap::Arg;
428
429 let cmd = Command::new("test")
430 .arg(Arg::new("zebra").short('z').long("zebra"))
431 .arg(Arg::new("alpha").short('a').long("alpha"));
432
433 assert!(is_sorted(&cmd).is_err());
434 }
435
436 #[test]
437 fn test_recursive_subcommand_args() {
438 use clap::{Arg, ArgAction};
439
440 let cmd = Command::new("test")
441 .arg(Arg::new("verbose").short('v').long("verbose").action(ArgAction::SetTrue))
442 .subcommand(
443 Command::new("sub")
444 .arg(Arg::new("debug").short('d').long("debug").action(ArgAction::SetTrue))
445 .arg(Arg::new("output").short('o').long("output")),
446 );
447
448 assert_sorted(&cmd);
449 }
450
451 #[test]
452 #[should_panic(expected = "Flags with short options")]
453 fn test_recursive_subcommand_args_fails() {
454 use clap::Arg;
455
456 let cmd = Command::new("test")
457 .subcommand(
458 Command::new("sub")
459 .arg(Arg::new("output").short('o').long("output"))
460 .arg(Arg::new("debug").short('d').long("debug")),
461 );
462
463 assert_sorted(&cmd);
464 }
465}