just/src/justfile.rs

1077 lines
21 KiB
Rust
Raw Normal View History

2022-12-27 20:16:18 -08:00
use {super::*, serde::Serialize};
2021-11-17 00:07:48 -08:00
#[derive(Debug, PartialEq, Serialize)]
pub(crate) struct Justfile<'src> {
pub(crate) aliases: Table<'src, Alias<'src>>,
2021-11-17 00:07:48 -08:00
pub(crate) assignments: Table<'src, Assignment<'src>>,
#[serde(serialize_with = "keyed::serialize_option")]
pub(crate) first: Option<Rc<Recipe<'src>>>,
pub(crate) recipes: Table<'src, Rc<Recipe<'src>>>,
pub(crate) settings: Settings<'src>,
pub(crate) warnings: Vec<Warning>,
2017-11-16 23:30:08 -08:00
}
impl<'src> Justfile<'src> {
pub(crate) fn count(&self) -> usize {
2017-11-16 23:30:08 -08:00
self.recipes.len()
}
pub(crate) fn suggest_recipe(&self, input: &str) -> Option<Suggestion<'src>> {
let mut suggestions = self
.recipes
.keys()
.map(|name| {
(
edit_distance(name, input),
Suggestion { name, target: None },
)
})
.chain(self.aliases.iter().map(|(name, alias)| {
(
edit_distance(name, input),
Suggestion {
name,
target: Some(alias.target.name.lexeme()),
},
)
}))
.filter(|(distance, _suggestion)| distance < &3)
.collect::<Vec<(usize, Suggestion)>>();
suggestions.sort_by_key(|(distance, _suggestion)| *distance);
suggestions
.into_iter()
.map(|(_distance, suggestion)| suggestion)
.next()
2017-11-16 23:30:08 -08:00
}
pub(crate) fn suggest_variable(&self, input: &str) -> Option<Suggestion<'src>> {
let mut suggestions = self
.assignments
.keys()
.map(|name| {
(
edit_distance(name, input),
Suggestion { name, target: None },
)
})
.filter(|(distance, _suggestion)| distance < &3)
.collect::<Vec<(usize, Suggestion)>>();
suggestions.sort_by_key(|(distance, _suggestion)| *distance);
suggestions
.into_iter()
.map(|(_distance, suggestion)| suggestion)
.next()
}
pub(crate) fn run(
&self,
config: &Config,
search: &Search,
overrides: &BTreeMap<String, String>,
arguments: &[String],
) -> RunResult<'src, ()> {
let unknown_overrides = overrides
.keys()
.filter(|name| !self.assignments.contains_key(name.as_str()))
.cloned()
.collect::<Vec<String>>();
2017-11-16 23:30:08 -08:00
if !unknown_overrides.is_empty() {
return Err(Error::UnknownOverrides {
overrides: unknown_overrides,
});
2017-11-16 23:30:08 -08:00
}
let dotenv = if config.load_dotenv {
2022-01-30 12:16:10 -08:00
load_dotenv(config, &self.settings, &search.working_directory)?
} else {
BTreeMap::new()
};
2018-03-05 13:21:35 -08:00
let scope = {
let mut scope = Scope::new();
let mut unknown_overrides = Vec::new();
for (name, value) in overrides {
if let Some(assignment) = self.assignments.get(name) {
scope.bind(assignment.export, assignment.name, value.clone());
} else {
unknown_overrides.push(name.clone());
}
}
if !unknown_overrides.is_empty() {
return Err(Error::UnknownOverrides {
overrides: unknown_overrides,
});
}
Evaluator::evaluate_assignments(
&self.assignments,
config,
&dotenv,
scope,
&self.settings,
search,
)?
};
2017-11-16 23:30:08 -08:00
2021-05-09 20:35:35 -07:00
match &config.subcommand {
Subcommand::Command {
binary, arguments, ..
} => {
let mut command = if config.shell_command {
2022-01-30 12:16:10 -08:00
let mut command = self.settings.shell_command(config);
2021-05-09 20:35:35 -07:00
command.arg(binary);
command
} else {
2021-05-09 20:35:35 -07:00
Command::new(binary)
};
command.args(arguments);
command.current_dir(&search.working_directory);
let scope = scope.child();
command.export(&self.settings, &dotenv, &scope);
let status = InterruptHandler::guard(|| command.status()).map_err(|io_error| {
Error::CommandInvoke {
2021-05-09 20:35:35 -07:00
binary: binary.clone(),
arguments: arguments.clone(),
io_error,
}
})?;
if !status.success() {
return Err(Error::CommandStatus {
binary: binary.clone(),
arguments: arguments.clone(),
status,
});
2021-05-09 20:35:35 -07:00
};
return Ok(());
}
2021-05-09 20:35:35 -07:00
Subcommand::Evaluate { variable, .. } => {
if let Some(variable) = variable {
if let Some(value) = scope.value(variable) {
2022-12-15 16:53:21 -08:00
print!("{value}");
2021-05-09 20:35:35 -07:00
} else {
return Err(Error::EvalUnknownVariable {
2022-01-30 12:16:10 -08:00
suggestion: self.suggest_variable(variable),
variable: variable.clone(),
2021-05-09 20:35:35 -07:00
});
}
} else {
let mut width = 0;
for name in scope.names() {
width = cmp::max(name.len(), width);
}
for binding in scope.bindings() {
println!(
"{0:1$} := \"{2}\"",
binding.name.lexeme(),
width,
binding.value
);
}
}
2021-05-09 20:35:35 -07:00
return Ok(());
}
_ => {}
2017-11-16 23:30:08 -08:00
}
let argvec: Vec<&str> = if !arguments.is_empty() {
arguments.iter().map(String::as_str).collect()
2021-11-17 00:07:48 -08:00
} else if let Some(recipe) = &self.first {
let min_arguments = recipe.min_arguments();
if min_arguments > 0 {
return Err(Error::DefaultRecipeRequiresArguments {
recipe: recipe.name.lexeme(),
min_arguments,
});
}
vec![recipe.name()]
} else if self.recipes.is_empty() {
return Err(Error::NoRecipes);
} else {
return Err(Error::NoDefaultRecipe);
};
let arguments = argvec.as_slice();
2017-11-16 23:30:08 -08:00
let mut missing = vec![];
let mut grouped = vec![];
let mut rest = arguments;
2017-11-16 23:30:08 -08:00
while let Some((argument, mut tail)) = rest.split_first() {
if let Some(recipe) = self.get_recipe(argument) {
2017-11-16 23:30:08 -08:00
if recipe.parameters.is_empty() {
grouped.push((recipe, &[][..]));
2017-11-16 23:30:08 -08:00
} else {
let argument_range = recipe.argument_range();
let argument_count = cmp::min(tail.len(), recipe.max_arguments());
if !argument_range.range_contains(&argument_count) {
return Err(Error::ArgumentCountMismatch {
recipe: recipe.name(),
parameters: recipe.parameters.clone(),
found: tail.len(),
min: recipe.min_arguments(),
max: recipe.max_arguments(),
2017-11-16 23:30:08 -08:00
});
}
grouped.push((recipe, &tail[0..argument_count]));
tail = &tail[argument_count..];
}
} else {
missing.push((*argument).to_owned());
2017-11-16 23:30:08 -08:00
}
rest = tail;
}
if !missing.is_empty() {
let suggestion = if missing.len() == 1 {
self.suggest_recipe(missing.first().unwrap())
2017-11-16 23:30:08 -08:00
} else {
None
};
return Err(Error::UnknownRecipes {
recipes: missing,
suggestion,
});
2017-11-16 23:30:08 -08:00
}
let context = RecipeContext {
settings: &self.settings,
config,
scope,
search,
};
2018-08-27 18:36:40 -07:00
let mut ran = BTreeSet::new();
2017-11-16 23:30:08 -08:00
for (recipe, arguments) in grouped {
2022-11-22 16:36:23 -08:00
Self::run_recipe(&context, recipe, arguments, &dotenv, search, &mut ran)?;
2017-11-16 23:30:08 -08:00
}
Ok(())
}
pub(crate) fn get_alias(&self, name: &str) -> Option<&Alias<'src>> {
self.aliases.get(name)
}
pub(crate) fn get_recipe(&self, name: &str) -> Option<&Recipe<'src>> {
self
.recipes
.get(name)
.map(Rc::as_ref)
.or_else(|| self.aliases.get(name).map(|alias| alias.target.as_ref()))
}
fn run_recipe(
context: &RecipeContext<'src, '_>,
recipe: &Recipe<'src>,
arguments: &[&str],
dotenv: &BTreeMap<String, String>,
search: &Search,
ran: &mut BTreeSet<Vec<String>>,
) -> RunResult<'src, ()> {
let mut invocation = vec![recipe.name().to_owned()];
for argument in arguments {
invocation.push((*argument).to_string());
}
if ran.contains(&invocation) {
return Ok(());
}
2023-11-16 15:51:34 -08:00
if !context.config.yes && !recipe.confirm()? {
return Err(Error::NotConfirmed {
recipe: recipe.name(),
});
}
let (outer, positional) = Evaluator::evaluate_parameters(
context.config,
dotenv,
&recipe.parameters,
arguments,
&context.scope,
context.settings,
search,
)?;
let scope = outer.child();
let mut evaluator =
Evaluator::recipe_evaluator(context.config, dotenv, &scope, context.settings, search);
for Dependency { recipe, arguments } in recipe.dependencies.iter().take(recipe.priors) {
let arguments = arguments
.iter()
.map(|argument| evaluator.evaluate_expression(argument))
.collect::<RunResult<Vec<String>>>()?;
Self::run_recipe(
context,
recipe,
&arguments.iter().map(String::as_ref).collect::<Vec<&str>>(),
dotenv,
search,
ran,
)?;
2017-11-16 23:30:08 -08:00
}
recipe.run(context, dotenv, scope.child(), search, &positional)?;
{
let mut ran = BTreeSet::new();
for Dependency { recipe, arguments } in recipe.dependencies.iter().skip(recipe.priors) {
let mut evaluated = Vec::new();
for argument in arguments {
evaluated.push(evaluator.evaluate_expression(argument)?);
}
Self::run_recipe(
context,
recipe,
&evaluated.iter().map(String::as_ref).collect::<Vec<&str>>(),
dotenv,
search,
&mut ran,
)?;
}
}
ran.insert(invocation);
2017-11-16 23:30:08 -08:00
Ok(())
}
pub(crate) fn public_recipes(&self, source_order: bool) -> Vec<&Recipe<'src, Dependency>> {
let mut recipes = self
.recipes
.values()
.map(AsRef::as_ref)
.filter(|recipe| recipe.public())
.collect::<Vec<&Recipe<Dependency>>>();
if source_order {
recipes.sort_by_key(|recipe| recipe.name.offset);
}
recipes
}
2017-11-16 23:30:08 -08:00
}
impl<'src> ColorDisplay for Justfile<'src> {
fn fmt(&self, f: &mut Formatter, color: Color) -> Result<(), fmt::Error> {
let mut items = self.recipes.len() + self.assignments.len() + self.aliases.len();
for (name, assignment) in &self.assignments {
if assignment.export {
2017-11-16 23:30:08 -08:00
write!(f, "export ")?;
}
2022-12-15 16:53:21 -08:00
write!(f, "{name} := {}", assignment.value)?;
2017-11-16 23:30:08 -08:00
items -= 1;
if items != 0 {
write!(f, "\n\n")?;
}
}
for alias in self.aliases.values() {
2022-12-15 16:53:21 -08:00
write!(f, "{alias}")?;
items -= 1;
if items != 0 {
write!(f, "\n\n")?;
}
}
2017-11-16 23:30:08 -08:00
for recipe in self.recipes.values() {
write!(f, "{}", recipe.color_display(color))?;
2017-11-16 23:30:08 -08:00
items -= 1;
if items != 0 {
write!(f, "\n\n")?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
2017-11-16 23:30:08 -08:00
use super::*;
use testing::compile;
use Error::*;
run_error! {
name: unknown_recipes,
src: "a:\nb:\nc:",
args: ["a", "x", "y", "z"],
error: UnknownRecipes {
recipes,
suggestion,
},
check: {
assert_eq!(recipes, &["x", "y", "z"]);
assert_eq!(suggestion, None);
2017-11-16 23:30:08 -08:00
}
}
run_error! {
name: unknown_recipes_show_alias_suggestion,
src: "
foo:
echo foo
alias z := foo
",
args: ["zz"],
error: UnknownRecipes {
recipes,
suggestion,
},
check: {
assert_eq!(recipes, &["zz"]);
assert_eq!(suggestion, Some(Suggestion {
name: "z",
target: Some("foo"),
}
));
}
}
// This test exists to make sure that shebang recipes run correctly. Although
// this script is still executed by a shell its behavior depends on the value of
// a variable and continuing even though a command fails, whereas in plain
// recipes variables are not available in subsequent lines and execution stops
// when a line fails.
run_error! {
name: run_shebang,
src: "
a:
#!/usr/bin/env sh
code=200
x() { return $code; }
x
x
",
args: ["a"],
error: Code {
recipe,
line_number,
code,
print_message,
},
check: {
assert_eq!(recipe, "a");
assert_eq!(code, 200);
assert_eq!(line_number, None);
assert!(print_message);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
2017-11-17 17:28:06 -08:00
run_error! {
name: code_error,
src: "
fail:
@exit 100
",
args: ["fail"],
error: Code {
recipe,
line_number,
code,
print_message,
},
check: {
assert_eq!(recipe, "fail");
assert_eq!(code, 100);
assert_eq!(line_number, Some(2));
assert!(print_message);
}
2017-11-16 23:30:08 -08:00
}
run_error! {
name: run_args,
src: r#"
a return code:
@x() { {{return}} {{code + "0"}}; }; x
"#,
args: ["a", "return", "15"],
error: Code {
recipe,
line_number,
code,
print_message,
},
check: {
assert_eq!(recipe, "a");
assert_eq!(code, 150);
assert_eq!(line_number, Some(2));
assert!(print_message);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
run_error! {
name: missing_some_arguments,
src: "a b c d:",
args: ["a", "b", "c"],
error: ArgumentCountMismatch {
recipe,
parameters,
found,
min,
max,
},
check: {
let param_names = parameters
.iter()
.map(|p| p.name.lexeme())
.collect::<Vec<&str>>();
assert_eq!(recipe, "a");
assert_eq!(param_names, ["b", "c", "d"]);
assert_eq!(found, 2);
assert_eq!(min, 3);
assert_eq!(max, 3);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
run_error! {
name: missing_some_arguments_variadic,
src: "a b c +d:",
args: ["a", "B", "C"],
error: ArgumentCountMismatch {
recipe,
parameters,
found,
min,
max,
},
check: {
let param_names = parameters
.iter()
.map(|p| p.name.lexeme())
.collect::<Vec<&str>>();
assert_eq!(recipe, "a");
assert_eq!(param_names, ["b", "c", "d"]);
assert_eq!(found, 2);
assert_eq!(min, 3);
assert_eq!(max, usize::MAX - 1);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
run_error! {
name: missing_all_arguments,
src: "a b c d:\n echo {{b}}{{c}}{{d}}",
args: ["a"],
error: ArgumentCountMismatch {
recipe,
parameters,
found,
min,
max,
},
check: {
let param_names = parameters
.iter()
.map(|p| p.name.lexeme())
.collect::<Vec<&str>>();
assert_eq!(recipe, "a");
assert_eq!(param_names, ["b", "c", "d"]);
assert_eq!(found, 0);
assert_eq!(min, 3);
assert_eq!(max, 3);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
run_error! {
name: missing_some_defaults,
src: "a b c d='hello':",
args: ["a", "b"],
error: ArgumentCountMismatch {
recipe,
parameters,
found,
min,
max,
},
check: {
let param_names = parameters
.iter()
.map(|p| p.name.lexeme())
.collect::<Vec<&str>>();
assert_eq!(recipe, "a");
assert_eq!(param_names, ["b", "c", "d"]);
assert_eq!(found, 1);
assert_eq!(min, 2);
assert_eq!(max, 3);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
run_error! {
name: missing_all_defaults,
src: "a b c='r' d='h':",
args: ["a"],
error: ArgumentCountMismatch {
recipe,
parameters,
found,
min,
max,
},
check: {
let param_names = parameters
.iter()
.map(|p| p.name.lexeme())
.collect::<Vec<&str>>();
assert_eq!(recipe, "a");
assert_eq!(param_names, ["b", "c", "d"]);
assert_eq!(found, 0);
assert_eq!(min, 1);
assert_eq!(max, 3);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
run_error! {
name: unknown_overrides,
src: "
a:
echo {{`f() { return 100; }; f`}}
",
args: ["foo=bar", "baz=bob", "a"],
error: UnknownOverrides { overrides },
check: {
assert_eq!(overrides, &["baz", "foo"]);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
run_error! {
name: export_failure,
src: r#"
export foo := "a"
baz := "c"
export bar := "b"
export abc := foo + bar + baz
wut:
echo $foo $bar $baz
"#,
args: ["--quiet", "wut"],
error: Code {
recipe,
line_number,
print_message,
..
},
check: {
assert_eq!(recipe, "wut");
assert_eq!(line_number, Some(7));
assert!(print_message);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
2023-11-16 13:51:57 -08:00
fn case(input: &str, expected: &str) {
let justfile = compile(input);
let actual = format!("{}", justfile.color_display(Color::never()));
assert_eq!(actual, expected);
println!("Re-parsing...");
let reparsed = compile(&actual);
let redumped = format!("{}", reparsed.color_display(Color::never()));
assert_eq!(redumped, actual);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parse_empty() {
case(
"
# hello
",
2023-11-16 13:51:57 -08:00
"",
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parse_string_default() {
case(
r#"
foo a="b\t":
"#,
2023-11-16 13:51:57 -08:00
r#"foo a="b\t":"#,
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parse_multiple() {
case(
r"
a:
b:
2023-11-16 13:51:57 -08:00
", r"a:
2023-11-16 13:51:57 -08:00
b:",
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parse_variadic() {
case(
r"
foo +a:
2023-11-16 13:51:57 -08:00
",
r"foo +a:",
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parse_variadic_string_default() {
case(
r#"
foo +a="Hello":
"#,
2023-11-16 13:51:57 -08:00
r#"foo +a="Hello":"#,
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parse_raw_string_default() {
case(
r"
foo a='b\t':
2023-10-08 19:34:05 -07:00
",
2023-11-16 13:51:57 -08:00
r"foo a='b\t':",
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parse_export() {
case(
r#"
export a := "hello"
"#,
2023-11-16 13:51:57 -08:00
r#"export a := "hello""#,
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parse_alias_after_target() {
case(
r"
foo:
echo a
alias f := foo
2023-11-16 13:51:57 -08:00
",
r"alias f := foo
foo:
2023-11-16 13:51:57 -08:00
echo a",
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parse_alias_before_target() {
case(
r"
alias f := foo
foo:
echo a
2023-11-16 13:51:57 -08:00
",
r"alias f := foo
foo:
2023-11-16 13:51:57 -08:00
echo a",
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parse_alias_with_comment() {
case(
r"
alias f := foo #comment
foo:
echo a
2023-11-16 13:51:57 -08:00
",
r"alias f := foo
foo:
2023-11-16 13:51:57 -08:00
echo a",
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parse_complex() {
case(
"
x:
y:
z:
foo := \"xx\"
bar := foo
goodbye := \"y\"
hello a b c : x y z #hello
#! blah
#blarg
{{ foo + bar}}abc{{ goodbye\t + \"x\" }}xyz
1
2
3
",
2023-11-16 13:51:57 -08:00
"bar := foo
foo := \"xx\"
goodbye := \"y\"
hello a b c: x y z
#! blah
#blarg
2021-06-08 01:01:27 -07:00
{{ foo + bar }}abc{{ goodbye + \"x\" }}xyz
1
2
3
x:
y:
2023-11-16 13:51:57 -08:00
z:",
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parse_shebang() {
case(
"
practicum := 'hello'
install:
\t#!/bin/sh
\tif [[ -f {{practicum}} ]]; then
\t\treturn
\tfi
",
2023-11-16 13:51:57 -08:00
"practicum := 'hello'
install:
#!/bin/sh
2021-06-08 01:01:27 -07:00
if [[ -f {{ practicum }} ]]; then
\treturn
fi",
2023-11-16 13:51:57 -08:00
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parse_simple_shebang() {
case("a:\n #!\n print(1)", "a:\n #!\n print(1)");
}
2023-11-16 13:51:57 -08:00
#[test]
fn parse_assignments() {
case(
r#"a := "0"
c := a + b + a + b
b := "1"
"#,
2023-11-16 13:51:57 -08:00
r#"a := "0"
b := "1"
c := a + b + a + b"#,
2023-11-16 13:51:57 -08:00
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parse_assignment_backticks() {
case(
"a := `echo hello`
c := a + b + a + b
b := `echo goodbye`",
2023-11-16 13:51:57 -08:00
"a := `echo hello`
b := `echo goodbye`
c := a + b + a + b",
2023-11-16 13:51:57 -08:00
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parse_interpolation_backticks() {
case(
r#"a:
echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#,
2023-11-16 13:51:57 -08:00
r#"a:
2021-06-08 01:01:27 -07:00
echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#,
2023-11-16 13:51:57 -08:00
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn eof_test() {
case("x:\ny:\nz:\na b c: x y z", "a b c: x y z\n\nx:\n\ny:\n\nz:");
}
2023-11-16 13:51:57 -08:00
#[test]
fn string_quote_escape() {
case(r#"a := "hello\"""#, r#"a := "hello\"""#);
}
2023-11-16 13:51:57 -08:00
#[test]
fn string_escapes() {
case(r#"a := "\n\t\r\"\\""#, r#"a := "\n\t\r\"\\""#);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parameters() {
case(
"a b c:
{{b}} {{c}}",
2023-11-16 13:51:57 -08:00
"a b c:
2021-06-08 01:01:27 -07:00
{{ b }} {{ c }}",
2023-11-16 13:51:57 -08:00
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn unary_functions() {
case(
"
x := arch()
a:
2023-08-02 16:52:21 -07:00
{{os()}} {{os_family()}} {{num_cpus()}}",
2023-11-16 13:51:57 -08:00
"x := arch()
a:
2023-08-02 16:52:21 -07:00
{{ os() }} {{ os_family() }} {{ num_cpus() }}",
2023-11-16 13:51:57 -08:00
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn env_functions() {
case(
r#"
x := env_var('foo',)
a:
{{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var("baz"))}}"#,
2023-11-16 13:51:57 -08:00
r#"x := env_var('foo')
a:
2021-06-08 01:01:27 -07:00
{{ env_var_or_default('foo' + 'bar', 'baz') }} {{ env_var(env_var("baz")) }}"#,
2023-11-16 13:51:57 -08:00
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parameter_default_string() {
case(
r#"
f x="abc":
"#,
2023-11-16 13:51:57 -08:00
r#"f x="abc":"#,
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parameter_default_raw_string() {
case(
r"
f x='abc':
2023-11-16 13:51:57 -08:00
",
r"f x='abc':",
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parameter_default_backtick() {
case(
r"
f x=`echo hello`:
2023-11-16 13:51:57 -08:00
",
r"f x=`echo hello`:",
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parameter_default_concatenation_string() {
case(
r#"
f x=(`echo hello` + "foo"):
"#,
2023-11-16 13:51:57 -08:00
r#"f x=(`echo hello` + "foo"):"#,
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parameter_default_concatenation_variable() {
case(
r#"
x := "10"
f y=(`echo hello` + x) +z="foo":
"#,
2023-11-16 13:51:57 -08:00
r#"x := "10"
f y=(`echo hello` + x) +z="foo":"#,
2023-11-16 13:51:57 -08:00
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn parameter_default_multiple() {
case(
r#"
x := "10"
f y=(`echo hello` + x) +z=("foo" + "bar"):
"#,
2023-11-16 13:51:57 -08:00
r#"x := "10"
f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
2023-11-16 13:51:57 -08:00
);
}
2023-11-16 13:51:57 -08:00
#[test]
fn concatenation_in_group() {
case("x := ('0' + '1')", "x := ('0' + '1')");
}
2023-11-16 13:51:57 -08:00
#[test]
fn string_in_group() {
case("x := ('0' )", "x := ('0')");
}
#[rustfmt::skip]
2023-11-16 13:51:57 -08:00
#[test]
fn escaped_dos_newlines() {
case("@spam:\r
\t{ \\\r
\t\tfiglet test; \\\r
\t\tcargo build --color always 2>&1; \\\r
\t\tcargo test --color always -- --color always 2>&1; \\\r
\t} | less\r
",
"@spam:
{ \\
\tfiglet test; \\
\tcargo build --color always 2>&1; \\
\tcargo test --color always -- --color always 2>&1; \\
2023-11-16 13:51:57 -08:00
} | less");
}
2017-11-16 23:30:08 -08:00
}