From 9c2ef95a3d007aa0b5de403eb538c5703268ca1e Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sun, 31 Dec 2023 13:52:20 -0800 Subject: [PATCH 1/5] Recipes can be invoked with path syntax --- src/justfile.rs | 42 +++++++++++++++++++++++------------ tests/modules.rs | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/src/justfile.rs b/src/justfile.rs index 6105e154ae..35c83a5646 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -2,7 +2,7 @@ use {super::*, serde::Serialize}; #[derive(Debug)] struct Invocation<'src: 'run, 'run> { - arguments: &'run [&'run str], + arguments: Vec<&'run str>, recipe: &'run Recipe<'src>, settings: &'run Settings<'src>, scope: &'run Scope<'src, 'run>, @@ -209,7 +209,7 @@ impl<'src> Justfile<'src> { _ => {} } - let argvec: Vec<&str> = if !arguments.is_empty() { + let mut remaining: Vec<&str> = if !arguments.is_empty() { arguments.iter().map(String::as_str).collect() } else if let Some(recipe) = &self.default { recipe.check_can_be_default_recipe()?; @@ -220,15 +220,29 @@ impl<'src> Justfile<'src> { return Err(Error::NoDefaultRecipe); }; - let arguments = argvec.as_slice(); - let mut missing = Vec::new(); let mut invocations = Vec::new(); - let mut remaining = arguments; let mut scopes = BTreeMap::new(); let arena: Arena = Arena::new(); - while let Some((first, mut rest)) = remaining.split_first() { + while let Some(first) = remaining.first().copied() { + if first.contains("::") { + if first.starts_with(':') || first.ends_with(':') || first.contains(":::") { + missing.push(first.to_string()); + remaining = remaining[1..].to_vec(); + continue; + } + + remaining = first + .split("::") + .chain(remaining[1..].iter().copied()) + .collect::>(); + + continue; + } + + let rest = &remaining[1..]; + if let Some((invocation, consumed)) = self.invocation( 0, &mut Vec::new(), @@ -241,12 +255,12 @@ impl<'src> Justfile<'src> { first, rest, )? { - rest = &rest[consumed..]; + remaining = rest[consumed..].to_vec(); invocations.push(invocation); } else { - missing.push((*first).to_owned()); + missing.push(first.to_string()); + remaining = rest.to_vec(); } - remaining = rest; } if !missing.is_empty() { @@ -273,7 +287,7 @@ impl<'src> Justfile<'src> { Self::run_recipe( &context, invocation.recipe, - invocation.arguments, + &invocation.arguments, &dotenv, search, &mut ran, @@ -306,7 +320,7 @@ impl<'src> Justfile<'src> { search: &'run Search, parent: &'run Scope<'src, 'run>, first: &'run str, - rest: &'run [&'run str], + rest: &[&'run str], ) -> RunResult<'src, Option<(Invocation<'src, 'run>, usize)>> { if let Some(module) = self.modules.get(first) { path.push(first); @@ -327,7 +341,7 @@ impl<'src> Justfile<'src> { Invocation { settings: &module.settings, recipe, - arguments: &[], + arguments: Vec::new(), scope, }, depth, @@ -352,7 +366,7 @@ impl<'src> Justfile<'src> { if recipe.parameters.is_empty() { Ok(Some(( Invocation { - arguments: &[], + arguments: Vec::new(), recipe, scope: parent, settings: &self.settings, @@ -373,7 +387,7 @@ impl<'src> Justfile<'src> { } Ok(Some(( Invocation { - arguments: &rest[..argument_count], + arguments: rest[..argument_count].to_vec(), recipe, scope: parent, settings: &self.settings, diff --git a/tests/modules.rs b/tests/modules.rs index 533fd67a82..b378aa97d1 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -52,6 +52,63 @@ fn module_recipes_can_be_run_as_subcommands() { .run(); } +#[test] +fn module_recipes_can_be_run_using_path_syntax() { + Test::new() + .write("foo.just", "foo:\n @echo FOO") + .justfile( + " + mod foo + ", + ) + .test_round_trip(false) + .arg("--unstable") + .arg("foo::foo") + .stdout("FOO\n") + .run(); +} + +#[test] +fn nested_module_recipes_can_be_run_using_path_syntax() { + Test::new() + .write("foo.just", "mod bar") + .write("bar.just", "baz:\n @echo BAZ") + .justfile( + " + mod foo + ", + ) + .test_round_trip(false) + .arg("--unstable") + .arg("foo::bar::baz") + .stdout("BAZ\n") + .run(); +} + +#[test] +fn invalid_path_syntax() { + Test::new() + .test_round_trip(false) + .arg(":foo::foo") + .stderr("error: Justfile does not contain recipe `:foo::foo`.\n") + .status(EXIT_FAILURE) + .run(); + + Test::new() + .test_round_trip(false) + .arg("foo::foo:") + .stderr("error: Justfile does not contain recipe `foo::foo:`.\n") + .status(EXIT_FAILURE) + .run(); + + Test::new() + .test_round_trip(false) + .arg("foo:::foo") + .stderr("error: Justfile does not contain recipe `foo:::foo`.\n") + .status(EXIT_FAILURE) + .run(); +} + #[test] fn assignments_are_evaluated_in_modules() { Test::new() From ae3f72b050d14023e96b5f7b29b9dc8e6a263cb7 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sun, 31 Dec 2023 13:58:19 -0800 Subject: [PATCH 2/5] Tweak --- tests/modules.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/modules.rs b/tests/modules.rs index b378aa97d1..48ec4158e5 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -109,6 +109,17 @@ fn invalid_path_syntax() { .run(); } +#[test] +fn missing_recipe_after_invalid_path() { + Test::new() + .test_round_trip(false) + .arg(":foo::foo") + .arg("bar") + .stderr("error: Justfile does not contain recipes `:foo::foo` or `bar`.\n") + .status(EXIT_FAILURE) + .run(); +} + #[test] fn assignments_are_evaluated_in_modules() { Test::new() From 6111bda8b5894194d05e7ff9fd576017f401c9ce Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sun, 31 Dec 2023 14:00:37 -0800 Subject: [PATCH 3/5] Tweak --- src/justfile.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/justfile.rs b/src/justfile.rs index 35c83a5646..8bec295688 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -236,7 +236,7 @@ impl<'src> Justfile<'src> { remaining = first .split("::") .chain(remaining[1..].iter().copied()) - .collect::>(); + .collect(); continue; } From e9a27c8173e07c13748a67236ac13d6b70571bcf Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sun, 31 Dec 2023 14:01:10 -0800 Subject: [PATCH 4/5] Tweak --- tests/modules.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/modules.rs b/tests/modules.rs index 48ec4158e5..f2ef01a488 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -53,7 +53,7 @@ fn module_recipes_can_be_run_as_subcommands() { } #[test] -fn module_recipes_can_be_run_using_path_syntax() { +fn module_recipes_can_be_run_with_path_syntax() { Test::new() .write("foo.just", "foo:\n @echo FOO") .justfile( @@ -69,7 +69,7 @@ fn module_recipes_can_be_run_using_path_syntax() { } #[test] -fn nested_module_recipes_can_be_run_using_path_syntax() { +fn nested_module_recipes_can_be_run_with_path_syntax() { Test::new() .write("foo.just", "mod bar") .write("bar.just", "baz:\n @echo BAZ") From b6159368c3c4a84f3c023ede0f72f35de092560d Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sun, 31 Dec 2023 14:02:01 -0800 Subject: [PATCH 5/5] Tweak --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index f2cd11910e..835dfbd8a1 100644 --- a/README.md +++ b/README.md @@ -2669,6 +2669,13 @@ $ just --unstable bar b B ``` +Or with path syntax: + +```sh +$ just --unstable bar::b +B +``` + If a module is named `foo`, just will search for the module file in `foo.just`, `foo/mod.just`, `foo/justfile`, and `foo/.justfile`. In the latter two cases, the module file may have any capitalization.