Skip to content

Commit

Permalink
Add env_var(key) and env_var_or_default(key, default) functions (#…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
casey authored Dec 2, 2017
1 parent 9a56e27 commit 79c0994
Show file tree
Hide file tree
Showing 13 changed files with 240 additions and 66 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion README.asc
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
7 changes: 6 additions & 1 deletion src/assignment_evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 3 additions & 12 deletions src/assignment_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand All @@ -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,
Expand Down
10 changes: 9 additions & 1 deletion src/compilation_error.rs
Original file line number Diff line number Diff line change
@@ -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>>;

Expand All @@ -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},
Expand Down Expand Up @@ -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. \
Expand Down
28 changes: 19 additions & 9 deletions src/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>},
Expand All @@ -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(())
}
Expand Down Expand Up @@ -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);
Expand Down
103 changes: 87 additions & 16 deletions src/functions.rs
Original file line number Diff line number Diff line change
@@ -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}))
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 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 {
Ok(())
Err(token.error(CompilationErrorKind::UnknownFunction{function: token.lexeme}))
}
}

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 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() -> Result<String, String> {
Ok(target::os().to_string())
}

pub fn os() -> &'static str {
target::os()
pub fn os_family() -> Result<String, String> {
Ok(target::os_family().to_string())
}

pub fn os_family() -> &'static str {
target::os_family()
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),
}
}
8 changes: 6 additions & 2 deletions src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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" );
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -332,6 +335,7 @@ mod test {
At => "@",
Backtick => "`",
Colon => ":",
Comma => ",",
Comment{..} => "#",
Dedent => "<",
Eof => ".",
Expand Down Expand Up @@ -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! {
Expand Down
Loading

0 comments on commit 79c0994

Please sign in to comment.