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
```
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.

View File

@ -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<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(
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,

View File

@ -52,6 +52,74 @@ fn module_recipes_can_be_run_as_subcommands() {
.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]
fn assignments_are_evaluated_in_modules() {
Test::new()