use clish::parse::CommandEntry;
use clish::prelude::*;
fn find_cmd(name: &str) -> &'static CommandEntry {
clish::inventory::iter::<CommandEntry>()
.find(|c| c.name == name)
.unwrap_or_else(|| panic!("command '{name}' not found in inventory"))
}
fn run_ok(name: &str, args: &[&str]) {
let cmd = find_cmd(name);
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let result = (cmd.run)(&args);
assert!(
result.is_ok(),
"run('{name}', {args:?}) expected Ok, got Err: {:?}",
result
);
}
fn run_err(name: &str, args: &[&str]) -> String {
let cmd = find_cmd(name);
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let result = (cmd.run)(&args);
match result {
Err(e) => e,
Ok(()) => panic!("run('{name}', {args:?}) expected Err, got Ok"),
}
}
#[command(help = "Required positional and named args")]
fn cmd_required(name: Pos<String>, greeting: Named<String>) {
assert_eq!(name, "world");
assert_eq!(greeting, "hello");
}
#[command(help = "Optional positional")]
fn cmd_optional_pos(name: Pos<Option<String>>) {
assert_eq!(name, Some("alice".to_string()));
}
#[command(help = "Optional positional with None")]
fn cmd_optional_pos_none(name: Pos<Option<String>>) {
assert_eq!(name, None);
}
#[command(help = "Variadic positional")]
fn cmd_variadic(files: Pos<Vec<String>>) {
assert_eq!(files, vec!["a", "b", "c"]);
}
#[command(help = "Empty variadic")]
fn cmd_empty_variadic(files: Pos<Vec<String>>) {
assert!(files.is_empty());
}
#[command(help = "Optional named")]
fn cmd_optional_named(name: Named<Option<String>>) {
assert_eq!(name, Some("bob".to_string()));
}
#[command(help = "Optional named absent")]
fn cmd_optional_named_absent(name: Named<Option<String>>) {
assert_eq!(name, None);
}
#[command(help = "Repeatable named")]
fn cmd_repeatable(tag: Named<Vec<String>>) {
assert_eq!(tag, vec!["x", "y", "z"]);
}
#[command(help = "Empty repeatable named")]
fn cmd_empty_repeatable(tag: Named<Vec<String>>) {
assert!(tag.is_empty());
}
#[command(help = "Flag")]
fn cmd_flag(verbose: bool) {
assert!(verbose);
}
#[command(help = "Flag absent")]
fn cmd_flag_absent(verbose: bool) {
assert!(!verbose);
}
#[command(help = "Parsed types")]
fn cmd_parsed(count: Pos<u32>, port: Named<u16>) {
assert_eq!(count, 42);
assert_eq!(port, 8080);
}
#[command(
help = "Short alias",
param(verbose, help = "Verbose output", short = 'v')
)]
fn cmd_short(name: Pos<String>, verbose: Named<String>) {
assert_eq!(name, "foo");
assert_eq!(verbose, "yes");
}
#[command(help = "Custom name")]
fn cmd_custom(cli_name: Pos<String>) {
assert_eq!(cli_name, "bar");
}
#[command(
help = "Default fallback",
param(name, help = "Name", default = "default_val")
)]
fn cmd_default(name: Named<String>) {
assert_eq!(name, "default_val");
}
#[command(
help = "Env fallback",
param(name, help = "Name", env = "CLISH_TEST_ENV_FALLBACK_VAR")
)]
fn cmd_env(name: Named<String>) {
assert_eq!(name, "from_env");
}
#[command(
help = "Choices validation",
param(mode, help = "Operating mode", choices = ["fast", "slow", "auto"])
)]
fn cmd_choices(mode: Named<String>) {
assert_eq!(mode, "fast");
}
#[command(
help = "Conflicts",
param(verbose, help = "Verbose", short = 'v'),
param(quiet, help = "Quiet", short = 'q', conflicts_with = ["verbose"])
)]
fn cmd_conflicts(verbose: bool, quiet: bool) {
assert!(verbose || quiet);
}
#[command(
help = "Requires",
param(host, help = "Host"),
param(port, help = "Port", requires = ["host"])
)]
fn cmd_requires(host: Named<String>, port: Named<u16>) {
assert_eq!(host, "localhost");
assert_eq!(port, 3000);
}
#[command(
help = "Hidden param",
param(secret, help = "Secret value", hide = true)
)]
fn cmd_hidden_param(name: Pos<String>, secret: Named<String>) {
assert_eq!(name, "visible");
assert_eq!(secret, "shh");
}
#[command(help = "Placeholder")]
fn cmd_placeholder(name: Pos<String>) {
assert_eq!(name, "test");
}
#[command(help = "Value hint")]
fn cmd_value_hint(path: Pos<String>) {
assert_eq!(path, "/tmp");
}
#[command(
help = "Custom cli name via param",
param(_x, help = "Custom name", name = "custom-name")
)]
fn cmd_param_name(_x: Named<String>) {
}
#[command]
fn cmd_doc_comment(name: Pos<String>) {
assert_eq!(name, "doc");
}
#[command(
help = "Aliases",
aliases = ["al1", "al2"]
)]
fn cmd_aliases(name: Pos<String>) {
assert_eq!(name, "alias_target");
}
#[command(help = "Hidden command", hidden = true)]
fn cmd_hidden_cmd(name: Pos<String>) {
assert_eq!(name, "hidden_ok");
}
#[command(
help = "Deprecated command",
deprecated = true,
deprecation_note = "use cmd_active instead"
)]
fn cmd_deprecated_cmd(name: Pos<String>) {
assert_eq!(name, "dep_ok");
}
#[command(help = "Name override", name = "renamed-command")]
fn original_name(name: Pos<String>) {
assert_eq!(name, "via_rename");
}
#[command(help = "Equals form parsing")]
fn cmd_equals(value: Named<String>) {
assert_eq!(value, "direct");
}
#[command(
help = "Bundled flags",
param(a, short = 'a'),
param(b, short = 'b'),
param(c, short = 'c')
)]
fn cmd_bundled(a: bool, b: bool, c: bool) {
assert!(a);
assert!(b);
assert!(c);
}
#[command(help = "Double dash separator")]
fn cmd_double_dash(files: Pos<Vec<String>>) {
assert_eq!(files, vec!["a", "--b", "c"]);
}
#[command(help = "Named with default and no value")]
fn cmd_named_default_absent(_name: Named<String>) {}
#[test]
fn test_cmd_metadata_help_from_attribute() {
let cmd = find_cmd("cmd_required");
assert_eq!(cmd.help, "Required positional and named args");
}
#[test]
fn test_cmd_metadata_help_from_doc_comment() {
let cmd = find_cmd("cmd_doc_comment");
assert_eq!(cmd.help, "Short help from doc comment");
assert_eq!(cmd.details, "Long details from doc comment");
}
#[test]
fn test_cmd_metadata_name_override() {
let cmd = find_cmd("renamed-command");
assert!(cmd.name == "renamed-command");
assert!(
clish::inventory::iter::<CommandEntry>()
.find(|c| c.name == "original_name")
.is_none(),
"original_name should not be in inventory when name= is used"
);
}
#[test]
fn test_cmd_metadata_aliases() {
let cmd = find_cmd("cmd_aliases");
assert_eq!(cmd.aliases, &["al1", "al2"]);
}
#[test]
fn test_cmd_metadata_hidden() {
let cmd = find_cmd("cmd_hidden_cmd");
assert!(cmd.hidden);
}
#[test]
fn test_cmd_metadata_not_hidden() {
let cmd = find_cmd("cmd_required");
assert!(!cmd.hidden);
}
#[test]
fn test_cmd_metadata_deprecated() {
let cmd = find_cmd("cmd_deprecated_cmd");
assert!(cmd.deprecated);
assert_eq!(cmd.deprecation_note, "use cmd_active instead");
}
#[test]
fn test_cmd_metadata_not_deprecated() {
let cmd = find_cmd("cmd_required");
assert!(!cmd.deprecated);
assert!(cmd.deprecation_note.is_empty());
}
#[test]
fn test_required_positional_and_named() {
run_ok("cmd_required", &["world", "--greeting", "hello"]);
}
#[test]
fn test_optional_positional_present() {
run_ok("cmd_optional_pos", &["alice"]);
}
#[test]
fn test_optional_positional_absent() {
run_ok("cmd_optional_pos_none", &[]);
}
#[test]
fn test_variadic_positional() {
run_ok("cmd_variadic", &["a", "b", "c"]);
}
#[test]
fn test_variadic_positional_empty() {
run_ok("cmd_empty_variadic", &[]);
}
#[test]
fn test_optional_named_present() {
run_ok("cmd_optional_named", &["--name", "bob"]);
}
#[test]
fn test_optional_named_absent() {
run_ok("cmd_optional_named_absent", &[]);
}
#[test]
fn test_repeatable_named() {
run_ok(
"cmd_repeatable",
&["--tag", "x", "--tag", "y", "--tag", "z"],
);
}
#[test]
fn test_repeatable_named_empty() {
run_ok("cmd_empty_repeatable", &[]);
}
#[test]
fn test_flag_present() {
run_ok("cmd_flag", &["--verbose"]);
}
#[test]
fn test_flag_absent() {
run_ok("cmd_flag_absent", &[]);
}
#[test]
fn test_parsed_types() {
run_ok("cmd_parsed", &["42", "--port", "8080"]);
}
#[test]
fn test_short_alias_named() {
run_ok("cmd_short", &["foo", "-v", "yes"]);
}
#[test]
fn test_equals_form() {
run_ok("cmd_equals", &["--value=direct"]);
}
#[test]
fn test_custom_param_name() {
run_ok("cmd_param_name", &["--custom-name", "val"]);
}
#[test]
fn test_param_short() {
run_ok("cmd_short", &["foo", "-v", "yes"]);
}
#[test]
fn test_param_default() {
run_ok("cmd_default", &[]);
}
#[test]
fn test_param_env() {
const ENV_VAR: &str = "CLISH_TEST_ENV_FALLBACK_VAR";
unsafe { std::env::set_var(ENV_VAR, "from_env") };
run_ok("cmd_env", &[]);
unsafe { std::env::remove_var(ENV_VAR) };
}
#[test]
fn test_param_choices_valid() {
run_ok("cmd_choices", &["--mode", "fast"]);
}
#[test]
fn test_param_choices_invalid() {
let err = run_err("cmd_choices", &["--mode", "invalid"]);
assert!(
err.contains("invalid choice") || err.contains("expected one of"),
"got: {err}"
);
}
#[test]
fn test_param_choices_via_short() {
run_ok("cmd_short", &["foo", "-v", "yes"]);
}
#[test]
fn test_param_hidden_still_works() {
run_ok("cmd_hidden_param", &["visible", "--secret", "shh"]);
}
#[test]
fn test_param_placeholder_appears_in_help() {
let cmd = find_cmd("cmd_placeholder");
let param = cmd.params.iter().find(|p| p.name == "name").unwrap();
assert_eq!(param.placeholder, "");
}
#[test]
fn test_long_equals() {
run_ok("cmd_equals", &["--value=direct"]);
}
#[test]
fn test_long_space() {
run_ok("cmd_equals", &["--value", "direct"]);
}
#[test]
fn test_short_space() {
run_ok("cmd_short", &["foo", "-v", "yes"]);
}
#[test]
fn test_bundled_flags() {
run_ok("cmd_bundled", &["-abc"]);
}
#[test]
fn test_double_dash() {
run_ok("cmd_double_dash", &["--", "a", "--b", "c"]);
}
#[test]
fn test_double_dash_with_positional_before() {
run_ok("cmd_double_dash", &["a", "--", "--b", "c"]);
}
#[test]
fn test_missing_required_positional() {
let err = run_err("cmd_required", &["--greeting", "hello"]);
assert!(
err.contains("missing argument") || err.contains("MissingArgument"),
"got: {err}"
);
}
#[test]
fn test_missing_required_named() {
let err = run_err("cmd_required", &["world"]);
assert!(err.contains("missing value"), "got: {err}");
}
#[test]
fn test_unknown_option() {
let err = run_err(
"cmd_required",
&["world", "--greeting", "hello", "--unknown"],
);
assert!(err.contains("unknown option"), "got: {err}");
}
#[test]
fn test_unknown_short_flag() {
let err = run_err("cmd_required", &["world", "--greeting", "hello", "-Z"]);
assert!(err.contains("unknown option"), "got: {err}");
}
#[test]
fn test_invalid_value_parse() {
let err = run_err("cmd_parsed", &["42", "--port", "not_a_number"]);
assert!(
err.contains("invalid value") || err.contains("invalid"),
"got: {err}"
);
}
#[test]
fn test_invalid_positional_parse() {
let err = run_err("cmd_parsed", &["not_a_number", "--port", "8080"]);
assert!(
err.contains("invalid value") || err.contains("invalid"),
"got: {err}"
);
}
#[test]
fn test_conflicts_error() {
let err = run_err("cmd_conflicts", &["-v", "-q"]);
assert!(
err.contains("conflict") || err.contains("cannot be used together"),
"got: {err}"
);
}
#[test]
fn test_requires_error() {
let err = run_err("cmd_requires", &["--port", "3000"]);
assert!(err.contains("requires"), "got: {err}");
}
#[test]
fn test_missing_value_for_named() {
let err = run_err("cmd_parsed", &["42", "--port"]);
assert!(err.contains("missing value"), "got: {err}");
}
#[test]
fn test_empty_variadic() {
run_ok("cmd_empty_variadic", &[]);
}
#[test]
fn test_param_entry_kinds() {
let cmd = find_cmd("cmd_required");
assert_eq!(cmd.params[0].kind, "positional");
assert_eq!(cmd.params[1].kind, "named");
}
#[test]
fn test_param_entry_flag_kind() {
let cmd = find_cmd("cmd_flag");
assert_eq!(cmd.params[0].kind, "flag");
}
#[test]
fn test_param_entry_optional_positional_kind() {
let cmd = find_cmd("cmd_optional_pos");
assert_eq!(cmd.params[0].kind, "positional_optional");
}
#[test]
fn test_param_entry_variadic_kind() {
let cmd = find_cmd("cmd_variadic");
assert_eq!(cmd.params[0].kind, "positional_variadic");
}
#[test]
fn test_param_entry_optional_named_kind() {
let cmd = find_cmd("cmd_optional_named");
assert_eq!(cmd.params[0].kind, "named");
}
#[test]
fn test_param_entry_repeatable_kind() {
let cmd = find_cmd("cmd_repeatable");
assert_eq!(cmd.params[0].kind, "named");
}
#[test]
fn test_param_entry_short() {
let cmd = find_cmd("cmd_short");
let verbose = cmd.params.iter().find(|p| p.name == "verbose").unwrap();
assert_eq!(verbose.short, 'v');
}
#[test]
fn test_param_entry_no_short() {
let cmd = find_cmd("cmd_required");
let greeting = cmd.params.iter().find(|p| p.name == "greeting").unwrap();
assert_eq!(greeting.short, '\0');
}
#[test]
fn test_name_override_in_inventory() {
let cmd = find_cmd("renamed-command");
assert_eq!(cmd.name, "renamed-command");
}
#[test]
fn test_oneshot_compatible_command() {
let cmd = find_cmd("cmd_required");
assert_eq!(cmd.name, "cmd_required");
assert!(cmd.aliases.is_empty());
assert!(!cmd.hidden);
assert!(!cmd.deprecated);
}
#[test]
fn test_deprecated_command_still_runs() {
run_ok("cmd_deprecated_cmd", &["dep_ok"]);
}
#[command(
help = "Mixed flags and named options",
param(x, short = 'x'),
param(y, short = 'y'),
param(z, short = 'z')
)]
fn cmd_xyz_flags(x: bool, y: bool, z: bool) {
assert!(x, "expected x=true");
assert!(y, "expected y=true");
assert!(z, "expected z=true");
}
#[command(
help = "Partial flags",
param(a, short = 'a'),
param(b, short = 'b'),
param(c, short = 'c')
)]
fn cmd_partial_flags(a: bool, b: bool, c: bool) {
assert!(a, "expected a=true");
assert!(b, "expected b=true");
assert!(!c, "expected c=false");
}
#[test]
fn test_bundled_three_flags() {
run_ok("cmd_xyz_flags", &["-xyz"]);
}
#[test]
fn test_bundled_reverse_order() {
let cmd = find_cmd("cmd_xyz_flags");
let args: Vec<String> = vec!["-zyx"].iter().map(|s| s.to_string()).collect();
let result = (cmd.run)(&args);
assert!(result.is_ok(), "bundled -zyx failed: {result:?}");
}
#[test]
fn test_bundled_partial_flags() {
run_ok("cmd_partial_flags", &["-ab"]);
}
#[test]
fn test_bundled_flag_with_unknown_char_fails() {
let err = run_err("cmd_xyz_flags", &["-xyzQ"]);
assert!(
err.contains("unknown option"),
"expected unknown option error, got: {err}"
);
}
#[test]
fn test_bundled_flag_named_option_in_bundle_fails() {
let err = run_err("cmd_short", &["foo", "-av"]);
assert!(
err.contains("missing value") || err.contains("unknown option"),
"expected missing value or unknown option error, got: {err}"
);
}
#[test]
fn test_individual_short_flag() {
run_ok("cmd_xyz_flags", &["-x", "-y", "-z"]);
}
#[test]
fn test_individual_short_flag_mixed_with_long() {
let cmd = find_cmd("cmd_xyz_flags");
let args: Vec<String> = vec!["-x", "--y", "-z"]
.iter()
.map(|s| s.to_string())
.collect();
let result = (cmd.run)(&args);
assert!(result.is_ok(), "mixed short+long flags failed: {result:?}");
}
#[test]
fn test_short_named_option_separate_arg() {
run_ok("cmd_short", &["foo", "-v", "yes"]);
}
#[test]
fn test_short_named_option_with_long_positional() {
run_ok("cmd_short", &["foo", "--verbose", "yes"]);
}
#[test]
fn test_short_flag_with_long_positional_before() {
run_ok("cmd_bundled", &["-abc"]);
}
#[test]
fn test_short_flag_not_present() {
run_ok("cmd_flag_absent", &[]);
}
#[test]
fn test_bundled_single_versus_bundle_equivalence() {
run_ok("cmd_partial_flags", &["-ab"]);
run_ok("cmd_partial_flags", &["-a", "-b"]);
}
#[test]
fn test_short_flag_after_positional() {
run_ok("cmd_bundled", &["-ab", "-c"]);
}
#[test]
fn test_named_option_after_double_dash_is_positional() {
run_ok("cmd_double_dash", &["--", "a", "--b", "c"]);
}
#[test]
fn test_param_entry_all_fields_for_cmd_required() {
let cmd = find_cmd("cmd_required");
let name = cmd.params.iter().find(|p| p.name == "name").unwrap();
assert_eq!(name.kind, "positional");
assert_eq!(name.help, "");
assert_eq!(name.details, "");
assert_eq!(name.short, '\0');
assert_eq!(name.placeholder, "");
assert!(!name.hide);
assert_eq!(name.default, "");
assert_eq!(name.env, "");
assert!(name.choices.is_empty());
assert!(name.conflicts_with.is_empty());
assert!(name.requires.is_empty());
assert_eq!(name.value_hint, "");
let greeting = cmd.params.iter().find(|p| p.name == "greeting").unwrap();
assert_eq!(greeting.kind, "named");
assert_eq!(greeting.help, "");
assert_eq!(greeting.details, "");
assert_eq!(greeting.short, '\0');
assert_eq!(greeting.placeholder, "");
assert!(!greeting.hide);
assert_eq!(greeting.default, "");
assert_eq!(greeting.env, "");
assert!(greeting.choices.is_empty());
assert!(greeting.conflicts_with.is_empty());
assert!(greeting.requires.is_empty());
assert_eq!(greeting.value_hint, "");
}
#[test]
fn test_inventory_contains_all_commands() {
let names: Vec<&str> = clish::inventory::iter::<CommandEntry>()
.map(|c| c.name)
.collect();
assert!(
names.contains(&"cmd_required"),
"should contain cmd_required, got: {names:?}"
);
assert!(
names.contains(&"cmd_variadic"),
"should contain cmd_variadic, got: {names:?}"
);
assert!(
names.contains(&"cmd_flag"),
"should contain cmd_flag, got: {names:?}"
);
assert!(
names.contains(&"cmd_parsed"),
"should contain cmd_parsed, got: {names:?}"
);
}