1
0
mirror of https://github.com/casey/just synced 2024-07-08 20:16:14 +00:00

Allow passing arguments to dependencies (#555)

Allow recipes that take parameters to be used as dependencies.
This commit is contained in:
Casey Rodarmor 2019-12-07 04:03:03 -08:00 committed by GitHub
parent 2d3134a91c
commit 0931fa8dbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 443 additions and 157 deletions

View File

@ -73,12 +73,13 @@ string : STRING
sequence : expression ',' sequence
| expression ','?
recipe : '@'? NAME parameter* ('+' parameter)? ':' dependencies? body?
recipe : '@'? NAME parameter* ('+' parameter)? ':' dependency* body?
parameter : NAME
| NAME '=' value
dependencies : NAME+
dependency : NAME
| '(' NAME expression* ')
body : INDENT line+ DEDENT

View File

@ -547,9 +547,7 @@ build target:
cd {{target}} && make
```
Other recipes may not depend on a recipe with parameters.
To pass arguments, put them after the recipe name:
To pass arguments on the command line, put them after the recipe name:
```sh
$ just build my-awesome-project
@ -557,6 +555,16 @@ Building my-awesome-project...
cd my-awesome-project && make
```
To pass arguments to a dependency, put the dependency in parentheses along with the arguments:
```make
default: (build "main")
build target:
@echo 'Building {{target}}...'
cd {{target}} && make
```
Parameters may have default values:
```make

View File

@ -3,7 +3,7 @@ use crate::common::*;
use CompilationErrorKind::*;
pub(crate) struct Analyzer<'src> {
recipes: Table<'src, Recipe<'src, Name<'src>>>,
recipes: Table<'src, UnresolvedRecipe<'src>>,
assignments: Table<'src, Assignment<'src>>,
aliases: Table<'src, Alias<'src, Name<'src>>>,
sets: Table<'src, Set<'src>>,
@ -91,7 +91,7 @@ impl<'src> Analyzer<'src> {
})
}
fn analyze_recipe(&self, recipe: &Recipe<'src, Name<'src>>) -> CompilationResult<'src, ()> {
fn analyze_recipe(&self, recipe: &UnresolvedRecipe<'src>) -> CompilationResult<'src, ()> {
if let Some(original) = self.recipes.get(recipe.name.lexeme()) {
return Err(recipe.name.token().error(DuplicateRecipe {
recipe: original.name(),
@ -125,17 +125,6 @@ impl<'src> Analyzer<'src> {
}
}
let mut dependencies = BTreeSet::new();
for dependency in &recipe.dependencies {
if dependencies.contains(dependency.lexeme()) {
return Err(dependency.token().error(DuplicateDependency {
recipe: recipe.name.lexeme(),
dependency: dependency.lexeme(),
}));
}
dependencies.insert(dependency.lexeme());
}
let mut continued = false;
for line in &recipe.body {
if !recipe.shebang && !continued {
@ -295,26 +284,6 @@ mod tests {
kind: ParameterShadowsVariable{parameter: "foo"},
}
analysis_error! {
name: dependency_has_parameters,
input: "foo arg:\nb: foo",
offset: 12,
line: 1,
column: 3,
width: 3,
kind: DependencyHasParameters{recipe: "b", dependency: "foo"},
}
analysis_error! {
name: duplicate_dependency,
input: "a b c: b c z z",
offset: 13,
line: 0,
column: 13,
width: 1,
kind: DuplicateDependency{recipe: "a", dependency: "z"},
}
analysis_error! {
name: duplicate_recipe,
input: "a:\nb:\na:",

View File

@ -62,8 +62,9 @@ pub(crate) use crate::{
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, state::State,
string_literal::StringLiteral, subcommand::Subcommand, table::Table, thunk::Thunk, token::Token,
token_kind::TokenKind, use_color::UseColor, variables::Variables, verbosity::Verbosity,
warning::Warning,
token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
verbosity::Verbosity, warning::Warning,
};
// type aliases

View File

@ -89,13 +89,6 @@ impl Display for CompilationError<'_> {
self.token.line.ordinal(),
)?;
}
DuplicateDependency { recipe, dependency } => {
writeln!(
f,
"Recipe `{}` has duplicate dependency `{}`",
recipe, dependency
)?;
}
DuplicateRecipe { recipe, first } => {
writeln!(
f,
@ -114,13 +107,28 @@ impl Display for CompilationError<'_> {
self.token.line.ordinal(),
)?;
}
DependencyHasParameters { recipe, dependency } => {
writeln!(
DependencyArgumentCountMismatch {
dependency,
found,
min,
max,
} => {
write!(
f,
"Recipe `{}` depends on `{}` which requires arguments. \
Dependencies may not require arguments",
recipe, dependency
"Dependency `{}` got {} {} but takes ",
dependency,
found,
Count("argument", found),
)?;
if min == max {
let expected = min;
writeln!(f, "{} {}", expected, Count("argument", expected))?;
} else if found < min {
writeln!(f, "at least {} {}", min, Count("argument", min))?;
} else {
writeln!(f, "at most {} {}", max, Count("argument", max))?;
}
}
ParameterShadowsVariable { parameter } => {
writeln!(

View File

@ -14,18 +14,16 @@ pub(crate) enum CompilationErrorKind<'src> {
variable: &'src str,
circle: Vec<&'src str>,
},
DependencyHasParameters {
recipe: &'src str,
DependencyArgumentCountMismatch {
dependency: &'src str,
found: usize,
min: usize,
max: usize,
},
DuplicateAlias {
alias: &'src str,
first: usize,
},
DuplicateDependency {
recipe: &'src str,
dependency: &'src str,
},
DuplicateParameter {
recipe: &'src str,
parameter: &'src str,

View File

@ -1,4 +1,23 @@
use crate::common::*;
#[derive(PartialEq, Debug)]
pub(crate) struct Dependency<'src>(pub(crate) Rc<Recipe<'src>>);
pub(crate) struct Dependency<'src> {
pub(crate) recipe: Rc<Recipe<'src>>,
pub(crate) arguments: Vec<Expression<'src>>,
}
impl<'src> Display for Dependency<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
if self.arguments.is_empty() {
write!(f, "{}", self.recipe.name())
} else {
write!(f, "({}", self.recipe.name())?;
for argument in &self.arguments {
write!(f, " {}", argument)?;
}
write!(f, ")")
}
}
}

View File

@ -206,7 +206,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
Ok(scope)
}
pub(crate) fn line_evaluator(
pub(crate) fn recipe_evaluator(
config: &'run Config,
dotenv: &'run BTreeMap<String, String>,
scope: &'run Scope<'src, 'run>,

View File

@ -5,6 +5,6 @@ use crate::common::*;
pub(crate) enum Item<'src> {
Alias(Alias<'src, Name<'src>>),
Assignment(Assignment<'src>),
Recipe(Recipe<'src, Name<'src>>),
Recipe(UnresolvedRecipe<'src>),
Set(Set<'src>),
}

View File

@ -175,7 +175,7 @@ impl<'src> Justfile<'src> {
working_directory,
};
let mut ran = empty();
let mut ran = BTreeSet::new();
for (recipe, arguments) in grouped {
self.run_recipe(&context, recipe, arguments, &dotenv, &mut ran)?
}
@ -201,17 +201,54 @@ impl<'src> Justfile<'src> {
&self,
context: &'run RecipeContext<'src, 'run>,
recipe: &Recipe<'src>,
arguments: &[&'src str],
arguments: &[&'run str],
dotenv: &BTreeMap<String, String>,
ran: &mut BTreeSet<&'src str>,
ran: &mut BTreeSet<Vec<String>>,
) -> RunResult<'src, ()> {
for Dependency(dependency) in &recipe.dependencies {
if !ran.contains(dependency.name()) {
self.run_recipe(context, dependency, &[], dotenv, ran)?;
let scope = Evaluator::evaluate_parameters(
context.config,
dotenv,
&recipe.parameters,
arguments,
&context.scope,
context.settings,
context.working_directory,
)?;
let mut evaluator = Evaluator::recipe_evaluator(
context.config,
dotenv,
&scope,
context.settings,
context.working_directory,
);
for Dependency { recipe, arguments } in &recipe.dependencies {
let mut invocation = vec![recipe.name().to_owned()];
for argument in arguments {
invocation.push(evaluator.evaluate_expression(argument)?);
}
if !ran.contains(&invocation) {
let arguments = invocation
.iter()
.skip(1)
.map(String::as_ref)
.collect::<Vec<&str>>();
self.run_recipe(context, recipe, &arguments, dotenv, ran)?;
}
}
recipe.run(context, arguments, dotenv)?;
ran.insert(recipe.name());
recipe.run(context, dotenv, scope)?;
let mut invocation = Vec::new();
invocation.push(recipe.name().to_owned());
for argument in arguments.iter().cloned() {
invocation.push(argument.to_owned());
}
ran.insert(invocation);
Ok(())
}
}

View File

@ -85,6 +85,8 @@ mod table;
mod thunk;
mod token;
mod token_kind;
mod unresolved_dependency;
mod unresolved_recipe;
mod use_color;
mod variables;
mod verbosity;

View File

@ -81,7 +81,7 @@ impl<'src> Node<'src> for Expression<'src> {
}
}
impl<'src> Node<'src> for Recipe<'src, Name<'src>> {
impl<'src> Node<'src> for UnresolvedRecipe<'src> {
fn tree(&self) -> Tree<'src> {
let mut t = Tree::atom("recipe");
@ -111,14 +111,19 @@ impl<'src> Node<'src> for Recipe<'src, Name<'src>> {
}
if !self.dependencies.is_empty() {
t = t.push(
Tree::atom("deps").extend(
self
.dependencies
.iter()
.map(|dependency| dependency.lexeme()),
),
);
let mut dependencies = Tree::atom("deps");
for dependency in &self.dependencies {
let mut d = Tree::atom(dependency.recipe.lexeme());
for argument in &dependency.arguments {
d.push_mut(argument.tree());
}
dependencies.push_mut(d);
}
t.push_mut(dependencies);
}
if !self.body.is_empty() {

View File

@ -217,7 +217,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
}
}
/// Accept a token of kind `Identifier` and parse into an `Name`
/// Accept a token of kind `Identifier` and parse into a `Name`
fn accept_name(&mut self) -> CompilationResult<'src, Option<Name<'src>>> {
if self.next_is(Identifier) {
Ok(Some(self.parse_name()?))
@ -226,6 +226,28 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
}
}
/// Accept a dependency
fn accept_dependency(&mut self) -> CompilationResult<'src, Option<UnresolvedDependency<'src>>> {
if let Some(recipe) = self.accept_name()? {
Ok(Some(UnresolvedDependency {
arguments: Vec::new(),
recipe,
}))
} else if self.accepted(ParenL)? {
let recipe = self.parse_name()?;
let mut arguments = Vec::new();
while !self.accepted(ParenR)? {
arguments.push(self.parse_expression()?);
}
Ok(Some(UnresolvedDependency { recipe, arguments }))
} else {
Ok(None)
}
}
/// Accept and return `true` if next token is of kind `kind`
fn accepted(&mut self, kind: TokenKind) -> CompilationResult<'src, bool> {
Ok(self.accept(kind)?.is_some())
@ -470,7 +492,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
&mut self,
doc: Option<&'src str>,
quiet: bool,
) -> CompilationResult<'src, Recipe<'src, Name<'src>>> {
) -> CompilationResult<'src, UnresolvedRecipe<'src>> {
let name = self.parse_name()?;
let mut positional = Vec::new();
@ -521,7 +543,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
let mut dependencies = Vec::new();
while let Some(dependency) = self.accept_name()? {
while let Some(dependency) = self.accept_dependency()? {
dependencies.push(dependency);
}
@ -934,6 +956,30 @@ mod tests {
tree: (justfile (recipe foo (deps bar baz))),
}
test! {
name: recipe_dependency_parenthesis,
text: "foo: (bar)",
tree: (justfile (recipe foo (deps bar))),
}
test! {
name: recipe_dependency_argument_string,
text: "foo: (bar 'baz')",
tree: (justfile (recipe foo (deps (bar "baz")))),
}
test! {
name: recipe_dependency_argument_identifier,
text: "foo: (bar baz)",
tree: (justfile (recipe foo (deps (bar baz)))),
}
test! {
name: recipe_dependency_argument_concatination,
text: "foo: (bar 'a' + 'b' 'c' + 'd')",
tree: (justfile (recipe foo (deps (bar (+ 'a' 'b') (+ 'c' 'd'))))),
}
test! {
name: recipe_line_single,
text: "foo:\n bar",

View File

@ -67,8 +67,8 @@ impl<'src, D> Recipe<'src, D> {
pub(crate) fn run<'run>(
&self,
context: &RecipeContext<'src, 'run>,
arguments: &[&'src str],
dotenv: &BTreeMap<String, String>,
scope: Scope<'src, 'run>,
) -> RunResult<'src, ()> {
let config = &context.config;
@ -82,17 +82,7 @@ impl<'src, D> Recipe<'src, D> {
);
}
let scope = Evaluator::evaluate_parameters(
context.config,
dotenv,
&self.parameters,
arguments,
&context.scope,
context.settings,
context.working_directory,
)?;
let mut evaluator = Evaluator::line_evaluator(
let mut evaluator = Evaluator::recipe_evaluator(
context.config,
dotenv,
&scope,
@ -300,25 +290,6 @@ impl<'src, D> Recipe<'src, D> {
}
}
impl<'src> Recipe<'src, Name<'src>> {
pub(crate) fn resolve(self, resolved: Vec<Dependency<'src>>) -> Recipe<'src> {
assert_eq!(self.dependencies.len(), resolved.len());
for (name, resolved) in self.dependencies.iter().zip(&resolved) {
assert_eq!(name.lexeme(), resolved.0.name.lexeme());
}
Recipe {
dependencies: resolved,
doc: self.doc,
body: self.body,
name: self.name,
parameters: self.parameters,
private: self.private,
quiet: self.quiet,
shebang: self.shebang,
}
}
}
impl<'src, D> Keyed<'src> for Recipe<'src, D> {
fn key(&self) -> &'src str {
self.name.lexeme()
@ -342,7 +313,7 @@ impl<'src> Display for Recipe<'src> {
}
write!(f, ":")?;
for dependency in &self.dependencies {
write!(f, " {}", dependency.0.name())?;
write!(f, " {}", dependency)?;
}
for (i, line) in self.body.iter().enumerate() {

View File

@ -3,15 +3,15 @@ use crate::common::*;
use CompilationErrorKind::*;
pub(crate) struct RecipeResolver<'src: 'run, 'run> {
unresolved_recipes: Table<'src, Recipe<'src, Name<'src>>>,
unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
resolved_recipes: Table<'src, Rc<Recipe<'src>>>,
assignments: &'run Table<'src, Assignment<'src>>,
}
impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
pub(crate) fn resolve_recipes(
unresolved_recipes: Table<'src, Recipe<'src, Name<'src>>>,
assignments: &'run Table<'src, Assignment<'src>>,
unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
assignments: &Table<'src, Assignment<'src>>,
) -> CompilationResult<'src, Table<'src, Rc<Recipe<'src>>>> {
let mut resolver = RecipeResolver {
resolved_recipes: empty(),
@ -32,6 +32,14 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
}
}
for dependency in &recipe.dependencies {
for argument in &dependency.arguments {
for variable in argument.variables() {
resolver.resolve_variable(&variable, &recipe.parameters)?;
}
}
}
for line in &recipe.body {
for fragment in &line.fragments {
if let Fragment::Interpolation { expression, .. } = fragment {
@ -65,7 +73,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
fn resolve_recipe(
&mut self,
stack: &mut Vec<&'src str>,
recipe: Recipe<'src, Name<'src>>,
recipe: UnresolvedRecipe<'src>,
) -> CompilationResult<'src, Rc<Recipe<'src>>> {
if let Some(resolved) = self.resolved_recipes.get(recipe.name()) {
return Ok(resolved.clone());
@ -73,53 +81,39 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
stack.push(recipe.name());
let mut dependencies: Vec<Dependency> = Vec::new();
let mut dependencies: Vec<Rc<Recipe>> = Vec::new();
for dependency in &recipe.dependencies {
let name = dependency.lexeme();
let name = dependency.recipe.lexeme();
if let Some(resolved) = self.resolved_recipes.get(name) {
// dependency already resolved
if !resolved.parameters.is_empty() {
return Err(dependency.error(DependencyHasParameters {
recipe: recipe.name(),
dependency: name,
}));
}
dependencies.push(Dependency(resolved.clone()));
dependencies.push(resolved.clone());
} else if stack.contains(&name) {
let first = stack[0];
stack.push(first);
return Err(
dependency.error(CircularRecipeDependency {
dependency.recipe.error(CircularRecipeDependency {
recipe: recipe.name(),
circle: stack
.iter()
.skip_while(|name| **name != dependency.lexeme())
.skip_while(|name| **name != dependency.recipe.lexeme())
.cloned()
.collect(),
}),
);
} else if let Some(unresolved) = self.unresolved_recipes.remove(name) {
// resolve unresolved dependency
if !unresolved.parameters.is_empty() {
return Err(dependency.error(DependencyHasParameters {
recipe: recipe.name(),
dependency: name,
}));
}
dependencies.push(Dependency(self.resolve_recipe(stack, unresolved)?));
dependencies.push(self.resolve_recipe(stack, unresolved)?);
} else {
// dependency is unknown
return Err(dependency.error(UnknownDependency {
return Err(dependency.recipe.error(UnknownDependency {
recipe: recipe.name(),
unknown: name,
}));
}
}
let resolved = Rc::new(recipe.resolve(dependencies));
let resolved = Rc::new(recipe.resolve(dependencies)?);
self.resolved_recipes.insert(resolved.clone());
stack.pop();
Ok(resolved)
@ -189,4 +183,14 @@ mod tests {
width: 3,
kind: UndefinedVariable{variable: "foo"},
}
analysis_error! {
name: unknown_variable_in_dependency_argument,
input: "bar x:\nfoo: (bar baz)",
offset: 17,
line: 1,
column: 10,
width: 3,
kind: UndefinedVariable{variable: "baz"},
}
}

View File

@ -1,5 +1,7 @@
use crate::common::*;
use pretty_assertions::assert_eq;
pub(crate) fn compile(text: &str) -> Justfile {
match Compiler::compile(text) {
Ok(justfile) => justfile,

View File

@ -0,0 +1,7 @@
use crate::common::*;
#[derive(PartialEq, Debug)]
pub(crate) struct UnresolvedDependency<'src> {
pub(crate) recipe: Name<'src>,
pub(crate) arguments: Vec<Expression<'src>>,
}

49
src/unresolved_recipe.rs Normal file
View File

@ -0,0 +1,49 @@
use crate::common::*;
pub(crate) type UnresolvedRecipe<'src> = Recipe<'src, UnresolvedDependency<'src>>;
impl<'src> UnresolvedRecipe<'src> {
pub(crate) fn resolve(
self,
resolved: Vec<Rc<Recipe<'src>>>,
) -> CompilationResult<'src, Recipe<'src>> {
assert_eq!(self.dependencies.len(), resolved.len());
for (unresolved, resolved) in self.dependencies.iter().zip(&resolved) {
assert_eq!(unresolved.recipe.lexeme(), resolved.name.lexeme());
if !resolved
.argument_range()
.contains(&unresolved.arguments.len())
{
return Err(unresolved.recipe.error(
CompilationErrorKind::DependencyArgumentCountMismatch {
dependency: unresolved.recipe.lexeme(),
found: unresolved.arguments.len(),
min: resolved.min_arguments(),
max: resolved.max_arguments(),
},
));
}
}
let dependencies = self
.dependencies
.into_iter()
.zip(resolved)
.map(|(unresolved, resolved)| Dependency {
recipe: resolved,
arguments: unresolved.arguments,
})
.collect();
Ok(Recipe {
doc: self.doc,
body: self.body,
name: self.name,
parameters: self.parameters,
private: self.private,
quiet: self.quiet,
shebang: self.shebang,
dependencies,
})
}
}

View File

@ -1441,19 +1441,53 @@ bar:"#,
}
test! {
name: dependency_takes_arguments,
justfile: "b: a\na FOO:",
name: dependency_takes_arguments_exact,
justfile: "
a FOO:
b: a
",
args: ("b"),
stdout: "",
stderr: "error: Recipe `b` depends on `a` which requires arguments. \
Dependencies may not require arguments
stderr: "error: Dependency `a` got 0 arguments but takes 1 argument
|
1 | b: a
2 | b: a
| ^
",
status: EXIT_FAILURE,
}
test! {
name: dependency_takes_arguments_at_least,
justfile: "
a FOO LUZ='hello':
b: a
",
args: ("b"),
stdout: "",
stderr: "error: Dependency `a` got 0 arguments but takes at least 1 argument
|
2 | b: a
| ^
",
status: EXIT_FAILURE,
}
test! {
name: dependency_takes_arguments_at_most,
justfile: "
a FOO LUZ='hello':
b: (a '0' '1' '2')
",
args: ("b"),
stdout: "",
stderr: "error: Dependency `a` got 3 arguments but takes at most 2 arguments
|
2 | b: (a '0' '1' '2')
| ^
",
status: EXIT_FAILURE,
}
test! {
name: duplicate_parameter,
justfile: "a foo foo:",
@ -1467,19 +1501,6 @@ test! {
status: EXIT_FAILURE,
}
test! {
name: duplicate_dependency,
justfile: "b:\na: b b",
args: ("a"),
stdout: "",
stderr: "error: Recipe `a` has duplicate dependency `b`
|
2 | a: b b
| ^
",
status: EXIT_FAILURE,
}
test! {
name: duplicate_recipe,
justfile: "b:\nb:",
@ -2261,3 +2282,141 @@ test! {
stderr: "echo bar\necho foo\n",
shell: false,
}
test! {
name: dependency_argument_string,
justfile: "
release: (build 'foo') (build 'bar')
build target:
echo 'Building {{target}}...'
",
args: (),
stdout: "Building foo...\nBuilding bar...\n",
stderr: "echo 'Building foo...'\necho 'Building bar...'\n",
shell: false,
}
test! {
name: dependency_argument_parameter,
justfile: "
default: (release '1.0')
release version: (build 'foo' version) (build 'bar' version)
build target version:
echo 'Building {{target}}@{{version}}...'
",
args: (),
stdout: "Building foo@1.0...\nBuilding bar@1.0...\n",
stderr: "echo 'Building foo@1.0...'\necho 'Building bar@1.0...'\n",
shell: false,
}
test! {
name: dependency_argument_function,
justfile: "
foo: (bar env_var_or_default('x', 'y'))
bar arg:
echo {{arg}}
",
args: (),
stdout: "y\n",
stderr: "echo y\n",
shell: false,
}
test! {
name: dependency_argument_backtick,
justfile: "
export X := 'X'
foo: (bar `echo $X`)
bar arg:
echo {{arg}}
echo $X
",
args: (),
stdout: "X\nX\n",
stderr: "echo X\necho $X\n",
shell: false,
}
test! {
name: dependency_argument_assignment,
justfile: "
v := '1.0'
default: (release v)
release version:
echo Release {{version}}...
",
args: (),
stdout: "Release 1.0...\n",
stderr: "echo Release 1.0...\n",
shell: false,
}
test! {
name: dependency_argument_variadic,
justfile: "
foo: (bar 'A' 'B' 'C')
bar +args:
echo {{args}}
",
args: (),
stdout: "A B C\n",
stderr: "echo A B C\n",
shell: false,
}
test! {
name: duplicate_dependency_no_args,
justfile: "
foo: bar bar bar bar
bar:
echo BAR
",
args: (),
stdout: "BAR\n",
stderr: "echo BAR\n",
shell: false,
}
test! {
name: duplicate_dependency_argument,
justfile: "
foo: (bar 'BAR') (bar `echo BAR`)
bar bar:
echo {{bar}}
",
args: (),
stdout: "BAR\n",
stderr: "echo BAR\n",
shell: false,
}
test! {
name: parameter_cross_reference_error,
justfile: "
foo:
bar a b=a:
",
args: (),
stdout: "",
stderr: "
error: Variable `a` not defined
|
3 | bar a b=a:
| ^
",
status: EXIT_FAILURE,
shell: false,
}