Add functions (#277)

– Parse unary (no-argument) functions
– Add functions for detecting the current os, arch, and os family, according to rustc's cfg attributes
This commit is contained in:
Casey Rodarmor 2017-12-02 14:37:10 +01:00 committed by GitHub
parent 66391de3f8
commit afa4aebd4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 277 additions and 35 deletions

7
Cargo.lock generated
View File

@ -119,6 +119,7 @@ dependencies = [
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"target 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -199,6 +200,11 @@ name = "strsim"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "target"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "tempdir"
version = "0.3.5"
@ -299,6 +305,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum regex 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ac6ab4e9218ade5b423358bbd2567d1617418403c7a512603630181813316322"
"checksum regex-syntax 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ad890a5eef7953f55427c50575c680c42841653abd2b028b68cd223d157f62db"
"checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694"
"checksum target 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "10000465bb0cc031c87a44668991b284fd84c0e6bd945f62d4af04e9e52a222a"
"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6"
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
"checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693"

View File

@ -18,6 +18,7 @@ itertools = "0.7"
lazy_static = "1.0.0"
libc = "0.2.21"
regex = "0.2.2"
target = "1.0.0"
tempdir = "0.3.5"
unicode-width = "0.1.3"

View File

@ -55,10 +55,14 @@ export : 'export' assignment
expression : value '+' expression
| value
value : STRING
value : NAME '(' arguments? ')'
| STRING
| RAW_STRING
| NAME
| BACKTICK
| NAME
arguments : expression ',' arguments
| expression ','?
recipe : '@'? NAME parameter* ('+' parameter)? ':' dependencies? body?

View File

@ -249,7 +249,31 @@ string!
"
```
=== Command Evaluation using Backticks
=== Functions
Just provides a few built-in functions that might be useful when writing recipes.
==== System Information
- `arch()` Instruction set architecture. Possible values are: `"aarch64"`, `"arm"`, `"asmjs"`, `"hexagon"`, `"mips"`, `"msp430"`, `"powerpc"`, `"powerpc64"`, `"s390x"`, `"sparc"`, `"wasm32"`, `"x86"`, `"x86_64"`, and `"xcore"`.
- `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"`.
For example:
```make
system-info:
@echo "This is an {{arch()}} machine".
```
```
$ just system-info
This is an x86_64 machine
```
=== Command Evaluation Using Backticks
Backticks can be used to store the result of commands:
@ -390,7 +414,7 @@ search QUERY:
lynx 'https://www.google.com/?q={{QUERY}}'
```
=== Write Recipes in other Languages
=== Writing Recipes in Other Languages
Recipes that start with a `#!` are executed as scripts, so you can write recipes in other languages:

View File

@ -82,37 +82,40 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
expression: &Expression<'a>,
arguments: &Map<&str, Cow<str>>
) -> RunResult<'a, String> {
Ok(match *expression {
match *expression {
Expression::Variable{name, ..} => {
if self.evaluated.contains_key(name) {
self.evaluated[name].clone()
Ok(self.evaluated[name].clone())
} else if self.scope.contains_key(name) {
self.scope[name].clone()
Ok(self.scope[name].clone())
} else if self.assignments.contains_key(name) {
self.evaluate_assignment(name)?;
self.evaluated[name].clone()
Ok(self.evaluated[name].clone())
} else if arguments.contains_key(name) {
arguments[name].to_string()
Ok(arguments[name].to_string())
} else {
return Err(RuntimeError::Internal {
Err(RuntimeError::Internal {
message: format!("attempted to evaluate undefined variable `{}`", name)
});
})
}
}
Expression::String{ref cooked_string} => cooked_string.cooked.clone(),
Expression::Call{name, ..} => ::functions::evaluate_function(name),
Expression::String{ref cooked_string} => Ok(cooked_string.cooked.clone()),
Expression::Backtick{raw, ref token} => {
if self.dry_run {
format!("`{}`", raw)
Ok(format!("`{}`", raw))
} else {
self.run_backtick(raw, token)?
Ok(self.run_backtick(raw, token)?)
}
}
Expression::Concatination{ref lhs, ref rhs} => {
Ok(
self.evaluate_expression(lhs, arguments)?
+
&self.evaluate_expression(rhs, arguments)?
)
}
}
})
}
fn run_backtick(

View File

@ -75,6 +75,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
return Err(token.error(UndefinedVariable{variable: name}));
}
}
Expression::Call{ref token, ..} => ::functions::resolve_function(token)?,
Expression::Concatination{ref lhs, ref rhs} => {
self.resolve_expression(lhs)?;
self.resolve_expression(rhs)?;
@ -129,4 +130,15 @@ mod test {
width: Some(2),
kind: UndefinedVariable{variable: "yy"},
}
compilation_error_test! {
name: unknown_function,
input: "a = foo()",
index: 4,
line: 0,
column: 4,
width: Some(3),
kind: UnknownFunction{function: "foo"},
}
}

View File

@ -29,12 +29,13 @@ pub enum CompilationErrorKind<'a> {
InvalidEscapeSequence{character: char},
MixedLeadingWhitespace{whitespace: &'a str},
OuterShebang,
ParameterFollowsVariadicParameter{parameter: &'a str},
ParameterShadowsVariable{parameter: &'a str},
RequiredParameterFollowsDefaultParameter{parameter: &'a str},
ParameterFollowsVariadicParameter{parameter: &'a str},
UndefinedVariable{variable: &'a str},
UnexpectedToken{expected: Vec<TokenKind>, found: TokenKind},
UnknownDependency{recipe: &'a str, unknown: &'a str},
UnknownFunction{function: &'a str},
UnknownStartOfToken,
UnterminatedString,
}
@ -123,6 +124,9 @@ impl<'a> Display for CompilationError<'a> {
UndefinedVariable{variable} => {
writeln!(f, "Variable `{}` not defined", variable)?;
}
UnknownFunction{function} => {
writeln!(f, "Call to unknown function `{}`", function)?;
}
UnknownStartOfToken => {
writeln!(f, "Unknown start of token:")?;
}

View File

@ -2,10 +2,11 @@ use common::*;
#[derive(PartialEq, Debug)]
pub enum Expression<'a> {
Variable{name: &'a str, token: Token<'a>},
String{cooked_string: CookedString<'a>},
Backtick{raw: &'a str, token: Token<'a>},
Call{name: &'a str, token: Token<'a>},
Concatination{lhs: Box<Expression<'a>>, rhs: Box<Expression<'a>>},
String{cooked_string: CookedString<'a>},
Variable{name: &'a str, token: Token<'a>},
}
impl<'a> Expression<'a> {
@ -14,12 +15,19 @@ impl<'a> Expression<'a> {
stack: vec![self],
}
}
pub fn functions(&'a self) -> Functions<'a> {
Functions {
stack: vec![self],
}
}
}
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)?,
@ -37,7 +45,10 @@ impl<'a> Iterator for Variables<'a> {
fn next(&mut self) -> Option<&'a Token<'a>> {
match self.stack.pop() {
None | Some(&Expression::String{..}) | Some(&Expression::Backtick{..}) => None,
None
| Some(&Expression::String{..})
| Some(&Expression::Backtick{..})
| Some(&Expression::Call{..}) => None,
Some(&Expression::Variable{ref token,..}) => Some(token),
Some(&Expression::Concatination{ref lhs, ref rhs}) => {
self.stack.push(lhs);
@ -47,3 +58,26 @@ impl<'a> Iterator for Variables<'a> {
}
}
}
pub struct Functions<'a> {
stack: Vec<&'a Expression<'a>>,
}
impl<'a> Iterator for Functions<'a> {
type Item = &'a Token<'a>;
fn next(&mut self) -> Option<&'a Token<'a>> {
match self.stack.pop() {
None
| Some(&Expression::String{..})
| Some(&Expression::Backtick{..})
| Some(&Expression::Variable{..}) => None,
Some(&Expression::Call{ref token, ..}) => Some(token),
Some(&Expression::Concatination{ref lhs, ref rhs}) => {
self.stack.push(lhs);
self.stack.push(rhs);
self.next()
}
}
}
}

33
src/functions.rs Normal file
View File

@ -0,0 +1,33 @@
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(())
}
}
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 {
message: format!("attempted to evaluate unknown function: `{}`", name)
})
}
}
pub fn arch() -> &'static str {
target::arch()
}
pub fn os() -> &'static str {
target::os()
}
pub fn os_family() -> &'static str {
target::os_family()
}

View File

@ -131,6 +131,8 @@ impl<'a> Lexer<'a> {
lazy_static! {
static ref BACKTICK: Regex = token(r"`[^`\n\r]*`" );
static ref COLON: Regex = token(r":" );
static ref PAREN_L: Regex = token(r"[(]" );
static ref PAREN_R: Regex = token(r"[)]" );
static ref AT: Regex = token(r"@" );
static ref COMMENT: Regex = token(r"#([^!\n\r].*)?$" );
static ref EOF: Regex = token(r"(?-m)$" );
@ -140,7 +142,7 @@ impl<'a> Lexer<'a> {
static ref INTERPOLATION_START_TOKEN: Regex = token(r"[{][{]" );
static ref NAME: Regex = token(r"([a-zA-Z_][a-zA-Z0-9_-]*)" );
static ref PLUS: Regex = token(r"[+]" );
static ref STRING: Regex = token("\"" );
static ref STRING: Regex = token(r#"["]"# );
static ref RAW_STRING: Regex = token(r#"'[^']*'"# );
static ref UNTERMINATED_RAW_STRING: Regex = token(r#"'[^']*"# );
static ref INTERPOLATION_START: Regex = re(r"^[{][{]" );
@ -209,6 +211,10 @@ 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) = 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) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), ParenR)
} else if let Some(captures) = PLUS.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Plus)
} else if let Some(captures) = EQUALS.captures(self.rest) {
@ -338,6 +344,8 @@ mod test {
InterpolationStart => "{",
Line{..} => "^",
Name => "N",
ParenL => "(",
ParenR => ")",
Plus => "+",
RawString => "'",
StringToken => "\"",
@ -510,6 +518,12 @@ c: b
"$N:N$>^_$$<N:$>^_$^_$$<N:N$>^_$$<N:N$>^_<.",
}
summary_test! {
tokenize_parens,
r"((())) )abc(+",
"((())))N(+.",
}
error_test! {
name: tokenize_space_then_tab,
input: "a:

View File

@ -7,6 +7,7 @@ extern crate edit_distance;
extern crate itertools;
extern crate libc;
extern crate regex;
extern crate target;
extern crate tempdir;
extern crate unicode_width;
@ -23,6 +24,7 @@ mod configuration;
mod cooked_string;
mod expression;
mod fragment;
mod functions;
mod justfile;
mod lexer;
mod misc;

View File

@ -251,7 +251,19 @@ impl<'a> Parser<'a> {
fn expression(&mut self, interpolation: bool) -> CompilationResult<'a, Expression<'a>> {
let first = self.tokens.next().unwrap();
let lhs = match first.kind {
Name => Expression::Variable {name: first.lexeme, token: first},
Name => {
if self.peek(ParenL) {
if let Some(token) = self.expect(ParenL) {
return Err(self.unexpected_token(&token, &[ParenL]));
}
if let Some(token) = self.expect(ParenR) {
return Err(self.unexpected_token(&token, &[ParenR]));
}
Expression::Call {name: first.lexeme, token: first}
} else {
Expression::Variable {name: first.lexeme, token: first}
}
}
Backtick => Expression::Backtick {
raw: &first.lexeme[1..first.lexeme.len()-1],
token: first
@ -764,6 +776,26 @@ c = a + b + a + b",
kind: UnexpectedToken{expected: vec![Plus, Eol, InterpolationEnd], found: Dedent},
}
compilation_error_test! {
name: unclosed_parenthesis_in_expression,
input: "x = foo(",
index: 8,
line: 0,
column: 8,
width: Some(0),
kind: UnexpectedToken{expected: vec![ParenR], found: Eof},
}
compilation_error_test! {
name: unclosed_parenthesis_in_interpolation,
input: "a:\n echo {{foo(}}",
index: 15,
line: 1,
column: 12,
width: Some(2),
kind: UnexpectedToken{expected: vec![ParenR], found: InterpolationEnd},
}
compilation_error_test! {
name: plus_following_parameter,
input: "a b c+:",

View File

@ -27,24 +27,39 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
resolver.seen = empty();
}
for recipe in recipes.values() {
for line in &recipe.lines {
for fragment in line {
if let Fragment::Expression{ref expression, ..} = *fragment {
for variable in expression.variables() {
let name = variable.lexeme;
let undefined = !assignments.contains_key(name)
&& !recipe.parameters.iter().any(|p| p.name == name);
if undefined {
// There's a borrow issue here that seems too difficult to solve.
// The error derived from the variable token has too short a lifetime,
// There are borrow issues here that seems too difficult to solve.
// The errors derived from the variable token has too short a lifetime,
// so we create a new error from its contents, which do live long
// enough.
//
// I suspect the solution here is to give recipes, pieces, and expressions
// two lifetime parameters instead of one, with one being the lifetime
// of the struct, and the second being the lifetime of the tokens
// that it contains
// that it contains.
for recipe in recipes.values() {
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) {
return Err(CompilationError {
text: text,
index: error.index,
line: error.line,
column: error.column,
width: error.width,
kind: UnknownFunction {
function: &text[error.index..error.index + error.width.unwrap()],
}
});
}
}
for variable in expression.variables() {
let name = variable.lexeme;
let undefined = !assignments.contains_key(name)
&& !recipe.parameters.iter().any(|p| p.name == name);
if undefined {
let error = variable.error(UndefinedVariable{variable: name});
return Err(CompilationError {
text: text,
@ -152,4 +167,14 @@ mod test {
width: Some(3),
kind: UndefinedVariable{variable: "lol"},
}
compilation_error_test! {
name: unknown_function_in_interpolation,
input: "a:\n echo {{bar()}}",
index: 11,
line: 1,
column: 8,
width: Some(3),
kind: UnknownFunction{function: "bar"},
}
}

View File

@ -39,6 +39,8 @@ pub enum TokenKind {
InterpolationStart,
Line,
Name,
ParenL,
ParenR,
Plus,
RawString,
StringToken,
@ -63,6 +65,8 @@ impl Display for TokenKind {
Name => "name",
Plus => "'+'",
At => "'@'",
ParenL => "'('",
ParenR => "')'",
StringToken => "string",
RawString => "raw string",
Text => "command text",

View File

@ -1,6 +1,7 @@
extern crate brev;
extern crate executable_path;
extern crate libc;
extern crate target;
extern crate tempdir;
use executable_path::executable_path;
@ -1174,6 +1175,35 @@ foo:
status: EXIT_SUCCESS,
}
integration_test! {
name: test_os_arch_functions_in_interpolation,
justfile: r#"
foo:
echo {{arch()}} {{os()}} {{os_family()}}
"#,
args: (),
stdout: format!("{} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
stderr: format!("echo {} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
status: EXIT_SUCCESS,
}
integration_test! {
name: test_os_arch_functions_in_expression,
justfile: r#"
a = arch()
o = os()
f = os_family()
foo:
echo {{a}} {{o}} {{f}}
"#,
args: (),
stdout: format!("{} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
stderr: format!("echo {} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
status: EXIT_SUCCESS,
}
integration_test! {
name: quiet_recipe,
justfile: r#"
@ -1260,6 +1290,19 @@ integration_test! {
status: EXIT_FAILURE,
}
integration_test! {
name: unknown_function_in_assignment,
justfile: r#"foo = foo() + "hello"
bar:"#,
args: ("bar"),
stdout: "",
stderr: r#"error: Call to unknown function `foo`
|
1 | foo = foo() + "hello"
| ^^^
"#,
status: EXIT_FAILURE,
}
integration_test! {
name: dependency_takes_arguments,