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:
parent
66391de3f8
commit
afa4aebd4a
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -119,6 +119,7 @@ dependencies = [
|
|||||||
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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)",
|
"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)",
|
"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)",
|
"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)",
|
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
@ -199,6 +200,11 @@ name = "strsim"
|
|||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "tempdir"
|
name = "tempdir"
|
||||||
version = "0.3.5"
|
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 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 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 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 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 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"
|
"checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693"
|
||||||
|
@ -18,6 +18,7 @@ itertools = "0.7"
|
|||||||
lazy_static = "1.0.0"
|
lazy_static = "1.0.0"
|
||||||
libc = "0.2.21"
|
libc = "0.2.21"
|
||||||
regex = "0.2.2"
|
regex = "0.2.2"
|
||||||
|
target = "1.0.0"
|
||||||
tempdir = "0.3.5"
|
tempdir = "0.3.5"
|
||||||
unicode-width = "0.1.3"
|
unicode-width = "0.1.3"
|
||||||
|
|
||||||
|
@ -55,10 +55,14 @@ export : 'export' assignment
|
|||||||
expression : value '+' expression
|
expression : value '+' expression
|
||||||
| value
|
| value
|
||||||
|
|
||||||
value : STRING
|
value : NAME '(' arguments? ')'
|
||||||
|
| STRING
|
||||||
| RAW_STRING
|
| RAW_STRING
|
||||||
| NAME
|
|
||||||
| BACKTICK
|
| BACKTICK
|
||||||
|
| NAME
|
||||||
|
|
||||||
|
arguments : expression ',' arguments
|
||||||
|
| expression ','?
|
||||||
|
|
||||||
recipe : '@'? NAME parameter* ('+' parameter)? ':' dependencies? body?
|
recipe : '@'? NAME parameter* ('+' parameter)? ':' dependencies? body?
|
||||||
|
|
||||||
|
28
README.asc
28
README.asc
@ -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:
|
Backticks can be used to store the result of commands:
|
||||||
|
|
||||||
@ -390,7 +414,7 @@ search QUERY:
|
|||||||
lynx 'https://www.google.com/?q={{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:
|
Recipes that start with a `#!` are executed as scripts, so you can write recipes in other languages:
|
||||||
|
|
||||||
|
@ -82,37 +82,40 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
expression: &Expression<'a>,
|
expression: &Expression<'a>,
|
||||||
arguments: &Map<&str, Cow<str>>
|
arguments: &Map<&str, Cow<str>>
|
||||||
) -> RunResult<'a, String> {
|
) -> RunResult<'a, String> {
|
||||||
Ok(match *expression {
|
match *expression {
|
||||||
Expression::Variable{name, ..} => {
|
Expression::Variable{name, ..} => {
|
||||||
if self.evaluated.contains_key(name) {
|
if self.evaluated.contains_key(name) {
|
||||||
self.evaluated[name].clone()
|
Ok(self.evaluated[name].clone())
|
||||||
} else if self.scope.contains_key(name) {
|
} else if self.scope.contains_key(name) {
|
||||||
self.scope[name].clone()
|
Ok(self.scope[name].clone())
|
||||||
} else if self.assignments.contains_key(name) {
|
} else if self.assignments.contains_key(name) {
|
||||||
self.evaluate_assignment(name)?;
|
self.evaluate_assignment(name)?;
|
||||||
self.evaluated[name].clone()
|
Ok(self.evaluated[name].clone())
|
||||||
} else if arguments.contains_key(name) {
|
} else if arguments.contains_key(name) {
|
||||||
arguments[name].to_string()
|
Ok(arguments[name].to_string())
|
||||||
} else {
|
} else {
|
||||||
return Err(RuntimeError::Internal {
|
Err(RuntimeError::Internal {
|
||||||
message: format!("attempted to evaluate undefined variable `{}`", name)
|
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} => {
|
Expression::Backtick{raw, ref token} => {
|
||||||
if self.dry_run {
|
if self.dry_run {
|
||||||
format!("`{}`", raw)
|
Ok(format!("`{}`", raw))
|
||||||
} else {
|
} else {
|
||||||
self.run_backtick(raw, token)?
|
Ok(self.run_backtick(raw, token)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expression::Concatination{ref lhs, ref rhs} => {
|
Expression::Concatination{ref lhs, ref rhs} => {
|
||||||
self.evaluate_expression(lhs, arguments)?
|
Ok(
|
||||||
+
|
self.evaluate_expression(lhs, arguments)?
|
||||||
&self.evaluate_expression(rhs, arguments)?
|
+
|
||||||
|
&self.evaluate_expression(rhs, arguments)?
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_backtick(
|
fn run_backtick(
|
||||||
|
@ -75,6 +75,7 @@ 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::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)?;
|
||||||
@ -129,4 +130,15 @@ mod test {
|
|||||||
width: Some(2),
|
width: Some(2),
|
||||||
kind: UndefinedVariable{variable: "yy"},
|
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"},
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -29,12 +29,13 @@ pub enum CompilationErrorKind<'a> {
|
|||||||
InvalidEscapeSequence{character: char},
|
InvalidEscapeSequence{character: char},
|
||||||
MixedLeadingWhitespace{whitespace: &'a str},
|
MixedLeadingWhitespace{whitespace: &'a str},
|
||||||
OuterShebang,
|
OuterShebang,
|
||||||
|
ParameterFollowsVariadicParameter{parameter: &'a str},
|
||||||
ParameterShadowsVariable{parameter: &'a str},
|
ParameterShadowsVariable{parameter: &'a str},
|
||||||
RequiredParameterFollowsDefaultParameter{parameter: &'a str},
|
RequiredParameterFollowsDefaultParameter{parameter: &'a str},
|
||||||
ParameterFollowsVariadicParameter{parameter: &'a str},
|
|
||||||
UndefinedVariable{variable: &'a str},
|
UndefinedVariable{variable: &'a str},
|
||||||
UnexpectedToken{expected: Vec<TokenKind>, found: TokenKind},
|
UnexpectedToken{expected: Vec<TokenKind>, found: TokenKind},
|
||||||
UnknownDependency{recipe: &'a str, unknown: &'a str},
|
UnknownDependency{recipe: &'a str, unknown: &'a str},
|
||||||
|
UnknownFunction{function: &'a str},
|
||||||
UnknownStartOfToken,
|
UnknownStartOfToken,
|
||||||
UnterminatedString,
|
UnterminatedString,
|
||||||
}
|
}
|
||||||
@ -123,6 +124,9 @@ impl<'a> Display for CompilationError<'a> {
|
|||||||
UndefinedVariable{variable} => {
|
UndefinedVariable{variable} => {
|
||||||
writeln!(f, "Variable `{}` not defined", variable)?;
|
writeln!(f, "Variable `{}` not defined", variable)?;
|
||||||
}
|
}
|
||||||
|
UnknownFunction{function} => {
|
||||||
|
writeln!(f, "Call to unknown function `{}`", function)?;
|
||||||
|
}
|
||||||
UnknownStartOfToken => {
|
UnknownStartOfToken => {
|
||||||
writeln!(f, "Unknown start of token:")?;
|
writeln!(f, "Unknown start of token:")?;
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,11 @@ use common::*;
|
|||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
pub enum Expression<'a> {
|
pub enum Expression<'a> {
|
||||||
Variable{name: &'a str, token: Token<'a>},
|
|
||||||
String{cooked_string: CookedString<'a>},
|
|
||||||
Backtick{raw: &'a str, token: Token<'a>},
|
Backtick{raw: &'a str, token: Token<'a>},
|
||||||
|
Call{name: &'a str, token: Token<'a>},
|
||||||
Concatination{lhs: Box<Expression<'a>>, rhs: Box<Expression<'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> {
|
impl<'a> Expression<'a> {
|
||||||
@ -14,12 +15,19 @@ impl<'a> Expression<'a> {
|
|||||||
stack: vec![self],
|
stack: vec![self],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn functions(&'a self) -> Functions<'a> {
|
||||||
|
Functions {
|
||||||
|
stack: vec![self],
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)?,
|
||||||
@ -37,8 +45,34 @@ impl<'a> Iterator for Variables<'a> {
|
|||||||
|
|
||||||
fn next(&mut self) -> Option<&'a Token<'a>> {
|
fn next(&mut self) -> Option<&'a Token<'a>> {
|
||||||
match self.stack.pop() {
|
match self.stack.pop() {
|
||||||
None | Some(&Expression::String{..}) | Some(&Expression::Backtick{..}) => None,
|
None
|
||||||
Some(&Expression::Variable{ref token,..}) => Some(token),
|
| 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);
|
||||||
|
self.stack.push(rhs);
|
||||||
|
self.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}) => {
|
Some(&Expression::Concatination{ref lhs, ref rhs}) => {
|
||||||
self.stack.push(lhs);
|
self.stack.push(lhs);
|
||||||
self.stack.push(rhs);
|
self.stack.push(rhs);
|
||||||
|
33
src/functions.rs
Normal file
33
src/functions.rs
Normal 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()
|
||||||
|
}
|
16
src/lexer.rs
16
src/lexer.rs
@ -131,6 +131,8 @@ impl<'a> Lexer<'a> {
|
|||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref BACKTICK: Regex = token(r"`[^`\n\r]*`" );
|
static ref BACKTICK: Regex = token(r"`[^`\n\r]*`" );
|
||||||
static ref COLON: Regex = token(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 AT: 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)$" );
|
||||||
@ -140,7 +142,7 @@ impl<'a> Lexer<'a> {
|
|||||||
static ref INTERPOLATION_START_TOKEN: Regex = token(r"[{][{]" );
|
static ref INTERPOLATION_START_TOKEN: Regex = token(r"[{][{]" );
|
||||||
static ref NAME: Regex = token(r"([a-zA-Z_][a-zA-Z0-9_-]*)" );
|
static ref NAME: Regex = token(r"([a-zA-Z_][a-zA-Z0-9_-]*)" );
|
||||||
static ref PLUS: Regex = token(r"[+]" );
|
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 RAW_STRING: Regex = token(r#"'[^']*'"# );
|
||||||
static ref UNTERMINATED_RAW_STRING: Regex = token(r#"'[^']*"# );
|
static ref UNTERMINATED_RAW_STRING: Regex = token(r#"'[^']*"# );
|
||||||
static ref INTERPOLATION_START: Regex = re(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)
|
(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) = 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) {
|
} else if let Some(captures) = PLUS.captures(self.rest) {
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Plus)
|
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Plus)
|
||||||
} else if let Some(captures) = EQUALS.captures(self.rest) {
|
} else if let Some(captures) = EQUALS.captures(self.rest) {
|
||||||
@ -338,6 +344,8 @@ mod test {
|
|||||||
InterpolationStart => "{",
|
InterpolationStart => "{",
|
||||||
Line{..} => "^",
|
Line{..} => "^",
|
||||||
Name => "N",
|
Name => "N",
|
||||||
|
ParenL => "(",
|
||||||
|
ParenR => ")",
|
||||||
Plus => "+",
|
Plus => "+",
|
||||||
RawString => "'",
|
RawString => "'",
|
||||||
StringToken => "\"",
|
StringToken => "\"",
|
||||||
@ -510,6 +518,12 @@ c: b
|
|||||||
"$N:N$>^_$$<N:$>^_$^_$$<N:N$>^_$$<N:N$>^_<.",
|
"$N:N$>^_$$<N:$>^_$^_$$<N:N$>^_$$<N:N$>^_<.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
summary_test! {
|
||||||
|
tokenize_parens,
|
||||||
|
r"((())) )abc(+",
|
||||||
|
"((())))N(+.",
|
||||||
|
}
|
||||||
|
|
||||||
error_test! {
|
error_test! {
|
||||||
name: tokenize_space_then_tab,
|
name: tokenize_space_then_tab,
|
||||||
input: "a:
|
input: "a:
|
||||||
|
@ -7,6 +7,7 @@ extern crate edit_distance;
|
|||||||
extern crate itertools;
|
extern crate itertools;
|
||||||
extern crate libc;
|
extern crate libc;
|
||||||
extern crate regex;
|
extern crate regex;
|
||||||
|
extern crate target;
|
||||||
extern crate tempdir;
|
extern crate tempdir;
|
||||||
extern crate unicode_width;
|
extern crate unicode_width;
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ mod configuration;
|
|||||||
mod cooked_string;
|
mod cooked_string;
|
||||||
mod expression;
|
mod expression;
|
||||||
mod fragment;
|
mod fragment;
|
||||||
|
mod functions;
|
||||||
mod justfile;
|
mod justfile;
|
||||||
mod lexer;
|
mod lexer;
|
||||||
mod misc;
|
mod misc;
|
||||||
|
@ -251,8 +251,20 @@ impl<'a> Parser<'a> {
|
|||||||
fn expression(&mut self, interpolation: bool) -> CompilationResult<'a, Expression<'a>> {
|
fn expression(&mut self, interpolation: bool) -> 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 => Expression::Variable {name: first.lexeme, token: first},
|
Name => {
|
||||||
Backtick => Expression::Backtick {
|
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],
|
raw: &first.lexeme[1..first.lexeme.len()-1],
|
||||||
token: first
|
token: first
|
||||||
},
|
},
|
||||||
@ -764,6 +776,26 @@ c = a + b + a + b",
|
|||||||
kind: UnexpectedToken{expected: vec![Plus, Eol, InterpolationEnd], found: Dedent},
|
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! {
|
compilation_error_test! {
|
||||||
name: plus_following_parameter,
|
name: plus_following_parameter,
|
||||||
input: "a b c+:",
|
input: "a b c+:",
|
||||||
|
@ -27,24 +27,39 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
|
|||||||
resolver.seen = empty();
|
resolver.seen = empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
for recipe in recipes.values() {
|
for recipe in recipes.values() {
|
||||||
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() {
|
||||||
|
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() {
|
for variable in expression.variables() {
|
||||||
let name = variable.lexeme;
|
let name = variable.lexeme;
|
||||||
let undefined = !assignments.contains_key(name)
|
let undefined = !assignments.contains_key(name)
|
||||||
&& !recipe.parameters.iter().any(|p| p.name == name);
|
&& !recipe.parameters.iter().any(|p| p.name == name);
|
||||||
if undefined {
|
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,
|
|
||||||
// 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
|
|
||||||
let error = variable.error(UndefinedVariable{variable: name});
|
let error = variable.error(UndefinedVariable{variable: name});
|
||||||
return Err(CompilationError {
|
return Err(CompilationError {
|
||||||
text: text,
|
text: text,
|
||||||
@ -152,4 +167,14 @@ mod test {
|
|||||||
width: Some(3),
|
width: Some(3),
|
||||||
kind: UndefinedVariable{variable: "lol"},
|
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"},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,8 @@ pub enum TokenKind {
|
|||||||
InterpolationStart,
|
InterpolationStart,
|
||||||
Line,
|
Line,
|
||||||
Name,
|
Name,
|
||||||
|
ParenL,
|
||||||
|
ParenR,
|
||||||
Plus,
|
Plus,
|
||||||
RawString,
|
RawString,
|
||||||
StringToken,
|
StringToken,
|
||||||
@ -63,6 +65,8 @@ impl Display for TokenKind {
|
|||||||
Name => "name",
|
Name => "name",
|
||||||
Plus => "'+'",
|
Plus => "'+'",
|
||||||
At => "'@'",
|
At => "'@'",
|
||||||
|
ParenL => "'('",
|
||||||
|
ParenR => "')'",
|
||||||
StringToken => "string",
|
StringToken => "string",
|
||||||
RawString => "raw string",
|
RawString => "raw string",
|
||||||
Text => "command text",
|
Text => "command text",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
extern crate brev;
|
extern crate brev;
|
||||||
extern crate executable_path;
|
extern crate executable_path;
|
||||||
extern crate libc;
|
extern crate libc;
|
||||||
|
extern crate target;
|
||||||
extern crate tempdir;
|
extern crate tempdir;
|
||||||
|
|
||||||
use executable_path::executable_path;
|
use executable_path::executable_path;
|
||||||
@ -1174,6 +1175,35 @@ foo:
|
|||||||
status: EXIT_SUCCESS,
|
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! {
|
integration_test! {
|
||||||
name: quiet_recipe,
|
name: quiet_recipe,
|
||||||
justfile: r#"
|
justfile: r#"
|
||||||
@ -1260,6 +1290,19 @@ integration_test! {
|
|||||||
status: EXIT_FAILURE,
|
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! {
|
integration_test! {
|
||||||
name: dependency_takes_arguments,
|
name: dependency_takes_arguments,
|
||||||
|
Loading…
Reference in New Issue
Block a user