diff --git a/src/assignment_resolver.rs b/src/assignment_resolver.rs index b263db6..035e25b 100644 --- a/src/assignment_resolver.rs +++ b/src/assignment_resolver.rs @@ -73,6 +73,13 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> { Expression::Call { thunk } => match thunk { Thunk::Nullary { .. } => Ok(()), 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], .. } => { self.resolve_expression(a)?; self.resolve_expression(b) diff --git a/src/evaluator.rs b/src/evaluator.rs index c3ae8a0..40db30f 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -92,6 +92,23 @@ impl<'src, 'run> Evaluator<'src, 'run> { 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 { name, function, diff --git a/src/function.rs b/src/function.rs index 7138a47..8102c8a 100644 --- a/src/function.rs +++ b/src/function.rs @@ -10,6 +10,7 @@ use { pub(crate) enum Function { Nullary(fn(&FunctionContext) -> Result), Unary(fn(&FunctionContext, &str) -> Result), + UnaryOpt(fn(&FunctionContext, &str, Option<&str>) -> Result), Binary(fn(&FunctionContext, &str, &str) -> Result), BinaryPlus(fn(&FunctionContext, &str, &str, &[String]) -> Result), Ternary(fn(&FunctionContext, &str, &str, &str) -> Result), @@ -21,6 +22,7 @@ pub(crate) fn get(name: &str) -> Option { "arch" => Nullary(arch), "capitalize" => Unary(capitalize), "clean" => Unary(clean), + "env" => UnaryOpt(env), "env_var" => Unary(env_var), "env_var_or_default" => Binary(env_var_or_default), "error" => Unary(error), @@ -70,6 +72,7 @@ impl Function { match *self { Nullary(_) => 0..0, Unary(_) => 1..1, + UnaryOpt(_) => 1..2, Binary(_) => 2..2, BinaryPlus(_) => 2..usize::MAX, Ternary(_) => 3..3, @@ -144,6 +147,13 @@ fn env_var_or_default( } } +fn env(context: &FunctionContext, key: &str, default: Option<&str>) -> Result { + match default { + Some(val) => env_var_or_default(context, key, val), + None => env_var(context, key), + } +} + fn error(_context: &FunctionContext, message: &str) -> Result { Err(message.to_owned()) } diff --git a/src/node.rs b/src/node.rs index b128842..e231c24 100644 --- a/src/node.rs +++ b/src/node.rs @@ -79,6 +79,15 @@ impl<'src> Node<'src> for Expression<'src> { tree.push_mut(name.lexeme()); 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 { name, args: [a, b], .. } => { diff --git a/src/parser.rs b/src/parser.rs index 066bdca..08045ef 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -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! { name: function_argument_count_binary, input: "x := env_var_or_default('foo')", diff --git a/src/summary.rs b/src/summary.rs index 091e08b..becfccb 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -225,6 +225,23 @@ impl Expression { name: name.lexeme().to_owned(), 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 { name, args: [a, b], .. } => Expression::Call { diff --git a/src/thunk.rs b/src/thunk.rs index 74112db..353e3c9 100644 --- a/src/thunk.rs +++ b/src/thunk.rs @@ -14,6 +14,12 @@ pub(crate) enum Thunk<'src> { function: fn(&FunctionContext, &str) -> Result, arg: Box>, }, + UnaryOpt { + name: Name<'src>, + #[derivative(Debug = "ignore", PartialEq = "ignore")] + function: fn(&FunctionContext, &str, Option<&str>) -> Result, + args: (Box>, Box>>), + }, Binary { name: Name<'src>, #[derivative(Debug = "ignore", PartialEq = "ignore")] @@ -39,6 +45,7 @@ impl<'src> Thunk<'src> { match self { Self::Nullary { name, .. } | Self::Unary { name, .. } + | Self::UnaryOpt { name, .. } | Self::Binary { name, .. } | Self::BinaryPlus { name, .. } | Self::Ternary { name, .. } => name, @@ -60,6 +67,18 @@ impl<'src> Thunk<'src> { arg: Box::new(arguments.pop().unwrap()), 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) => { let b = Box::new(arguments.pop().unwrap()); let a = Box::new(arguments.pop().unwrap()); @@ -105,6 +124,15 @@ impl Display for Thunk<'_> { match self { Nullary { name, .. } => write!(f, "{}()", 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 { name, args: [a, b], .. } => write!(f, "{}({a}, {b})", name.lexeme()), @@ -139,6 +167,14 @@ impl<'src> Serialize for Thunk<'src> { match self { Self::Nullary { .. } => {} 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, .. } => { for arg in args { seq.serialize_element(arg)?; diff --git a/src/variables.rs b/src/variables.rs index 9d620a2..821f650 100644 --- a/src/variables.rs +++ b/src/variables.rs @@ -20,6 +20,14 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> { Expression::Call { thunk } => match thunk { Thunk::Nullary { .. } => {} 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, .. } => { for arg in args.iter().rev() { self.stack.push(arg); diff --git a/tests/misc.rs b/tests/misc.rs index 6c2fb93..df6f1ca 100644 --- a/tests/misc.rs +++ b/tests/misc.rs @@ -1914,6 +1914,66 @@ test! { 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! { name: dependency_argument_backtick, justfile: "