Skip to content

Commit

Permalink
Recipes can be invoked with path syntax (#1809)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey authored Dec 31, 2023
1 parent 743ab2f commit 5c3b72a
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 14 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
42 changes: 28 additions & 14 deletions src/justfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -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()?;
Expand All @@ -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(),
Expand All @@ -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() {
Expand All @@ -273,7 +287,7 @@ impl<'src> Justfile<'src> {
Self::run_recipe(
&context,
invocation.recipe,
invocation.arguments,
&invocation.arguments,
&dotenv,
search,
&mut ran,
Expand Down Expand Up @@ -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);
Expand All @@ -327,7 +341,7 @@ impl<'src> Justfile<'src> {
Invocation {
settings: &module.settings,
recipe,
arguments: &[],
arguments: Vec::new(),
scope,
},
depth,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
68 changes: 68 additions & 0 deletions tests/modules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 5c3b72a

Please sign in to comment.