Recipes can be invoked with path syntax (#1809)

This commit is contained in:
Casey Rodarmor 2023-12-31 14:03:49 -08:00 committed by GitHub
parent 743ab2fc82
commit 5c3b72a121
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 103 additions and 14 deletions

View File

@ -2669,6 +2669,13 @@ $ just --unstable bar b
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`, 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, `foo/mod.just`, `foo/justfile`, and `foo/.justfile`. In the latter two cases,
the module file may have any capitalization. the module file may have any capitalization.

View File

@ -2,7 +2,7 @@ use {super::*, serde::Serialize};
#[derive(Debug)] #[derive(Debug)]
struct Invocation<'src: 'run, 'run> { struct Invocation<'src: 'run, 'run> {
arguments: &'run [&'run str], arguments: Vec<&'run str>,
recipe: &'run Recipe<'src>, recipe: &'run Recipe<'src>,
settings: &'run Settings<'src>, settings: &'run Settings<'src>,
scope: &'run Scope<'src, 'run>, 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() arguments.iter().map(String::as_str).collect()
} else if let Some(recipe) = &self.default { } else if let Some(recipe) = &self.default {
recipe.check_can_be_default_recipe()?; recipe.check_can_be_default_recipe()?;
@ -220,15 +220,29 @@ impl<'src> Justfile<'src> {
return Err(Error::NoDefaultRecipe); return Err(Error::NoDefaultRecipe);
}; };
let arguments = argvec.as_slice();
let mut missing = Vec::new(); let mut missing = Vec::new();
let mut invocations = Vec::new(); let mut invocations = Vec::new();
let mut remaining = arguments;
let mut scopes = BTreeMap::new(); let mut scopes = BTreeMap::new();
let arena: Arena<Scope> = Arena::new(); let arena: Arena<Scope> = 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( if let Some((invocation, consumed)) = self.invocation(
0, 0,
&mut Vec::new(), &mut Vec::new(),
@ -241,12 +255,12 @@ impl<'src> Justfile<'src> {
first, first,
rest, rest,
)? { )? {
rest = &rest[consumed..]; remaining = rest[consumed..].to_vec();
invocations.push(invocation); invocations.push(invocation);
} else { } else {
missing.push((*first).to_owned()); missing.push(first.to_string());
remaining = rest.to_vec();
} }
remaining = rest;
} }
if !missing.is_empty() { if !missing.is_empty() {
@ -273,7 +287,7 @@ impl<'src> Justfile<'src> {
Self::run_recipe( Self::run_recipe(
&context, &context,
invocation.recipe, invocation.recipe,
invocation.arguments, &invocation.arguments,
&dotenv, &dotenv,
search, search,
&mut ran, &mut ran,
@ -306,7 +320,7 @@ impl<'src> Justfile<'src> {
search: &'run Search, search: &'run Search,
parent: &'run Scope<'src, 'run>, parent: &'run Scope<'src, 'run>,
first: &'run str, first: &'run str,
rest: &'run [&'run str], rest: &[&'run str],
) -> RunResult<'src, Option<(Invocation<'src, 'run>, usize)>> { ) -> RunResult<'src, Option<(Invocation<'src, 'run>, usize)>> {
if let Some(module) = self.modules.get(first) { if let Some(module) = self.modules.get(first) {
path.push(first); path.push(first);
@ -327,7 +341,7 @@ impl<'src> Justfile<'src> {
Invocation { Invocation {
settings: &module.settings, settings: &module.settings,
recipe, recipe,
arguments: &[], arguments: Vec::new(),
scope, scope,
}, },
depth, depth,
@ -352,7 +366,7 @@ impl<'src> Justfile<'src> {
if recipe.parameters.is_empty() { if recipe.parameters.is_empty() {
Ok(Some(( Ok(Some((
Invocation { Invocation {
arguments: &[], arguments: Vec::new(),
recipe, recipe,
scope: parent, scope: parent,
settings: &self.settings, settings: &self.settings,
@ -373,7 +387,7 @@ impl<'src> Justfile<'src> {
} }
Ok(Some(( Ok(Some((
Invocation { Invocation {
arguments: &rest[..argument_count], arguments: rest[..argument_count].to_vec(),
recipe, recipe,
scope: parent, scope: parent,
settings: &self.settings, settings: &self.settings,

View File

@ -52,6 +52,74 @@ fn module_recipes_can_be_run_as_subcommands() {
.run(); .run();
} }
#[test]
fn module_recipes_can_be_run_with_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_with_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 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] #[test]
fn assignments_are_evaluated_in_modules() { fn assignments_are_evaluated_in_modules() {
Test::new() Test::new()