use std::cmp::Ordering; use clap_lex::RawOsStr; use crate::builder::OsStr; use crate::builder::ValueRange; use crate::mkeymap::KeyType; use crate::util::FlatSet; use crate::util::Id; use crate::ArgAction; use crate::INTERNAL_ERROR_MSG; use crate::{Arg, Command, ValueHint}; pub(crate) fn assert_app(cmd: &Command) { debug!("Command::_debug_asserts"); let mut short_flags = vec![]; let mut long_flags = vec![]; // Invalid version flag settings if cmd.get_version().is_none() && cmd.get_long_version().is_none() { // PropagateVersion is meaningless if there is no version assert!( !cmd.is_propagate_version_set(), "Command {}: No version information via Command::version or Command::long_version to propagate", cmd.get_name(), ); // Used `Command::mut_arg("version", ..) but did not provide any version information to display let version_needed = cmd .get_arguments() .filter(|x| matches!(x.get_action(), ArgAction::Version)) .map(|x| x.get_id()) .collect::>(); assert_eq!(version_needed, Vec::<&str>::new(), "Command {}: `ArgAction::Version` used without providing Command::version or Command::long_version" ,cmd.get_name() ); } for sc in cmd.get_subcommands() { if let Some(s) = sc.get_short_flag().as_ref() { short_flags.push(Flag::Command(format!("-{}", s), sc.get_name())); } for short_alias in sc.get_all_short_flag_aliases() { short_flags.push(Flag::Command(format!("-{}", short_alias), sc.get_name())); } if let Some(l) = sc.get_long_flag().as_ref() { assert!(!l.starts_with('-'), "Command {}: long_flag {:?} must not start with a `-`, that will be handled by the parser", sc.get_name(), l); long_flags.push(Flag::Command(format!("--{}", l), sc.get_name())); } for long_alias in sc.get_all_long_flag_aliases() { long_flags.push(Flag::Command(format!("--{}", long_alias), sc.get_name())); } } for arg in cmd.get_arguments() { assert_arg(arg); assert!( !cmd.is_multicall_set(), "Command {}: Arguments like {} cannot be set on a multicall command", cmd.get_name(), arg.get_id() ); if let Some(s) = arg.get_short() { short_flags.push(Flag::Arg(format!("-{}", s), arg.get_id().as_str())); } for (short_alias, _) in &arg.short_aliases { short_flags.push(Flag::Arg( format!("-{}", short_alias), arg.get_id().as_str(), )); } if let Some(l) = arg.get_long() { assert!(!l.starts_with('-'), "Argument {}: long {:?} must not start with a `-`, that will be handled by the parser", arg.get_id(), l); long_flags.push(Flag::Arg(format!("--{}", l), arg.get_id().as_str())); } for (long_alias, _) in &arg.aliases { long_flags.push(Flag::Arg( format!("--{}", long_alias), arg.get_id().as_str(), )); } // Name conflicts if let Some((first, second)) = cmd.two_args_of(|x| x.get_id() == arg.get_id()) { panic!( "Command {}: Argument names must be unique, but '{}' is in use by more than one argument or group{}", cmd.get_name(), arg.get_id(), duplicate_tip(cmd, first, second), ); } // Long conflicts if let Some(l) = arg.get_long() { if let Some((first, second)) = cmd.two_args_of(|x| x.get_long() == Some(l)) { panic!( "Command {}: Long option names must be unique for each argument, \ but '--{}' is in use by both '{}' and '{}'{}", cmd.get_name(), l, first.get_id(), second.get_id(), duplicate_tip(cmd, first, second) ) } } // Short conflicts if let Some(s) = arg.get_short() { if let Some((first, second)) = cmd.two_args_of(|x| x.get_short() == Some(s)) { panic!( "Command {}: Short option names must be unique for each argument, \ but '-{}' is in use by both '{}' and '{}'{}", cmd.get_name(), s, first.get_id(), second.get_id(), duplicate_tip(cmd, first, second), ) } } // Index conflicts if let Some(idx) = arg.index { if let Some((first, second)) = cmd.two_args_of(|x| x.is_positional() && x.get_index() == Some(idx)) { panic!( "Command {}: Argument '{}' has the same index as '{}' \ and they are both positional arguments\n\n\t \ Use `Arg::num_args(1..)` to allow one \ positional argument to take multiple values", cmd.get_name(), first.get_id(), second.get_id() ) } } // requires, r_if, r_unless for req in &arg.requires { assert!( cmd.id_exists(&req.1), "Command {}: Argument or group '{}' specified in 'requires*' for '{}' does not exist", cmd.get_name(), req.1, arg.get_id(), ); } for req in &arg.r_ifs { assert!( !arg.is_required_set(), "Argument {}: `required` conflicts with `required_if_eq*`", arg.get_id() ); assert!( cmd.id_exists(&req.0), "Command {}: Argument or group '{}' specified in 'required_if_eq*' for '{}' does not exist", cmd.get_name(), req.0, arg.get_id() ); } for req in &arg.r_ifs_all { assert!( !arg.is_required_set(), "Argument {}: `required` conflicts with `required_if_eq_all`", arg.get_id() ); assert!( cmd.id_exists(&req.0), "Command {}: Argument or group '{}' specified in 'required_if_eq_all' for '{}' does not exist", cmd.get_name(), req.0, arg.get_id() ); } for req in &arg.r_unless { assert!( !arg.is_required_set(), "Argument {}: `required` conflicts with `required_unless*`", arg.get_id() ); assert!( cmd.id_exists(req), "Command {}: Argument or group '{}' specified in 'required_unless*' for '{}' does not exist", cmd.get_name(), req, arg.get_id(), ); } for req in &arg.r_unless_all { assert!( !arg.is_required_set(), "Argument {}: `required` conflicts with `required_unless*`", arg.get_id() ); assert!( cmd.id_exists(req), "Command {}: Argument or group '{}' specified in 'required_unless*' for '{}' does not exist", cmd.get_name(), req, arg.get_id(), ); } // blacklist for req in &arg.blacklist { assert!( cmd.id_exists(req), "Command {}: Argument or group '{}' specified in 'conflicts_with*' for '{}' does not exist", cmd.get_name(), req, arg.get_id(), ); } // overrides for req in &arg.overrides { assert!( cmd.id_exists(req), "Command {}: Argument or group '{}' specified in 'overrides_with*' for '{}' does not exist", cmd.get_name(), req, arg.get_id(), ); } if arg.is_last_set() { assert!( arg.get_long().is_none(), "Command {}: Flags or Options cannot have last(true) set. '{}' has both a long and last(true) set.", cmd.get_name(), arg.get_id() ); assert!( arg.get_short().is_none(), "Command {}: Flags or Options cannot have last(true) set. '{}' has both a short and last(true) set.", cmd.get_name(), arg.get_id() ); } assert!( !(arg.is_required_set() && arg.is_global_set()), "Command {}: Global arguments cannot be required.\n\n\t'{}' is marked as both global and required", cmd.get_name(), arg.get_id() ); if arg.get_value_hint() == ValueHint::CommandWithArguments { assert!( arg.is_positional(), "Command {}: Argument '{}' has hint CommandWithArguments and must be positional.", cmd.get_name(), arg.get_id() ); assert!( arg.is_trailing_var_arg_set() || arg.is_last_set(), "Command {}: Positional argument '{}' has hint CommandWithArguments, so Command must have `trailing_var_arg(true)` or `last(true)` set.", cmd.get_name(), arg.get_id() ); } } for group in cmd.get_groups() { let derive_hint = if cfg!(feature = "derive") { " (note: `Args` implicitly creates `ArgGroup`s; disable with `#[group(skip)]`" } else { "" }; // Name conflicts assert!( cmd.get_groups().filter(|x| x.id == group.id).count() < 2, "Command {}: Argument group name must be unique\n\n\t'{}' is already in use{}", cmd.get_name(), group.get_id(), derive_hint ); // Groups should not have naming conflicts with Args assert!( !cmd.get_arguments().any(|x| x.get_id() == group.get_id()), "Command {}: Argument group name '{}' must not conflict with argument name{}", cmd.get_name(), group.get_id(), derive_hint ); for arg in &group.args { // Args listed inside groups should exist assert!( cmd.get_arguments().any(|x| x.get_id() == arg), "Command {}: Argument group '{}' contains non-existent argument '{}'", cmd.get_name(), group.get_id(), arg ); } } // Conflicts between flags and subcommands long_flags.sort_unstable(); short_flags.sort_unstable(); detect_duplicate_flags(&long_flags, "long"); detect_duplicate_flags(&short_flags, "short"); let mut subs = FlatSet::new(); for sc in cmd.get_subcommands() { assert!( subs.insert(sc.get_name()), "Command {}: command name `{}` is duplicated", cmd.get_name(), sc.get_name() ); for alias in sc.get_all_aliases() { assert!( subs.insert(alias), "Command {}: command `{}` alias `{}` is duplicated", cmd.get_name(), sc.get_name(), alias ); } } _verify_positionals(cmd); #[cfg(feature = "help")] if let Some(help_template) = cmd.get_help_template() { assert!( !help_template.to_string().contains("{flags}"), "Command {}: {}", cmd.get_name(), "`{flags}` template variable was removed in clap3, they are now included in `{options}`", ); assert!( !help_template.to_string().contains("{unified}"), "Command {}: {}", cmd.get_name(), "`{unified}` template variable was removed in clap3, use `{options}` instead" ); #[cfg(feature = "unstable-v5")] assert!( !help_template.to_string().contains("{bin}"), "Command {}: {}", cmd.get_name(), "`{bin}` template variable was removed in clap5, use `{name}` instead" ) } cmd._panic_on_missing_help(cmd.is_help_expected_set()); assert_app_flags(cmd); } fn duplicate_tip(cmd: &Command, first: &Arg, second: &Arg) -> &'static str { if !cmd.is_disable_help_flag_set() && (first.get_id() == Id::HELP || second.get_id() == Id::HELP) { " (call `cmd.disable_help_flag(true)` to remove the auto-generated `--help`)" } else if !cmd.is_disable_version_flag_set() && (first.get_id() == Id::VERSION || second.get_id() == Id::VERSION) { " (call `cmd.disable_version_flag(true)` to remove the auto-generated `--version`)" } else { "" } } #[derive(Eq)] enum Flag<'a> { Command(String, &'a str), Arg(String, &'a str), } impl PartialEq for Flag<'_> { fn eq(&self, other: &Flag) -> bool { self.cmp(other) == Ordering::Equal } } impl PartialOrd for Flag<'_> { fn partial_cmp(&self, other: &Flag) -> Option { use Flag::*; match (self, other) { (Command(s1, _), Command(s2, _)) | (Arg(s1, _), Arg(s2, _)) | (Command(s1, _), Arg(s2, _)) | (Arg(s1, _), Command(s2, _)) => { if s1 == s2 { Some(Ordering::Equal) } else { s1.partial_cmp(s2) } } } } } impl Ord for Flag<'_> { fn cmp(&self, other: &Self) -> Ordering { self.partial_cmp(other).unwrap() } } fn detect_duplicate_flags(flags: &[Flag], short_or_long: &str) { use Flag::*; for (one, two) in find_duplicates(flags) { match (one, two) { (Command(flag, one), Command(_, another)) if one != another => panic!( "the '{}' {} flag is specified for both '{}' and '{}' subcommands", flag, short_or_long, one, another ), (Arg(flag, one), Arg(_, another)) if one != another => panic!( "{} option names must be unique, but '{}' is in use by both '{}' and '{}'", short_or_long, flag, one, another ), (Arg(flag, arg), Command(_, sub)) | (Command(flag, sub), Arg(_, arg)) => panic!( "the '{}' {} flag for the '{}' argument conflicts with the short flag \ for '{}' subcommand", flag, short_or_long, arg, sub ), _ => {} } } } /// Find duplicates in a sorted array. /// /// The algorithm is simple: the array is sorted, duplicates /// must be placed next to each other, we can check only adjacent elements. fn find_duplicates(slice: &[T]) -> impl Iterator { slice.windows(2).filter_map(|w| { if w[0] == w[1] { Some((&w[0], &w[1])) } else { None } }) } fn assert_app_flags(cmd: &Command) { macro_rules! checker { ($a:ident requires $($b:ident)|+) => { if cmd.$a() { let mut s = String::new(); $( if !cmd.$b() { use std::fmt::Write; write!(&mut s, " AppSettings::{} is required when AppSettings::{} is set.\n", std::stringify!($b), std::stringify!($a)).unwrap(); } )+ if !s.is_empty() { panic!("{}", s) } } }; ($a:ident conflicts $($b:ident)|+) => { if cmd.$a() { let mut s = String::new(); $( if cmd.$b() { use std::fmt::Write; write!(&mut s, " AppSettings::{} conflicts with AppSettings::{}.\n", std::stringify!($b), std::stringify!($a)).unwrap(); } )+ if !s.is_empty() { panic!("{}\n{}", cmd.get_name(), s) } } }; } checker!(is_multicall_set conflicts is_no_binary_name_set); } #[cfg(debug_assertions)] fn _verify_positionals(cmd: &Command) -> bool { debug!("Command::_verify_positionals"); // Because you must wait until all arguments have been supplied, this is the first chance // to make assertions on positional argument indexes // // First we verify that the index highest supplied index, is equal to the number of // positional arguments to verify there are no gaps (i.e. supplying an index of 1 and 3 // but no 2) let highest_idx = cmd .get_keymap() .keys() .filter_map(|x| { if let KeyType::Position(n) = x { Some(*n) } else { None } }) .max() .unwrap_or(0); let num_p = cmd.get_keymap().keys().filter(|x| x.is_position()).count(); assert!( highest_idx == num_p, "Found positional argument whose index is {} but there \ are only {} positional arguments defined", highest_idx, num_p ); for arg in cmd.get_arguments() { if arg.index.unwrap_or(0) == highest_idx { assert!( !arg.is_trailing_var_arg_set() || !arg.is_last_set(), "{}:{}: `Arg::trailing_var_arg` and `Arg::last` cannot be used together", cmd.get_name(), arg.get_id() ); if arg.is_trailing_var_arg_set() { assert!( arg.is_multiple(), "{}:{}: `Arg::trailing_var_arg` must accept multiple values", cmd.get_name(), arg.get_id() ); } } else { assert!( !arg.is_trailing_var_arg_set(), "{}:{}: `Arg::trailing_var_arg` can only apply to last positional", cmd.get_name(), arg.get_id() ); } } // Next we verify that only the highest index has takes multiple arguments (if any) let only_highest = |a: &Arg| a.is_multiple() && (a.get_index().unwrap_or(0) != highest_idx); if cmd.get_positionals().any(only_highest) { // First we make sure if there is a positional that allows multiple values // the one before it (second to last) has one of these: // * a value terminator // * ArgSettings::Last // * The last arg is Required // We can't pass the closure (it.next()) to the macro directly because each call to // find() (iterator, not macro) gets called repeatedly. let last = &cmd.get_keymap()[&KeyType::Position(highest_idx)]; let second_to_last = &cmd.get_keymap()[&KeyType::Position(highest_idx - 1)]; // Either the final positional is required // Or the second to last has a terminator or .last(true) set let ok = last.is_required_set() || (second_to_last.terminator.is_some() || second_to_last.is_last_set()) || last.is_last_set(); assert!( ok, "When using a positional argument with `.num_args(1..)` that is *not the \ last* positional argument, the last positional argument (i.e. the one \ with the highest index) *must* have .required(true) or .last(true) set." ); // We make sure if the second to last is Multiple the last is ArgSettings::Last let ok = second_to_last.is_multiple() || last.is_last_set(); assert!( ok, "Only the last positional argument, or second to last positional \ argument may be set to `.num_args(1..)`" ); // Next we check how many have both Multiple and not a specific number of values set let count = cmd .get_positionals() .filter(|p| { p.is_multiple_values_set() && !p.get_num_args().expect(INTERNAL_ERROR_MSG).is_fixed() }) .count(); let ok = count <= 1 || (last.is_last_set() && last.is_multiple() && second_to_last.is_multiple() && count == 2); assert!( ok, "Only one positional argument with `.num_args(1..)` set is allowed per \ command, unless the second one also has .last(true) set" ); } let mut found = false; if cmd.is_allow_missing_positional_set() { // Check that if a required positional argument is found, all positions with a lower // index are also required. let mut foundx2 = false; for p in cmd.get_positionals() { if foundx2 && !p.is_required_set() { assert!( p.is_required_set(), "Found non-required positional argument with a lower \ index than a required positional argument by two or more: {:?} \ index {:?}", p.get_id(), p.get_index() ); } else if p.is_required_set() && !p.is_last_set() { // Args that .last(true) don't count since they can be required and have // positionals with a lower index that aren't required // Imagine: prog [opt1] -- // Both of these are valid invocations: // $ prog r1 -- r2 // $ prog r1 o1 -- r2 if found { foundx2 = true; continue; } found = true; continue; } else { found = false; } } } else { // Check that if a required positional argument is found, all positions with a lower // index are also required for p in (1..=num_p).rev().filter_map(|n| cmd.get_keymap().get(&n)) { if found { assert!( p.is_required_set(), "Found non-required positional argument with a lower \ index than a required positional argument: {:?} index {:?}", p.get_id(), p.get_index() ); } else if p.is_required_set() && !p.is_last_set() { // Args that .last(true) don't count since they can be required and have // positionals with a lower index that aren't required // Imagine: prog [opt1] -- // Both of these are valid invocations: // $ prog r1 -- r2 // $ prog r1 o1 -- r2 found = true; continue; } } } assert!( cmd.get_positionals().filter(|p| p.is_last_set()).count() < 2, "Only one positional argument may have last(true) set. Found two." ); if cmd .get_positionals() .any(|p| p.is_last_set() && p.is_required_set()) && cmd.has_subcommands() && !cmd.is_subcommand_negates_reqs_set() { panic!( "Having a required positional argument with .last(true) set *and* child \ subcommands without setting SubcommandsNegateReqs isn't compatible." ); } true } fn assert_arg(arg: &Arg) { debug!("Arg::_debug_asserts:{}", arg.get_id()); // Self conflict // TODO: this check should be recursive assert!( !arg.blacklist.iter().any(|x| x == arg.get_id()), "Argument '{}' cannot conflict with itself", arg.get_id(), ); assert_eq!( arg.get_action().takes_values(), arg.is_takes_value_set(), "Argument `{}`'s selected action {:?} contradicts `takes_value`", arg.get_id(), arg.get_action() ); if let Some(action_type_id) = arg.get_action().value_type_id() { assert_eq!( action_type_id, arg.get_value_parser().type_id(), "Argument `{}`'s selected action {:?} contradicts `value_parser` ({:?})", arg.get_id(), arg.get_action(), arg.get_value_parser() ); } if arg.get_value_hint() != ValueHint::Unknown { assert!( arg.is_takes_value_set(), "Argument '{}' has value hint but takes no value", arg.get_id() ); if arg.get_value_hint() == ValueHint::CommandWithArguments { assert!( arg.is_multiple_values_set(), "Argument '{}' uses hint CommandWithArguments and must accept multiple values", arg.get_id() ) } } if arg.index.is_some() { assert!( arg.is_positional(), "Argument '{}' is a positional argument and can't have short or long name versions", arg.get_id() ); assert!( arg.is_takes_value_set(), "Argument '{}` is positional, it must take a value{}", arg.get_id(), if arg.get_id() == Id::HELP { " (`mut_arg` no longer works with implicit `--help`)" } else if arg.get_id() == Id::VERSION { " (`mut_arg` no longer works with implicit `--version`)" } else { "" } ); } let num_vals = arg.get_num_args().expect(INTERNAL_ERROR_MSG); // This can be the cause of later asserts, so put this first if num_vals != ValueRange::EMPTY { // HACK: Don't check for flags to make the derive easier let num_val_names = arg.get_value_names().unwrap_or(&[]).len(); if num_vals.max_values() < num_val_names { panic!( "Argument {}: Too many value names ({}) compared to `num_args` ({})", arg.get_id(), num_val_names, num_vals ); } } assert_eq!( num_vals.takes_values(), arg.is_takes_value_set(), "Argument {}: mismatch between `num_args` ({}) and `takes_value`", arg.get_id(), num_vals, ); assert_eq!( num_vals.is_multiple(), arg.is_multiple_values_set(), "Argument {}: mismatch between `num_args` ({}) and `multiple_values`", arg.get_id(), num_vals, ); if 1 < num_vals.min_values() { assert!( !arg.is_require_equals_set(), "Argument {}: cannot accept more than 1 arg (num_args={}) with require_equals", arg.get_id(), num_vals ); } if num_vals == ValueRange::SINGLE { assert!( !arg.is_multiple_values_set(), "Argument {}: mismatch between `num_args` and `multiple_values`", arg.get_id() ); } assert_arg_flags(arg); assert_defaults(arg, "default_value", arg.default_vals.iter()); assert_defaults( arg, "default_missing_value", arg.default_missing_vals.iter(), ); assert_defaults( arg, "default_value_if", arg.default_vals_ifs .iter() .filter_map(|(_, _, default)| default.as_ref()), ); } fn assert_arg_flags(arg: &Arg) { macro_rules! checker { ($a:ident requires $($b:ident)|+) => { if arg.$a() { let mut s = String::new(); $( if !arg.$b() { use std::fmt::Write; write!(&mut s, " Arg::{} is required when Arg::{} is set.\n", std::stringify!($b), std::stringify!($a)).unwrap(); } )+ if !s.is_empty() { panic!("Argument {:?}\n{}", arg.get_id(), s) } } } } checker!(is_hide_possible_values_set requires is_takes_value_set); checker!(is_allow_hyphen_values_set requires is_takes_value_set); checker!(is_allow_negative_numbers_set requires is_takes_value_set); checker!(is_require_equals_set requires is_takes_value_set); checker!(is_last_set requires is_takes_value_set); checker!(is_hide_default_value_set requires is_takes_value_set); checker!(is_multiple_values_set requires is_takes_value_set); checker!(is_ignore_case_set requires is_takes_value_set); } fn assert_defaults<'d>( arg: &Arg, field: &'static str, defaults: impl IntoIterator, ) { for default_os in defaults { let value_parser = arg.get_value_parser(); let assert_cmd = Command::new("assert"); if let Some(delim) = arg.get_value_delimiter() { let default_os = RawOsStr::new(default_os); for part in default_os.split(delim) { if let Err(err) = value_parser.parse_ref(&assert_cmd, Some(arg), &part.to_os_str()) { panic!( "Argument `{}`'s {}={:?} failed validation: {}", arg.get_id(), field, part.to_str_lossy(), err ); } } } else if let Err(err) = value_parser.parse_ref(&assert_cmd, Some(arg), default_os) { panic!( "Argument `{}`'s {}={:?} failed validation: {}", arg.get_id(), field, default_os, err ); } } }