Add env_var(key) and env_var_or_default(key, default) functions (#280)

`env_var(key)` looks up the value of the environment variable with name `key`, aborting execution if it is not found.

`env_var_or_default(key, default)` looks up the value of the environment variable with name `key`, returning `default` if it is not found.
This commit is contained in:
Casey Rodarmor 2017-12-02 23:59:07 +01:00 committed by GitHub
parent 9a56e27e18
commit 79c0994387
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 241 additions and 67 deletions

View File

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Added ### Added
- Align doc-comments in `--list` output (#273) - Align doc-comments in `--list` output (#273)
- Add `arch()`, `os()`, and `os_family()` functions (#277) - Add `arch()`, `os()`, and `os_family()` functions (#277)
- Add `env_var(key)` and `env_var_or_default(key, default)` functions (#280)
## [0.3.4] - 2017-10-06 ## [0.3.4] - 2017-10-06
### Added ### Added

View File

@ -259,7 +259,7 @@ Just provides a few built-in functions that might be useful when writing recipes
- `os()` Operating system. Possible values are: `"android"`, `"bitrig"`, `"dragonfly"`, `"emscripten"`, `"freebsd"`, `"haiku"`, `"ios"`, `"linux"`, `"macos"`, `"netbsd"`, `"openbsd"`, `"solaris"`, and `"windows"`. - `os()` Operating system. Possible values are: `"android"`, `"bitrig"`, `"dragonfly"`, `"emscripten"`, `"freebsd"`, `"haiku"`, `"ios"`, `"linux"`, `"macos"`, `"netbsd"`, `"openbsd"`, `"solaris"`, and `"windows"`.
- `os_family()` - Operating system family; possible values are: `"unix"` and `"windows"`. - `os_family()` Operating system family; possible values are: `"unix"` and `"windows"`.
For example: For example:
@ -273,6 +273,12 @@ $ just system-info
This is an x86_64 machine This is an x86_64 machine
``` ```
==== Environment Variables
- `env_var(key)` Retrieves the environment variable with name `key`, aborting if it is not present.
- `env_var_or_default(key, default)` Retrieves the environment variable with name `key`, returning `default` if it is not present.
=== Command Evaluation Using Backticks === Command Evaluation Using Backticks
Backticks can be used to store the result of commands: Backticks can be used to store the result of commands:

View File

@ -99,7 +99,12 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
}) })
} }
} }
Expression::Call{name, ..} => ::functions::evaluate_function(name), Expression::Call{name, arguments: ref call_arguments, ref token} => {
let call_arguments = call_arguments.iter().map(|argument| {
self.evaluate_expression(argument, &arguments)
}).collect::<Result<Vec<String>, RuntimeError>>()?;
::functions::evaluate_function(&token, name, &call_arguments)
}
Expression::String{ref cooked_string} => Ok(cooked_string.cooked.clone()), Expression::String{ref cooked_string} => Ok(cooked_string.cooked.clone()),
Expression::Backtick{raw, ref token} => { Expression::Backtick{raw, ref token} => {
if self.dry_run { if self.dry_run {

View File

@ -75,7 +75,9 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
return Err(token.error(UndefinedVariable{variable: name})); return Err(token.error(UndefinedVariable{variable: name}));
} }
} }
Expression::Call{ref token, ..} => ::functions::resolve_function(token)?, Expression::Call{ref token, ref arguments, ..} => {
::functions::resolve_function(token, arguments.len())?
}
Expression::Concatination{ref lhs, ref rhs} => { Expression::Concatination{ref lhs, ref rhs} => {
self.resolve_expression(lhs)?; self.resolve_expression(lhs)?;
self.resolve_expression(rhs)?; self.resolve_expression(rhs)?;
@ -89,17 +91,6 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use TokenKind::*;
compilation_error_test! {
name: unclosed_interpolation_delimiter,
input: "a:\n echo {{ foo",
index: 15,
line: 1,
column: 12,
width: Some(0),
kind: UnexpectedToken{expected: vec![Plus, Eol, InterpolationEnd], found: Dedent},
}
compilation_error_test! { compilation_error_test! {
name: circular_variable_dependency, name: circular_variable_dependency,

View File

@ -1,6 +1,6 @@
use common::*; use common::*;
use misc::{Or, write_error_context, show_whitespace}; use misc::{Or, write_error_context, show_whitespace, maybe_s};
pub type CompilationResult<'a, T> = Result<T, CompilationError<'a>>; pub type CompilationResult<'a, T> = Result<T, CompilationError<'a>>;
@ -24,6 +24,7 @@ pub enum CompilationErrorKind<'a> {
DuplicateRecipe{recipe: &'a str, first: usize}, DuplicateRecipe{recipe: &'a str, first: usize},
DuplicateVariable{variable: &'a str}, DuplicateVariable{variable: &'a str},
ExtraLeadingWhitespace, ExtraLeadingWhitespace,
FunctionArgumentCountMismatch{function: &'a str, found: usize, expected: usize},
InconsistentLeadingWhitespace{expected: &'a str, found: &'a str}, InconsistentLeadingWhitespace{expected: &'a str, found: &'a str},
Internal{message: String}, Internal{message: String},
InvalidEscapeSequence{character: char}, InvalidEscapeSequence{character: char},
@ -109,6 +110,13 @@ impl<'a> Display for CompilationError<'a> {
ExtraLeadingWhitespace => { ExtraLeadingWhitespace => {
writeln!(f, "Recipe line has extra leading whitespace")?; writeln!(f, "Recipe line has extra leading whitespace")?;
} }
FunctionArgumentCountMismatch{function, found, expected} => {
writeln!(
f,
"Function `{}` called with {} argument{} but takes {}",
function, found, maybe_s(found), expected
)?;
}
InconsistentLeadingWhitespace{expected, found} => { InconsistentLeadingWhitespace{expected, found} => {
writeln!(f, writeln!(f,
"Recipe line has inconsistent leading whitespace. \ "Recipe line has inconsistent leading whitespace. \

View File

@ -3,7 +3,7 @@ use common::*;
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub enum Expression<'a> { pub enum Expression<'a> {
Backtick{raw: &'a str, token: Token<'a>}, Backtick{raw: &'a str, token: Token<'a>},
Call{name: &'a str, token: Token<'a>}, Call{name: &'a str, token: Token<'a>, arguments: Vec<Expression<'a>>},
Concatination{lhs: Box<Expression<'a>>, rhs: Box<Expression<'a>>}, Concatination{lhs: Box<Expression<'a>>, rhs: Box<Expression<'a>>},
String{cooked_string: CookedString<'a>}, String{cooked_string: CookedString<'a>},
Variable{name: &'a str, token: Token<'a>}, Variable{name: &'a str, token: Token<'a>},
@ -26,11 +26,21 @@ impl<'a> Expression<'a> {
impl<'a> Display for Expression<'a> { impl<'a> Display for Expression<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self { match *self {
Expression::Backtick {raw, .. } => write!(f, "`{}`", raw)?, Expression::Backtick {raw, .. } => write!(f, "`{}`", raw)?,
Expression::Call {name, .. } => write!(f, "{}()", name)?, Expression::Concatination{ref lhs, ref rhs } => write!(f, "{} + {}", lhs, rhs)?,
Expression::Concatination{ref lhs, ref rhs } => write!(f, "{} + {}", lhs, rhs)?, Expression::String {ref cooked_string } => write!(f, "\"{}\"", cooked_string.raw)?,
Expression::String {ref cooked_string} => write!(f, "\"{}\"", cooked_string.raw)?, Expression::Variable {name, .. } => write!(f, "{}", name)?,
Expression::Variable {name, .. } => write!(f, "{}", name)?, Expression::Call {name, ref arguments, ..} => {
write!(f, "{}(", name)?;
for (i, argument) in arguments.iter().enumerate() {
if i > 0 {
write!(f, ", {}", argument)?;
} else {
write!(f, "{}", argument)?;
}
}
write!(f, ")")?;
}
} }
Ok(()) Ok(())
} }
@ -64,15 +74,15 @@ pub struct Functions<'a> {
} }
impl<'a> Iterator for Functions<'a> { impl<'a> Iterator for Functions<'a> {
type Item = &'a Token<'a>; type Item = (&'a Token<'a>, usize);
fn next(&mut self) -> Option<&'a Token<'a>> { fn next(&mut self) -> Option<Self::Item> {
match self.stack.pop() { match self.stack.pop() {
None None
| Some(&Expression::String{..}) | Some(&Expression::String{..})
| Some(&Expression::Backtick{..}) | Some(&Expression::Backtick{..})
| Some(&Expression::Variable{..}) => None, | Some(&Expression::Variable{..}) => None,
Some(&Expression::Call{ref token, ..}) => Some(token), Some(&Expression::Call{ref token, ref arguments, ..}) => Some((token, arguments.len())),
Some(&Expression::Concatination{ref lhs, ref rhs}) => { Some(&Expression::Concatination{ref lhs, ref rhs}) => {
self.stack.push(lhs); self.stack.push(lhs);
self.stack.push(rhs); self.stack.push(rhs);

View File

@ -1,33 +1,104 @@
use common::*; use common::*;
use target; use target;
pub fn resolve_function<'a>(token: &Token<'a>) -> CompilationResult<'a, ()> { lazy_static! {
if !&["arch", "os", "os_family"].contains(&token.lexeme) { static ref FUNCTIONS: Map<&'static str, Function> = vec![
Err(token.error(CompilationErrorKind::UnknownFunction{function: token.lexeme})) ("arch", Function::Nullary(arch )),
} else { ("os", Function::Nullary(os )),
Ok(()) ("os_family", Function::Nullary(os_family )),
("env_var", Function::Unary (env_var )),
("env_var_or_default", Function::Binary (env_var_or_default)),
].into_iter().collect();
}
enum Function {
Nullary(fn( ) -> Result<String, String>),
Unary (fn(&str ) -> Result<String, String>),
Binary (fn(&str, &str) -> Result<String, String>),
}
impl Function {
fn argc(&self) -> usize {
use self::Function::*;
match *self {
Nullary(_) => 0,
Unary(_) => 1,
Binary(_) => 2,
}
} }
} }
pub fn evaluate_function<'a>(name: &'a str) -> RunResult<'a, String> { pub fn resolve_function<'a>(token: &Token<'a>, argc: usize) -> CompilationResult<'a, ()> {
match name { let name = token.lexeme;
"arch" => Ok(arch().to_string()), if let Some(function) = FUNCTIONS.get(&name) {
"os" => Ok(os().to_string()), use self::Function::*;
"os_family" => Ok(os_family().to_string()), match (function, argc) {
_ => Err(RuntimeError::Internal { (&Nullary(_), 0) => Ok(()),
(&Unary(_), 1) => Ok(()),
(&Binary(_), 2) => Ok(()),
_ => {
Err(token.error(CompilationErrorKind::FunctionArgumentCountMismatch{
function: name, found: argc, expected: function.argc(),
}))
}
}
} else {
Err(token.error(CompilationErrorKind::UnknownFunction{function: token.lexeme}))
}
}
pub fn evaluate_function<'a>(token: &Token<'a>, name: &'a str, arguments: &[String]) -> RunResult<'a, String> {
if let Some(function) = FUNCTIONS.get(name) {
use self::Function::*;
let argc = arguments.len();
match (function, argc) {
(&Nullary(f), 0) => f()
.map_err(|message| RuntimeError::FunctionCall{token: token.clone(), message}),
(&Unary(f), 1) => f(&arguments[0])
.map_err(|message| RuntimeError::FunctionCall{token: token.clone(), message}),
(&Binary(f), 2) => f(&arguments[0], &arguments[1])
.map_err(|message| RuntimeError::FunctionCall{token: token.clone(), message}),
_ => {
Err(RuntimeError::Internal {
message: format!("attempted to evaluate function `{}` with {} arguments", name, argc)
})
}
}
} else {
Err(RuntimeError::Internal {
message: format!("attempted to evaluate unknown function: `{}`", name) message: format!("attempted to evaluate unknown function: `{}`", name)
}) })
} }
} }
pub fn arch() -> &'static str { pub fn arch() -> Result<String, String> {
target::arch() Ok(target::arch().to_string())
} }
pub fn os() -> &'static str { pub fn os() -> Result<String, String> {
target::os() Ok(target::os().to_string())
} }
pub fn os_family() -> &'static str { pub fn os_family() -> Result<String, String> {
target::os_family() Ok(target::os_family().to_string())
}
pub fn env_var<'a>(key: &str) -> Result<String, String> {
use std::env::VarError::*;
match env::var(key) {
Err(NotPresent) => Err(format!("environment variable `{}` not present", key)),
Err(NotUnicode(os_string)) =>
Err(format!("environment variable `{}` not unicode: {:?}", key, os_string)),
Ok(value) => Ok(value),
}
}
pub fn env_var_or_default<'a>(key: &str, default: &str) -> Result<String, String> {
use std::env::VarError::*;
match env::var(key) {
Err(NotPresent) => Ok(default.to_string()),
Err(NotUnicode(os_string)) =>
Err(format!("environment variable `{}` not unicode: {:?}", key, os_string)),
Ok(value) => Ok(value),
}
} }

View File

@ -134,6 +134,7 @@ impl<'a> Lexer<'a> {
static ref PAREN_L: Regex = token(r"[(]" ); static ref PAREN_L: Regex = token(r"[(]" );
static ref PAREN_R: Regex = token(r"[)]" ); static ref PAREN_R: Regex = token(r"[)]" );
static ref AT: Regex = token(r"@" ); static ref AT: Regex = token(r"@" );
static ref COMMA: Regex = token(r"," );
static ref COMMENT: Regex = token(r"#([^!\n\r].*)?$" ); static ref COMMENT: Regex = token(r"#([^!\n\r].*)?$" );
static ref EOF: Regex = token(r"(?-m)$" ); static ref EOF: Regex = token(r"(?-m)$" );
static ref EOL: Regex = token(r"\n|\r\n" ); static ref EOL: Regex = token(r"\n|\r\n" );
@ -209,6 +210,8 @@ impl<'a> Lexer<'a> {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Colon) (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Colon)
} else if let Some(captures) = AT.captures(self.rest) { } else if let Some(captures) = AT.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), At) (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), At)
} else if let Some(captures) = COMMA.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Comma)
} else if let Some(captures) = PAREN_L.captures(self.rest) { } else if let Some(captures) = PAREN_L.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), ParenL) (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), ParenL)
} else if let Some(captures) = PAREN_R.captures(self.rest) { } else if let Some(captures) = PAREN_R.captures(self.rest) {
@ -332,6 +335,7 @@ mod test {
At => "@", At => "@",
Backtick => "`", Backtick => "`",
Colon => ":", Colon => ":",
Comma => ",",
Comment{..} => "#", Comment{..} => "#",
Dedent => "<", Dedent => "<",
Eof => ".", Eof => ".",
@ -420,8 +424,8 @@ mod test {
summary_test! { summary_test! {
tokenize_recipe_multiple_interpolations, tokenize_recipe_multiple_interpolations,
"foo:#ok\n {{a}}0{{b}}1{{c}}", "foo:,#ok\n {{a}}0{{b}}1{{c}}",
"N:#$>^{N}_{N}_{N}<.", "N:,#$>^{N}_{N}_{N}<.",
} }
summary_test! { summary_test! {

View File

@ -220,10 +220,11 @@ impl<'a> Parser<'a> {
return Err(self.unexpected_token(&token, &[Text, InterpolationStart, Eol])); return Err(self.unexpected_token(&token, &[Text, InterpolationStart, Eol]));
} else { } else {
fragments.push(Fragment::Expression{ fragments.push(Fragment::Expression{
expression: self.expression(true)? expression: self.expression()?
}); });
if let Some(token) = self.expect(InterpolationEnd) { if let Some(token) = self.expect(InterpolationEnd) {
return Err(self.unexpected_token(&token, &[InterpolationEnd])); return Err(self.unexpected_token(&token, &[Plus, InterpolationEnd]));
} }
} }
} }
@ -248,7 +249,7 @@ impl<'a> Parser<'a> {
Ok(()) Ok(())
} }
fn expression(&mut self, interpolation: bool) -> CompilationResult<'a, Expression<'a>> { fn expression(&mut self) -> CompilationResult<'a, Expression<'a>> {
let first = self.tokens.next().unwrap(); let first = self.tokens.next().unwrap();
let lhs = match first.kind { let lhs = match first.kind {
Name => { Name => {
@ -256,10 +257,11 @@ impl<'a> Parser<'a> {
if let Some(token) = self.expect(ParenL) { if let Some(token) = self.expect(ParenL) {
return Err(self.unexpected_token(&token, &[ParenL])); return Err(self.unexpected_token(&token, &[ParenL]));
} }
let arguments = self.arguments()?;
if let Some(token) = self.expect(ParenR) { if let Some(token) = self.expect(ParenR) {
return Err(self.unexpected_token(&token, &[ParenR])); return Err(self.unexpected_token(&token, &[Name, StringToken, ParenR]));
} }
Expression::Call {name: first.lexeme, token: first} Expression::Call {name: first.lexeme, token: first, arguments}
} else { } else {
Expression::Variable {name: first.lexeme, token: first} Expression::Variable {name: first.lexeme, token: first}
} }
@ -275,21 +277,31 @@ impl<'a> Parser<'a> {
}; };
if self.accepted(Plus) { if self.accepted(Plus) {
let rhs = self.expression(interpolation)?; let rhs = self.expression()?;
Ok(Expression::Concatination{lhs: Box::new(lhs), rhs: Box::new(rhs)}) Ok(Expression::Concatination{lhs: Box::new(lhs), rhs: Box::new(rhs)})
} else if interpolation && self.peek(InterpolationEnd) {
Ok(lhs)
} else if let Some(token) = self.expect_eol() {
if interpolation {
return Err(self.unexpected_token(&token, &[Plus, Eol, InterpolationEnd]))
} else {
Err(self.unexpected_token(&token, &[Plus, Eol]))
}
} else { } else {
Ok(lhs) Ok(lhs)
} }
} }
fn arguments(&mut self) -> CompilationResult<'a, Vec<Expression<'a>>> {
let mut arguments = Vec::new();
while !self.peek(ParenR) && !self.peek(Eof) && !self.peek(Eol) && !self.peek(InterpolationEnd) {
arguments.push(self.expression()?);
if !self.accepted(Comma) {
if self.peek(ParenR) {
break;
} else {
let next = self.tokens.next().unwrap();
return Err(self.unexpected_token(&next, &[Comma, ParenR]));
}
}
}
Ok(arguments)
}
fn assignment(&mut self, name: Token<'a>, export: bool) -> CompilationResult<'a, ()> { fn assignment(&mut self, name: Token<'a>, export: bool) -> CompilationResult<'a, ()> {
if self.assignments.contains_key(name.lexeme) { if self.assignments.contains_key(name.lexeme) {
return Err(name.error(DuplicateVariable {variable: name.lexeme})); return Err(name.error(DuplicateVariable {variable: name.lexeme}));
@ -297,7 +309,12 @@ impl<'a> Parser<'a> {
if export { if export {
self.exports.insert(name.lexeme); self.exports.insert(name.lexeme);
} }
let expression = self.expression(false)?;
let expression = self.expression()?;
if let Some(token) = self.expect_eol() {
return Err(self.unexpected_token(&token, &[Plus, Eol]));
}
self.assignments.insert(name.lexeme, expression); self.assignments.insert(name.lexeme, expression);
self.assignment_tokens.insert(name.lexeme, name); self.assignment_tokens.insert(name.lexeme, name);
Ok(()) Ok(())
@ -606,6 +623,32 @@ c = a + b + a + b",
{{b}} {{c}}", {{b}} {{c}}",
} }
summary_test! {
unary_functions,
"
x = arch()
a:
{{os()}} {{os_family()}}",
"x = arch()
a:
{{os()}} {{os_family()}}",
}
summary_test! {
env_functions,
r#"
x = env_var('foo',)
a:
{{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var("baz"))}}"#,
r#"x = env_var("foo")
a:
{{env_var_or_default("foo" + "bar", "baz")}} {{env_var(env_var("baz"))}}"#,
}
compilation_error_test! { compilation_error_test! {
name: missing_colon, name: missing_colon,
input: "a b c\nd e f", input: "a b c\nd e f",
@ -773,7 +816,7 @@ c = a + b + a + b",
line: 1, line: 1,
column: 12, column: 12,
width: Some(0), width: Some(0),
kind: UnexpectedToken{expected: vec![Plus, Eol, InterpolationEnd], found: Dedent}, kind: UnexpectedToken{expected: vec![Plus, InterpolationEnd], found: Dedent},
} }
compilation_error_test! { compilation_error_test! {
@ -783,7 +826,7 @@ c = a + b + a + b",
line: 0, line: 0,
column: 8, column: 8,
width: Some(0), width: Some(0),
kind: UnexpectedToken{expected: vec![ParenR], found: Eof}, kind: UnexpectedToken{expected: vec![Name, StringToken, ParenR], found: Eof},
} }
compilation_error_test! { compilation_error_test! {
@ -793,7 +836,7 @@ c = a + b + a + b",
line: 1, line: 1,
column: 12, column: 12,
width: Some(2), width: Some(2),
kind: UnexpectedToken{expected: vec![ParenR], found: InterpolationEnd}, kind: UnexpectedToken{expected: vec![Name, StringToken, ParenR], found: InterpolationEnd},
} }
compilation_error_test! { compilation_error_test! {

View File

@ -41,8 +41,8 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
for line in &recipe.lines { for line in &recipe.lines {
for fragment in line { for fragment in line {
if let Fragment::Expression{ref expression, ..} = *fragment { if let Fragment::Expression{ref expression, ..} = *fragment {
for function in expression.functions() { for (function, argc) in expression.functions() {
if let Err(error) = ::functions::resolve_function(function) { if let Err(error) = ::functions::resolve_function(function, argc) {
return Err(CompilationError { return Err(CompilationError {
text: text, text: text,
index: error.index, index: error.index,

View File

@ -25,14 +25,15 @@ pub enum RuntimeError<'a> {
Backtick{token: Token<'a>, output_error: OutputError}, Backtick{token: Token<'a>, output_error: OutputError},
Code{recipe: &'a str, line_number: Option<usize>, code: i32}, Code{recipe: &'a str, line_number: Option<usize>, code: i32},
Cygpath{recipe: &'a str, output_error: OutputError}, Cygpath{recipe: &'a str, output_error: OutputError},
FunctionCall{token: Token<'a>, message: String},
Internal{message: String}, Internal{message: String},
IoError{recipe: &'a str, io_error: io::Error}, IoError{recipe: &'a str, io_error: io::Error},
Shebang{recipe: &'a str, command: String, argument: Option<String>, io_error: io::Error}, Shebang{recipe: &'a str, command: String, argument: Option<String>, io_error: io::Error},
Signal{recipe: &'a str, line_number: Option<usize>, signal: i32}, Signal{recipe: &'a str, line_number: Option<usize>, signal: i32},
TmpdirIoError{recipe: &'a str, io_error: io::Error}, TmpdirIoError{recipe: &'a str, io_error: io::Error},
Unknown{recipe: &'a str, line_number: Option<usize>},
UnknownOverrides{overrides: Vec<&'a str>}, UnknownOverrides{overrides: Vec<&'a str>},
UnknownRecipes{recipes: Vec<&'a str>, suggestion: Option<&'a str>}, UnknownRecipes{recipes: Vec<&'a str>, suggestion: Option<&'a str>},
Unknown{recipe: &'a str, line_number: Option<usize>},
} }
impl<'a> RuntimeError<'a> { impl<'a> RuntimeError<'a> {
@ -117,6 +118,10 @@ impl<'a> Display for RuntimeError<'a> {
but output was not utf8: {}", recipe, utf8_error)?; but output was not utf8: {}", recipe, utf8_error)?;
} }
}, },
FunctionCall{ref token, ref message} => {
write!(f, "Call to function `{}` failed: {}\n", token.lexeme, message)?;
error_token = Some(token);
}
Shebang{recipe, ref command, ref argument, ref io_error} => { Shebang{recipe, ref command, ref argument, ref io_error} => {
if let Some(ref argument) = *argument { if let Some(ref argument) = *argument {
write!(f, "Recipe `{}` with shebang `#!{} {}` execution error: {}", write!(f, "Recipe `{}` with shebang `#!{} {}` execution error: {}",
@ -161,11 +166,11 @@ impl<'a> Display for RuntimeError<'a> {
error_token = Some(token); error_token = Some(token);
} }
OutputError::Signal(signal) => { OutputError::Signal(signal) => {
write!(f, "Backtick was terminated by signal {}", signal)?; write!(f, "Backtick was terminated by signal {}\n", signal)?;
error_token = Some(token); error_token = Some(token);
} }
OutputError::Unknown => { OutputError::Unknown => {
write!(f, "Backtick failed for an unknown reason")?; write!(f, "Backtick failed for an unknown reason\n")?;
error_token = Some(token); error_token = Some(token);
} }
OutputError::Io(ref io_error) => { OutputError::Io(ref io_error) => {
@ -181,7 +186,7 @@ impl<'a> Display for RuntimeError<'a> {
error_token = Some(token); error_token = Some(token);
} }
OutputError::Utf8(ref utf8_error) => { OutputError::Utf8(ref utf8_error) => {
write!(f, "Backtick succeeded but stdout was not utf8: {}", utf8_error)?; write!(f, "Backtick succeeded but stdout was not utf8: {}\n", utf8_error)?;
error_token = Some(token); error_token = Some(token);
} }
}, },

View File

@ -29,6 +29,7 @@ pub enum TokenKind {
At, At,
Backtick, Backtick,
Colon, Colon,
Comma,
Comment, Comment,
Dedent, Dedent,
Eof, Eof,
@ -53,6 +54,7 @@ impl Display for TokenKind {
write!(f, "{}", match *self { write!(f, "{}", match *self {
Backtick => "backtick", Backtick => "backtick",
Colon => "':'", Colon => "':'",
Comma => "','",
Comment => "comment", Comment => "comment",
Dedent => "dedent", Dedent => "dedent",
Eof => "end of file", Eof => "end of file",

View File

@ -1203,6 +1203,34 @@ foo:
status: EXIT_SUCCESS, status: EXIT_SUCCESS,
} }
integration_test! {
name: env_var_functions,
justfile: r#"
p = env_var('PATH')
b = env_var_or_default('ZADDY', 'HTAP')
x = env_var_or_default('XYZ', 'ABC')
foo:
/bin/echo '{{p}}' '{{b}}' '{{x}}'
"#,
args: (),
stdout: format!("{} HTAP ABC\n", env::var("PATH").unwrap()).as_str(),
stderr: format!("/bin/echo '{}' 'HTAP' 'ABC'\n", env::var("PATH").unwrap()).as_str(),
status: EXIT_SUCCESS,
}
integration_test! {
name: env_var_failure,
justfile: "a:\n echo {{env_var('ZADDY')}}",
args: ("a"),
stdout: "",
stderr: "error: Call to function `env_var` failed: environment variable `ZADDY` not present
|
2 | echo {{env_var('ZADDY')}}
| ^^^^^^^
",
status: EXIT_FAILURE,
}
integration_test! { integration_test! {
name: quiet_recipe, name: quiet_recipe,