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
- Align doc-comments in `--list` output (#273)
- 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
### 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_family()` - Operating system family; possible values are: `"unix"` and `"windows"`.
- `os_family()` Operating system family; possible values are: `"unix"` and `"windows"`.
For example:
@ -273,6 +273,12 @@ $ just system-info
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
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::Backtick{raw, ref token} => {
if self.dry_run {

View file

@ -75,7 +75,9 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
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} => {
self.resolve_expression(lhs)?;
self.resolve_expression(rhs)?;
@ -89,17 +91,6 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
#[cfg(test)]
mod test {
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! {
name: circular_variable_dependency,

View file

@ -1,6 +1,6 @@
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>>;
@ -24,6 +24,7 @@ pub enum CompilationErrorKind<'a> {
DuplicateRecipe{recipe: &'a str, first: usize},
DuplicateVariable{variable: &'a str},
ExtraLeadingWhitespace,
FunctionArgumentCountMismatch{function: &'a str, found: usize, expected: usize},
InconsistentLeadingWhitespace{expected: &'a str, found: &'a str},
Internal{message: String},
InvalidEscapeSequence{character: char},
@ -109,6 +110,13 @@ impl<'a> Display for CompilationError<'a> {
ExtraLeadingWhitespace => {
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} => {
writeln!(f,
"Recipe line has inconsistent leading whitespace. \

View file

@ -3,7 +3,7 @@ use common::*;
#[derive(PartialEq, Debug)]
pub enum Expression<'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>>},
String{cooked_string: CookedString<'a>},
Variable{name: &'a str, token: Token<'a>},
@ -26,11 +26,21 @@ impl<'a> Expression<'a> {
impl<'a> Display for Expression<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
Expression::Backtick {raw, .. } => write!(f, "`{}`", raw)?,
Expression::Call {name, .. } => write!(f, "{}()", name)?,
Expression::Concatination{ref lhs, ref rhs } => write!(f, "{} + {}", lhs, rhs)?,
Expression::String {ref cooked_string} => write!(f, "\"{}\"", cooked_string.raw)?,
Expression::Variable {name, .. } => write!(f, "{}", name)?,
Expression::Backtick {raw, .. } => write!(f, "`{}`", raw)?,
Expression::Concatination{ref lhs, ref rhs } => write!(f, "{} + {}", lhs, rhs)?,
Expression::String {ref cooked_string } => write!(f, "\"{}\"", cooked_string.raw)?,
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(())
}
@ -64,15 +74,15 @@ pub struct 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() {
None
| Some(&Expression::String{..})
| Some(&Expression::Backtick{..})
| 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}) => {
self.stack.push(lhs);
self.stack.push(rhs);

View file

@ -1,33 +1,104 @@
use common::*;
use target;
pub fn resolve_function<'a>(token: &Token<'a>) -> CompilationResult<'a, ()> {
if !&["arch", "os", "os_family"].contains(&token.lexeme) {
Err(token.error(CompilationErrorKind::UnknownFunction{function: token.lexeme}))
} else {
Ok(())
lazy_static! {
static ref FUNCTIONS: Map<&'static str, Function> = vec![
("arch", Function::Nullary(arch )),
("os", Function::Nullary(os )),
("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> {
match name {
"arch" => Ok(arch().to_string()),
"os" => Ok(os().to_string()),
"os_family" => Ok(os_family().to_string()),
_ => Err(RuntimeError::Internal {
pub fn resolve_function<'a>(token: &Token<'a>, argc: usize) -> CompilationResult<'a, ()> {
let name = token.lexeme;
if let Some(function) = FUNCTIONS.get(&name) {
use self::Function::*;
match (function, argc) {
(&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)
})
}
}
pub fn arch() -> &'static str {
target::arch()
pub fn arch() -> Result<String, String> {
Ok(target::arch().to_string())
}
pub fn os() -> &'static str {
target::os()
pub fn os() -> Result<String, String> {
Ok(target::os().to_string())
}
pub fn os_family() -> &'static str {
target::os_family()
pub fn os_family() -> Result<String, String> {
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_R: 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 EOF: Regex = token(r"(?-m)$" );
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)
} else if let Some(captures) = AT.captures(self.rest) {
(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) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), ParenL)
} else if let Some(captures) = PAREN_R.captures(self.rest) {
@ -332,6 +335,7 @@ mod test {
At => "@",
Backtick => "`",
Colon => ":",
Comma => ",",
Comment{..} => "#",
Dedent => "<",
Eof => ".",
@ -420,8 +424,8 @@ mod test {
summary_test! {
tokenize_recipe_multiple_interpolations,
"foo:#ok\n {{a}}0{{b}}1{{c}}",
"N:#$>^{N}_{N}_{N}<.",
"foo:,#ok\n {{a}}0{{b}}1{{c}}",
"N:,#$>^{N}_{N}_{N}<.",
}
summary_test! {

View file

@ -220,10 +220,11 @@ impl<'a> Parser<'a> {
return Err(self.unexpected_token(&token, &[Text, InterpolationStart, Eol]));
} else {
fragments.push(Fragment::Expression{
expression: self.expression(true)?
expression: self.expression()?
});
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(())
}
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 lhs = match first.kind {
Name => {
@ -256,10 +257,11 @@ impl<'a> Parser<'a> {
if let Some(token) = self.expect(ParenL) {
return Err(self.unexpected_token(&token, &[ParenL]));
}
let arguments = self.arguments()?;
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 {
Expression::Variable {name: first.lexeme, token: first}
}
@ -275,21 +277,31 @@ impl<'a> Parser<'a> {
};
if self.accepted(Plus) {
let rhs = self.expression(interpolation)?;
let rhs = self.expression()?;
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 {
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, ()> {
if self.assignments.contains_key(name.lexeme) {
return Err(name.error(DuplicateVariable {variable: name.lexeme}));
@ -297,7 +309,12 @@ impl<'a> Parser<'a> {
if export {
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.assignment_tokens.insert(name.lexeme, name);
Ok(())
@ -606,6 +623,32 @@ c = a + b + a + b",
{{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! {
name: missing_colon,
input: "a b c\nd e f",
@ -773,7 +816,7 @@ c = a + b + a + b",
line: 1,
column: 12,
width: Some(0),
kind: UnexpectedToken{expected: vec![Plus, Eol, InterpolationEnd], found: Dedent},
kind: UnexpectedToken{expected: vec![Plus, InterpolationEnd], found: Dedent},
}
compilation_error_test! {
@ -783,7 +826,7 @@ c = a + b + a + b",
line: 0,
column: 8,
width: Some(0),
kind: UnexpectedToken{expected: vec![ParenR], found: Eof},
kind: UnexpectedToken{expected: vec![Name, StringToken, ParenR], found: Eof},
}
compilation_error_test! {
@ -793,7 +836,7 @@ c = a + b + a + b",
line: 1,
column: 12,
width: Some(2),
kind: UnexpectedToken{expected: vec![ParenR], found: InterpolationEnd},
kind: UnexpectedToken{expected: vec![Name, StringToken, ParenR], found: InterpolationEnd},
}
compilation_error_test! {

View file

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

View file

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

View file

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

View file

@ -1203,6 +1203,34 @@ foo:
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! {
name: quiet_recipe,