Add env() function (#1613)

This commit is contained in:
Baden Ashford 2023-06-13 13:49:46 +01:00 committed by GitHub
parent 19c887fded
commit bba673fd79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 192 additions and 0 deletions

View File

@ -73,6 +73,13 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
Expression::Call { thunk } => match thunk { Expression::Call { thunk } => match thunk {
Thunk::Nullary { .. } => Ok(()), Thunk::Nullary { .. } => Ok(()),
Thunk::Unary { arg, .. } => self.resolve_expression(arg), Thunk::Unary { arg, .. } => self.resolve_expression(arg),
Thunk::UnaryOpt { args: (a, b), .. } => {
self.resolve_expression(a)?;
if let Some(b) = b.as_ref() {
self.resolve_expression(b)?;
}
Ok(())
}
Thunk::Binary { args: [a, b], .. } => { Thunk::Binary { args: [a, b], .. } => {
self.resolve_expression(a)?; self.resolve_expression(a)?;
self.resolve_expression(b) self.resolve_expression(b)

View File

@ -92,6 +92,23 @@ impl<'src, 'run> Evaluator<'src, 'run> {
message, message,
} }
}), }),
UnaryOpt {
name,
function,
args: (a, b),
..
} => {
let a = self.evaluate_expression(a)?;
let b = match b.as_ref() {
Some(b) => Some(self.evaluate_expression(b)?),
None => None,
};
function(&context, &a, b.as_deref()).map_err(|message| Error::FunctionCall {
function: *name,
message,
})
}
Binary { Binary {
name, name,
function, function,

View File

@ -10,6 +10,7 @@ use {
pub(crate) enum Function { pub(crate) enum Function {
Nullary(fn(&FunctionContext) -> Result<String, String>), Nullary(fn(&FunctionContext) -> Result<String, String>),
Unary(fn(&FunctionContext, &str) -> Result<String, String>), Unary(fn(&FunctionContext, &str) -> Result<String, String>),
UnaryOpt(fn(&FunctionContext, &str, Option<&str>) -> Result<String, String>),
Binary(fn(&FunctionContext, &str, &str) -> Result<String, String>), Binary(fn(&FunctionContext, &str, &str) -> Result<String, String>),
BinaryPlus(fn(&FunctionContext, &str, &str, &[String]) -> Result<String, String>), BinaryPlus(fn(&FunctionContext, &str, &str, &[String]) -> Result<String, String>),
Ternary(fn(&FunctionContext, &str, &str, &str) -> Result<String, String>), Ternary(fn(&FunctionContext, &str, &str, &str) -> Result<String, String>),
@ -21,6 +22,7 @@ pub(crate) fn get(name: &str) -> Option<Function> {
"arch" => Nullary(arch), "arch" => Nullary(arch),
"capitalize" => Unary(capitalize), "capitalize" => Unary(capitalize),
"clean" => Unary(clean), "clean" => Unary(clean),
"env" => UnaryOpt(env),
"env_var" => Unary(env_var), "env_var" => Unary(env_var),
"env_var_or_default" => Binary(env_var_or_default), "env_var_or_default" => Binary(env_var_or_default),
"error" => Unary(error), "error" => Unary(error),
@ -70,6 +72,7 @@ impl Function {
match *self { match *self {
Nullary(_) => 0..0, Nullary(_) => 0..0,
Unary(_) => 1..1, Unary(_) => 1..1,
UnaryOpt(_) => 1..2,
Binary(_) => 2..2, Binary(_) => 2..2,
BinaryPlus(_) => 2..usize::MAX, BinaryPlus(_) => 2..usize::MAX,
Ternary(_) => 3..3, Ternary(_) => 3..3,
@ -144,6 +147,13 @@ fn env_var_or_default(
} }
} }
fn env(context: &FunctionContext, key: &str, default: Option<&str>) -> Result<String, String> {
match default {
Some(val) => env_var_or_default(context, key, val),
None => env_var(context, key),
}
}
fn error(_context: &FunctionContext, message: &str) -> Result<String, String> { fn error(_context: &FunctionContext, message: &str) -> Result<String, String> {
Err(message.to_owned()) Err(message.to_owned())
} }

View File

@ -79,6 +79,15 @@ impl<'src> Node<'src> for Expression<'src> {
tree.push_mut(name.lexeme()); tree.push_mut(name.lexeme());
tree.push_mut(arg.tree()); tree.push_mut(arg.tree());
} }
UnaryOpt {
name, args: (a, b), ..
} => {
tree.push_mut(name.lexeme());
tree.push_mut(a.tree());
if let Some(b) = b.as_ref() {
tree.push_mut(b.tree());
}
}
Binary { Binary {
name, args: [a, b], .. name, args: [a, b], ..
} => { } => {

View File

@ -2333,6 +2333,34 @@ mod tests {
}, },
} }
error! {
name: function_argument_count_too_high_unary_opt,
input: "x := env('foo', 'foo', 'foo')",
offset: 5,
line: 0,
column: 5,
width: 3,
kind: FunctionArgumentCountMismatch {
function: "env",
found: 3,
expected: 1..2,
},
}
error! {
name: function_argument_count_too_low_unary_opt,
input: "x := env()",
offset: 5,
line: 0,
column: 5,
width: 3,
kind: FunctionArgumentCountMismatch {
function: "env",
found: 0,
expected: 1..2,
},
}
error! { error! {
name: function_argument_count_binary, name: function_argument_count_binary,
input: "x := env_var_or_default('foo')", input: "x := env_var_or_default('foo')",

View File

@ -225,6 +225,23 @@ impl Expression {
name: name.lexeme().to_owned(), name: name.lexeme().to_owned(),
arguments: vec![Expression::new(arg)], arguments: vec![Expression::new(arg)],
}, },
full::Thunk::UnaryOpt {
name,
args: (a, opt_b),
..
} => {
let mut arguments = vec![];
if let Some(b) = opt_b.as_ref() {
arguments.push(Expression::new(b));
}
arguments.push(Expression::new(a));
Expression::Call {
name: name.lexeme().to_owned(),
arguments,
}
}
full::Thunk::Binary { full::Thunk::Binary {
name, args: [a, b], .. name, args: [a, b], ..
} => Expression::Call { } => Expression::Call {

View File

@ -14,6 +14,12 @@ pub(crate) enum Thunk<'src> {
function: fn(&FunctionContext, &str) -> Result<String, String>, function: fn(&FunctionContext, &str) -> Result<String, String>,
arg: Box<Expression<'src>>, arg: Box<Expression<'src>>,
}, },
UnaryOpt {
name: Name<'src>,
#[derivative(Debug = "ignore", PartialEq = "ignore")]
function: fn(&FunctionContext, &str, Option<&str>) -> Result<String, String>,
args: (Box<Expression<'src>>, Box<Option<Expression<'src>>>),
},
Binary { Binary {
name: Name<'src>, name: Name<'src>,
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]
@ -39,6 +45,7 @@ impl<'src> Thunk<'src> {
match self { match self {
Self::Nullary { name, .. } Self::Nullary { name, .. }
| Self::Unary { name, .. } | Self::Unary { name, .. }
| Self::UnaryOpt { name, .. }
| Self::Binary { name, .. } | Self::Binary { name, .. }
| Self::BinaryPlus { name, .. } | Self::BinaryPlus { name, .. }
| Self::Ternary { name, .. } => name, | Self::Ternary { name, .. } => name,
@ -60,6 +67,18 @@ impl<'src> Thunk<'src> {
arg: Box::new(arguments.pop().unwrap()), arg: Box::new(arguments.pop().unwrap()),
name, name,
}), }),
(Function::UnaryOpt(function), 1..=2) => {
let a = Box::new(arguments.remove(0));
let b = match arguments.pop() {
Some(value) => Box::new(Some(value)),
None => Box::new(None),
};
Ok(Thunk::UnaryOpt {
function,
args: (a, b),
name,
})
}
(Function::Binary(function), 2) => { (Function::Binary(function), 2) => {
let b = Box::new(arguments.pop().unwrap()); let b = Box::new(arguments.pop().unwrap());
let a = Box::new(arguments.pop().unwrap()); let a = Box::new(arguments.pop().unwrap());
@ -105,6 +124,15 @@ impl Display for Thunk<'_> {
match self { match self {
Nullary { name, .. } => write!(f, "{}()", name.lexeme()), Nullary { name, .. } => write!(f, "{}()", name.lexeme()),
Unary { name, arg, .. } => write!(f, "{}({arg})", name.lexeme()), Unary { name, arg, .. } => write!(f, "{}({arg})", name.lexeme()),
UnaryOpt {
name, args: (a, b), ..
} => {
if let Some(b) = b.as_ref() {
write!(f, "{}({a}, {b})", name.lexeme())
} else {
write!(f, "{}({a})", name.lexeme())
}
}
Binary { Binary {
name, args: [a, b], .. name, args: [a, b], ..
} => write!(f, "{}({a}, {b})", name.lexeme()), } => write!(f, "{}({a}, {b})", name.lexeme()),
@ -139,6 +167,14 @@ impl<'src> Serialize for Thunk<'src> {
match self { match self {
Self::Nullary { .. } => {} Self::Nullary { .. } => {}
Self::Unary { arg, .. } => seq.serialize_element(&arg)?, Self::Unary { arg, .. } => seq.serialize_element(&arg)?,
Self::UnaryOpt {
args: (a, opt_b), ..
} => {
seq.serialize_element(a)?;
if let Some(b) = opt_b.as_ref() {
seq.serialize_element(b)?;
}
}
Self::Binary { args, .. } => { Self::Binary { args, .. } => {
for arg in args { for arg in args {
seq.serialize_element(arg)?; seq.serialize_element(arg)?;

View File

@ -20,6 +20,14 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
Expression::Call { thunk } => match thunk { Expression::Call { thunk } => match thunk {
Thunk::Nullary { .. } => {} Thunk::Nullary { .. } => {}
Thunk::Unary { arg, .. } => self.stack.push(arg), Thunk::Unary { arg, .. } => self.stack.push(arg),
Thunk::UnaryOpt {
args: (a, opt_b), ..
} => {
self.stack.push(a);
if let Some(b) = opt_b.as_ref() {
self.stack.push(b);
}
}
Thunk::Binary { args, .. } => { Thunk::Binary { args, .. } => {
for arg in args.iter().rev() { for arg in args.iter().rev() {
self.stack.push(arg); self.stack.push(arg);

View File

@ -1914,6 +1914,66 @@ test! {
shell: false, shell: false,
} }
test! {
name: env_function_as_env_var,
justfile: "
foo: (bar env('x'))
bar arg:
echo {{arg}}
",
args: (),
env: { "x": "z", },
stdout: "z\n",
stderr: "echo z\n",
shell: false,
}
test! {
name: env_function_as_env_var_or_default,
justfile: "
foo: (bar env('x', 'y'))
bar arg:
echo {{arg}}
",
args: (),
env: { "x": "z", },
stdout: "z\n",
stderr: "echo z\n",
shell: false,
}
test! {
name: env_function_as_env_var_with_existing_env_var,
justfile: "
foo: (bar env('x'))
bar arg:
echo {{arg}}
",
args: (),
env: { "x": "z", },
stdout: "z\n",
stderr: "echo z\n",
shell: false,
}
test! {
name: env_function_as_env_var_or_default_with_existing_env_var,
justfile: "
foo: (bar env('x', 'y'))
bar arg:
echo {{arg}}
",
args: (),
env: { "x": "z", },
stdout: "z\n",
stderr: "echo z\n",
shell: false,
}
test! { test! {
name: dependency_argument_backtick, name: dependency_argument_backtick,
justfile: " justfile: "