diff --git a/completions/just.elvish b/completions/just.elvish index 41524df..18382ea 100644 --- a/completions/just.elvish +++ b/completions/just.elvish @@ -33,10 +33,10 @@ set edit:completion:arg-completer[just] = {|@words| cand -c 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set' cand --command 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set' cand --completions 'Print shell completion script for ' - cand -l 'List available recipes and their arguments' - cand --list 'List available recipes and their arguments' - cand -s 'Show information about ' - cand --show 'Show information about ' + cand -l 'List available recipes' + cand --list 'List available recipes' + cand -s 'Show recipe at ' + cand --show 'Show recipe at ' cand --dotenv-filename 'Search for environment file named instead of `.env`' cand -E 'Load as environment file instead of searching for one' cand --dotenv-path 'Load as environment file instead of searching for one' diff --git a/completions/just.fish b/completions/just.fish index 4a4615d..7b5a1b4 100644 --- a/completions/just.fish +++ b/completions/just.fish @@ -48,8 +48,8 @@ complete -c just -l shell-arg -d 'Invoke shell with as an argument' complete -c just -s d -l working-directory -d 'Use as working directory. --justfile must also be set' -r -F complete -c just -s c -l command -d 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set' -r complete -c just -l completions -d 'Print shell completion script for ' -r -f -a "{bash '',elvish '',fish '',powershell '',zsh ''}" -complete -c just -s l -l list -d 'List available recipes and their arguments' -r -complete -c just -s s -l show -d 'Show information about ' -r +complete -c just -s l -l list -d 'List available recipes' -r +complete -c just -s s -l show -d 'Show recipe at ' -r complete -c just -l dotenv-filename -d 'Search for environment file named instead of `.env`' -r complete -c just -s E -l dotenv-path -d 'Load as environment file instead of searching for one' -r -F complete -c just -l timestamp-format -d 'Timestamp format string' -r diff --git a/completions/just.powershell b/completions/just.powershell index 88098f0..5413bb1 100644 --- a/completions/just.powershell +++ b/completions/just.powershell @@ -36,10 +36,10 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock { [CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set') [CompletionResult]::new('--command', 'command', [CompletionResultType]::ParameterName, 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set') [CompletionResult]::new('--completions', 'completions', [CompletionResultType]::ParameterName, 'Print shell completion script for ') - [CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'List available recipes and their arguments') - [CompletionResult]::new('--list', 'list', [CompletionResultType]::ParameterName, 'List available recipes and their arguments') - [CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Show information about ') - [CompletionResult]::new('--show', 'show', [CompletionResultType]::ParameterName, 'Show information about ') + [CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'List available recipes') + [CompletionResult]::new('--list', 'list', [CompletionResultType]::ParameterName, 'List available recipes') + [CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Show recipe at ') + [CompletionResult]::new('--show', 'show', [CompletionResultType]::ParameterName, 'Show recipe at ') [CompletionResult]::new('--dotenv-filename', 'dotenv-filename', [CompletionResultType]::ParameterName, 'Search for environment file named instead of `.env`') [CompletionResult]::new('-E', 'E ', [CompletionResultType]::ParameterName, 'Load as environment file instead of searching for one') [CompletionResult]::new('--dotenv-path', 'dotenv-path', [CompletionResultType]::ParameterName, 'Load as environment file instead of searching for one') diff --git a/completions/just.zsh b/completions/just.zsh index b8e0a2c..1c1fe92 100644 --- a/completions/just.zsh +++ b/completions/just.zsh @@ -31,10 +31,10 @@ _just() { '*-c+[Run an arbitrary command with the working directory, \`.env\`, overrides, and exports set]: : ' \ '*--command=[Run an arbitrary command with the working directory, \`.env\`, overrides, and exports set]: : ' \ '*--completions=[Print shell completion script for ]:SHELL:(bash elvish fish powershell zsh)' \ -'-l+[List available recipes and their arguments]' \ -'--list=[List available recipes and their arguments]' \ -'-s+[Show information about ]: :(_just_commands)' \ -'--show=[Show information about ]: :(_just_commands)' \ +'()-l+[List available recipes]' \ +'()--list=[List available recipes]' \ +'-s+[Show recipe at ]: :(_just_commands)' \ +'--show=[Show recipe at ]: :(_just_commands)' \ '(-E --dotenv-path)--dotenv-filename=[Search for environment file named instead of \`.env\`]: : ' \ '-E+[Load as environment file instead of searching for one]: :_files' \ '--dotenv-path=[Load as environment file instead of searching for one]: :_files' \ diff --git a/src/completions.rs b/src/completions.rs index bc8564b..23f2902 100644 --- a/src/completions.rs +++ b/src/completions.rs @@ -47,10 +47,10 @@ pub(crate) const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[ r"'*--set=[Override with ]: :(_just_variables)' \", ), ( - r"'()-s+[Show information about ]:RECIPE: ' \ -'()--show=[Show information about ]:RECIPE: ' \", - r"'-s+[Show information about ]: :(_just_commands)' \ -'--show=[Show information about ]: :(_just_commands)' \", + r"'()-s+[Show recipe at ]:PATH: ' \ +'()--show=[Show recipe at ]:PATH: ' \", + r"'-s+[Show recipe at ]: :(_just_commands)' \ +'--show=[Show recipe at ]: :(_just_commands)' \", ), ( "'*::ARGUMENTS -- Overrides and recipe(s) to run, defaulting to the first recipe in the \ diff --git a/src/config.rs b/src/config.rs index 9f31eec..cfe569c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,6 +2,7 @@ use { super::*, clap::{ builder::{styling::AnsiColor, FalseyValueParser, PossibleValuesParser, Styles}, + parser::ValuesRef, value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command, }, }; @@ -421,7 +422,8 @@ impl Config { .num_args(0..) .value_name("PATH") .action(ArgAction::Set) - .help("List available recipes and their arguments"), + .conflicts_with(arg::ARGUMENTS) + .help("List available recipes"), ) .arg( Arg::new(cmd::GROUPS) @@ -439,10 +441,11 @@ impl Config { Arg::new(cmd::SHOW) .short('s') .long("show") + .num_args(1..) .action(ArgAction::Set) - .value_name("RECIPE") + .value_name("PATH") .conflicts_with(arg::ARGUMENTS) - .help("Show information about "), + .help("Show recipe at "), ) .arg( Arg::new(cmd::SUMMARY) @@ -557,6 +560,18 @@ impl Config { } } + fn parse_module_path(path: ValuesRef) -> ConfigResult { + path + .clone() + .map(|s| (*s).as_str()) + .collect::>() + .as_slice() + .try_into() + .map_err(|()| ConfigError::ModulePath { + path: path.cloned().collect(), + }) + } + fn search_config(matches: &ArgMatches, positional: &Positional) -> ConfigResult { if matches.get_flag(arg::GLOBAL_JUSTFILE) { return Ok(SearchConfig::GlobalJustfile); @@ -676,22 +691,16 @@ impl Config { Subcommand::Init } else if let Some(path) = matches.get_many::(cmd::LIST) { Subcommand::List { - path: path - .clone() - .map(|s| (*s).as_str()) - .collect::>() - .as_slice() - .try_into() - .map_err(|()| ConfigError::ListPath { - path: path.cloned().collect(), - })?, + path: Self::parse_module_path(path)?, } } else if matches.get_flag(cmd::GROUPS) { Subcommand::Groups } else if matches.get_flag(cmd::MAN) { Subcommand::Man - } else if let Some(name) = matches.get_one::(cmd::SHOW).map(Into::into) { - Subcommand::Show { name } + } else if let Some(path) = matches.get_many::(cmd::SHOW) { + Subcommand::Show { + path: Self::parse_module_path(path)?, + } } else if matches.get_flag(cmd::EVALUATE) { if positional.arguments.len() > 1 { return Err(ConfigError::SubcommandArguments { @@ -1298,36 +1307,42 @@ mod tests { test! { name: subcommand_list_long, args: ["--list"], - subcommand: Subcommand::List{ path: ModulePath{ path: Vec::new(), spaced: false } }, + subcommand: Subcommand::List{ path: ModulePath { path: Vec::new(), spaced: false } }, } test! { name: subcommand_list_short, args: ["-l"], - subcommand: Subcommand::List{ path: ModulePath{ path: Vec::new(), spaced: false } }, + subcommand: Subcommand::List{ path: ModulePath { path: Vec::new(), spaced: false } }, } test! { name: subcommand_list_arguments, args: ["--list", "bar"], - subcommand: Subcommand::List{ path: ModulePath{ path: vec!["bar".into()], spaced: false } }, + subcommand: Subcommand::List{ path: ModulePath { path: vec!["bar".into()], spaced: false } }, } test! { name: subcommand_show_long, args: ["--show", "build"], - subcommand: Subcommand::Show { name: String::from("build") }, + subcommand: Subcommand::Show { path: ModulePath { path: vec!["build".into()], spaced: false } }, } test! { name: subcommand_show_short, args: ["-s", "build"], - subcommand: Subcommand::Show { name: String::from("build") }, + subcommand: Subcommand::Show { path: ModulePath { path: vec!["build".into()], spaced: false } }, } - error! { - name: subcommand_show_no_arg, - args: ["--show"], + test! { + name: subcommand_show_multiple_args, + args: ["--show", "foo", "bar"], + subcommand: Subcommand::Show { + path: ModulePath { + path: vec!["foo".into(), "bar".into()], + spaced: true, + }, + }, } test! { @@ -1602,20 +1617,6 @@ mod tests { }, } - error_matches! { - name: show_arguments, - args: ["--show", "foo", "bar"], - error: error, - check: { - assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict); - assert_eq!(error.context().collect::>(), vec![ - (ContextKind::InvalidArg, &ContextValue::String("--show ".into())), - (ContextKind::PriorArg, &ContextValue::String("[ARGUMENTS]...".into())), - (ContextKind::Usage, &ContextValue::StyledStr("\u{1b}[33mUsage:\u{1b}[0m \u{1b}[32mjust\u{1b}[0m \u{1b}[32m--show\u{1b}[0m\u{1b}[32m \u{1b}[0m\u{1b}[32m\u{1b}[0m \u{1b}[32m[ARGUMENTS]...\u{1b}[0m".into())), - ]); - }, - } - error! { name: summary_arguments, args: ["--summary", "bar"], diff --git a/src/config_error.rs b/src/config_error.rs index e59767b..6935b81 100644 --- a/src/config_error.rs +++ b/src/config_error.rs @@ -11,7 +11,7 @@ pub(crate) enum ConfigError { ))] Internal { message: String }, #[snafu(display("Invalid module path `{}`", path.join(" ")))] - ListPath { path: Vec }, + ModulePath { path: Vec }, #[snafu(display( "Path-prefixed recipes may not be used with `--working-directory` or `--justfile`." ))] diff --git a/src/subcommand.rs b/src/subcommand.rs index c859376..9cf24e3 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -40,7 +40,7 @@ pub(crate) enum Subcommand { overrides: BTreeMap, }, Show { - name: String, + path: ModulePath, }, Summary, Variables, @@ -91,7 +91,7 @@ impl Subcommand { Format => Self::format(config, &search, src, ast)?, Groups => Self::groups(config, justfile), List { path } => Self::list_module(config, justfile, path)?, - Show { ref name } => Self::show(config, name, justfile)?, + Show { path } => Self::show(config, justfile, path)?, Summary => Self::summary(config, justfile), Variables => Self::variables(justfile), Changelog | Completions { .. } | Edit | Init | Man | Run { .. } => unreachable!(), @@ -636,19 +636,32 @@ impl Subcommand { } } - fn show<'src>(config: &Config, name: &str, justfile: &Justfile<'src>) -> Result<(), Error<'src>> { - if let Some(alias) = justfile.get_alias(name) { - let recipe = justfile.get_recipe(alias.target.name.lexeme()).unwrap(); + fn show<'src>( + config: &Config, + mut module: &Justfile<'src>, + path: &ModulePath, + ) -> Result<(), Error<'src>> { + for name in &path.path[0..path.path.len() - 1] { + module = module + .modules + .get(name) + .ok_or_else(|| Error::UnknownSubmodule { path: path.clone() })?; + } + + let name = path.path.last().unwrap(); + + if let Some(alias) = module.get_alias(name) { + let recipe = module.get_recipe(alias.target.name.lexeme()).unwrap(); println!("{alias}"); println!("{}", recipe.color_display(config.color.stdout())); Ok(()) - } else if let Some(recipe) = justfile.get_recipe(name) { + } else if let Some(recipe) = module.get_recipe(name) { println!("{}", recipe.color_display(config.color.stdout())); Ok(()) } else { Err(Error::UnknownRecipes { recipes: vec![name.to_owned()], - suggestion: justfile.suggest_recipe(name), + suggestion: module.suggest_recipe(name), }) } } diff --git a/tests/show.rs b/tests/show.rs index a822d74..e9fe5e6 100644 --- a/tests/show.rs +++ b/tests/show.rs @@ -100,3 +100,27 @@ a Z="\t z": stderr: "error: Justfile does not contain recipe `fooooooo`.\n", status: EXIT_FAILURE, } + +#[test] +fn show_recipe_at_path() { + Test::new() + .write("foo.just", "bar:\n @echo MODULE") + .justfile( + " + mod foo + ", + ) + .test_round_trip(false) + .args(["--unstable", "--show", "foo::bar"]) + .stdout("bar:\n @echo MODULE\n") + .run(); +} + +#[test] +fn show_invalid_path() { + Test::new() + .args(["--show", "$hello"]) + .stderr("error: Invalid module path `$hello`\n") + .status(1) + .run(); +}