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 return Err(format!(
234 "Arguments in '{}' are not in correct group order!\nExpected: [positional, short flags, long-only flags]\nActual: {:?}\nExpected: {:?}",
235 cmd.get_name(),
236 arg_ids,
237 expected_order
238 ));
239 }
240
241 Ok(())
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use clap::{Command, CommandFactory, Parser, Subcommand};
248
249 #[test]
250 fn test_sorted_subcommands() {
251 let cmd = Command::new("test")
252 .subcommand(Command::new("add"))
253 .subcommand(Command::new("delete"))
254 .subcommand(Command::new("list"));
255
256 assert_sorted(&cmd);
257 }
258
259 #[test]
260 #[should_panic(expected = "are not sorted alphabetically")]
261 fn test_unsorted_subcommands() {
262 let cmd = Command::new("test")
263 .subcommand(Command::new("list"))
264 .subcommand(Command::new("add"))
265 .subcommand(Command::new("delete"));
266
267 assert_sorted(&cmd);
268 }
269
270 #[test]
271 fn test_is_sorted_ok() {
272 let cmd = Command::new("test")
273 .subcommand(Command::new("add"))
274 .subcommand(Command::new("delete"))
275 .subcommand(Command::new("list"));
276
277 assert!(is_sorted(&cmd).is_ok());
278 }
279
280 #[test]
281 fn test_is_sorted_err() {
282 let cmd = Command::new("test")
283 .subcommand(Command::new("list"))
284 .subcommand(Command::new("add"));
285
286 assert!(is_sorted(&cmd).is_err());
287 }
288
289 #[test]
290 fn test_no_subcommands() {
291 let cmd = Command::new("test");
292 assert_sorted(&cmd);
293 assert!(is_sorted(&cmd).is_ok());
294 }
295
296 #[test]
297 fn test_with_derive_sorted() {
298 #[derive(Parser)]
299 struct Cli {
300 #[command(subcommand)]
301 command: Commands,
302 }
303
304 #[derive(Subcommand)]
305 enum Commands {
306 Add,
307 Delete,
308 List,
309 }
310
311 let cmd = Cli::command();
312 assert_sorted(&cmd);
313 }
314
315 #[test]
316 #[should_panic(expected = "are not sorted alphabetically")]
317 fn test_with_derive_unsorted() {
318 #[derive(Parser)]
319 struct Cli {
320 #[command(subcommand)]
321 command: Commands,
322 }
323
324 #[derive(Subcommand)]
325 enum Commands {
326 List,
327 Add,
328 Delete,
329 }
330
331 let cmd = Cli::command();
332 assert_sorted(&cmd);
333 }
334
335 #[test]
338 fn test_arguments_correctly_sorted() {
339 use clap::{Arg, ArgAction};
340
341 let cmd = Command::new("test")
342 .arg(Arg::new("file")) .arg(Arg::new("debug").short('d').long("debug").action(ArgAction::SetTrue))
344 .arg(Arg::new("output").short('o').long("output"))
345 .arg(Arg::new("verbose").short('v').long("verbose").action(ArgAction::SetTrue))
346 .arg(Arg::new("config").long("config"))
347 .arg(Arg::new("no-color").long("no-color").action(ArgAction::SetTrue));
348
349 assert_sorted(&cmd);
350 }
351
352 #[test]
353 #[should_panic(expected = "Flags with short options")]
354 fn test_short_flags_unsorted() {
355 use clap::Arg;
356
357 let cmd = Command::new("test")
358 .arg(Arg::new("verbose").short('v').long("verbose"))
359 .arg(Arg::new("debug").short('d').long("debug"));
360
361 assert_sorted(&cmd);
362 }
363
364 #[test]
365 #[should_panic(expected = "Long-only flags")]
366 fn test_long_only_unsorted() {
367 use clap::{Arg, ArgAction};
368
369 let cmd = Command::new("test")
370 .arg(Arg::new("zebra").long("zebra").action(ArgAction::SetTrue))
371 .arg(Arg::new("alpha").long("alpha").action(ArgAction::SetTrue));
372
373 assert_sorted(&cmd);
374 }
375
376 #[test]
377 #[should_panic(expected = "not in correct group order")]
378 fn test_wrong_group_order() {
379 use clap::Arg;
380
381 let cmd = Command::new("test")
383 .arg(Arg::new("config").long("config"))
384 .arg(Arg::new("verbose").short('v').long("verbose"));
385
386 assert_sorted(&cmd);
387 }
388
389 #[test]
390 fn test_positional_order_not_enforced() {
391 let cmd = Command::new("test")
393 .arg(clap::Arg::new("second"))
394 .arg(clap::Arg::new("first"));
395
396 assert_sorted(&cmd);
397 }
398
399 #[test]
400 fn test_is_sorted_ok_with_args() {
401 use clap::{Arg, ArgAction};
402
403 let cmd = Command::new("test")
404 .arg(Arg::new("file"))
405 .arg(Arg::new("output").short('o').long("output"))
406 .arg(Arg::new("config").long("config"))
407 .subcommand(Command::new("add"))
408 .subcommand(Command::new("delete"));
409
410 assert!(is_sorted(&cmd).is_ok());
411 }
412
413 #[test]
414 fn test_is_sorted_err_args() {
415 use clap::Arg;
416
417 let cmd = Command::new("test")
418 .arg(Arg::new("zebra").short('z').long("zebra"))
419 .arg(Arg::new("alpha").short('a').long("alpha"));
420
421 assert!(is_sorted(&cmd).is_err());
422 }
423
424 #[test]
425 fn test_recursive_subcommand_args() {
426 use clap::{Arg, ArgAction};
427
428 let cmd = Command::new("test")
429 .arg(Arg::new("verbose").short('v').long("verbose").action(ArgAction::SetTrue))
430 .subcommand(
431 Command::new("sub")
432 .arg(Arg::new("debug").short('d').long("debug").action(ArgAction::SetTrue))
433 .arg(Arg::new("output").short('o').long("output")),
434 );
435
436 assert_sorted(&cmd);
437 }
438
439 #[test]
440 #[should_panic(expected = "Flags with short options")]
441 fn test_recursive_subcommand_args_fails() {
442 use clap::Arg;
443
444 let cmd = Command::new("test")
445 .subcommand(
446 Command::new("sub")
447 .arg(Arg::new("output").short('o').long("output"))
448 .arg(Arg::new("debug").short('d').long("debug")),
449 );
450
451 assert_sorted(&cmd);
452 }
453}