Allow arbitrary expressions as default arguments (#400)
This commit is contained in:
parent
12f9428695
commit
fe0a6c252c
@ -63,6 +63,7 @@ value : NAME '(' arguments? ')'
|
|||||||
| RAW_STRING
|
| RAW_STRING
|
||||||
| BACKTICK
|
| BACKTICK
|
||||||
| NAME
|
| NAME
|
||||||
|
| '(' expression ')'
|
||||||
|
|
||||||
arguments : expression ',' arguments
|
arguments : expression ',' arguments
|
||||||
| expression ','?
|
| expression ','?
|
||||||
@ -70,8 +71,7 @@ arguments : expression ',' arguments
|
|||||||
recipe : '@'? NAME parameter* ('+' parameter)? ':' dependencies? body?
|
recipe : '@'? NAME parameter* ('+' parameter)? ':' dependencies? body?
|
||||||
|
|
||||||
parameter : NAME
|
parameter : NAME
|
||||||
| NAME '=' STRING
|
| NAME '=' value
|
||||||
| NAME '=' RAW_STRING
|
|
||||||
|
|
||||||
dependencies : NAME+
|
dependencies : NAME+
|
||||||
|
|
||||||
|
13
README.adoc
13
README.adoc
@ -480,7 +480,9 @@ cd my-awesome-project && make
|
|||||||
Parameters may have default values:
|
Parameters may have default values:
|
||||||
|
|
||||||
```make
|
```make
|
||||||
test target tests='all':
|
default = 'all'
|
||||||
|
|
||||||
|
test target tests=default:
|
||||||
@echo 'Testing {{target}}:{{tests}}...'
|
@echo 'Testing {{target}}:{{tests}}...'
|
||||||
./test --tests {{tests}} {{target}}
|
./test --tests {{tests}} {{target}}
|
||||||
```
|
```
|
||||||
@ -501,6 +503,15 @@ Testing server:unit...
|
|||||||
./test --tests unit server
|
./test --tests unit server
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Default values may be arbitrary expressions, but concatenations must be parenthesized:
|
||||||
|
|
||||||
|
```make
|
||||||
|
arch = "wasm"
|
||||||
|
|
||||||
|
test triple=(arch + "-unknown-unknown"):
|
||||||
|
./test {{triple}}
|
||||||
|
```
|
||||||
|
|
||||||
The last parameter of a recipe may be variadic, indicated with a `+` before the argument name:
|
The last parameter of a recipe may be variadic, indicated with a `+` before the argument name:
|
||||||
|
|
||||||
```make
|
```make
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Alias<'a> {
|
pub struct Alias<'a> {
|
||||||
pub name: &'a str,
|
pub name: &'a str,
|
||||||
pub target: &'a str,
|
pub target: &'a str,
|
||||||
|
@ -83,7 +83,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn evaluate_expression(
|
pub fn evaluate_expression(
|
||||||
&mut self,
|
&mut self,
|
||||||
expression: &Expression<'a>,
|
expression: &Expression<'a>,
|
||||||
arguments: &BTreeMap<&str, Cow<str>>,
|
arguments: &BTreeMap<&str, Cow<str>>,
|
||||||
@ -120,7 +120,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
};
|
};
|
||||||
evaluate_function(token, name, &context, &call_arguments)
|
evaluate_function(token, name, &context, &call_arguments)
|
||||||
}
|
}
|
||||||
Expression::String { ref cooked_string } => Ok(cooked_string.cooked.clone()),
|
Expression::String { ref cooked_string } => Ok(cooked_string.cooked.to_string()),
|
||||||
Expression::Backtick { raw, ref token } => {
|
Expression::Backtick { raw, ref token } => {
|
||||||
if self.dry_run {
|
if self.dry_run {
|
||||||
Ok(format!("`{}`", raw))
|
Ok(format!("`{}`", raw))
|
||||||
@ -131,6 +131,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
Expression::Concatination { ref lhs, ref rhs } => {
|
Expression::Concatination { ref lhs, ref rhs } => {
|
||||||
Ok(self.evaluate_expression(lhs, arguments)? + &self.evaluate_expression(rhs, arguments)?)
|
Ok(self.evaluate_expression(lhs, arguments)? + &self.evaluate_expression(rhs, arguments)?)
|
||||||
}
|
}
|
||||||
|
Expression::Group { ref expression } => self.evaluate_expression(&expression, arguments),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_expression(&mut self, expression: &Expression<'a>) -> CompilationResult<'a, ()> {
|
fn resolve_expression(&mut self, expression: &Expression<'a>) -> CompilationResult<'a, ()> {
|
||||||
match *expression {
|
match expression {
|
||||||
Expression::Variable { name, ref token } => {
|
Expression::Variable { name, ref token } => {
|
||||||
if self.evaluated.contains(name) {
|
if self.evaluated.contains(name) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@ -83,6 +83,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
|
|||||||
self.resolve_expression(rhs)?;
|
self.resolve_expression(rhs)?;
|
||||||
}
|
}
|
||||||
Expression::String { .. } | Expression::Backtick { .. } => {}
|
Expression::String { .. } | Expression::Backtick { .. } => {}
|
||||||
|
Expression::Group { expression } => self.resolve_expression(expression)?,
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ use crate::common::*;
|
|||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
pub struct CookedString<'a> {
|
pub struct CookedString<'a> {
|
||||||
pub raw: &'a str,
|
pub raw: &'a str,
|
||||||
pub cooked: String,
|
pub cooked: Cow<'a, str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CookedString<'a> {
|
impl<'a> CookedString<'a> {
|
||||||
@ -12,7 +12,7 @@ impl<'a> CookedString<'a> {
|
|||||||
|
|
||||||
if let TokenKind::RawString = token.kind {
|
if let TokenKind::RawString = token.kind {
|
||||||
Ok(CookedString {
|
Ok(CookedString {
|
||||||
cooked: raw.to_string(),
|
cooked: Cow::Borrowed(raw),
|
||||||
raw,
|
raw,
|
||||||
})
|
})
|
||||||
} else if let TokenKind::StringToken = token.kind {
|
} else if let TokenKind::StringToken = token.kind {
|
||||||
@ -41,7 +41,10 @@ impl<'a> CookedString<'a> {
|
|||||||
}
|
}
|
||||||
cooked.push(c);
|
cooked.push(c);
|
||||||
}
|
}
|
||||||
Ok(CookedString { raw, cooked })
|
Ok(CookedString {
|
||||||
|
raw,
|
||||||
|
cooked: Cow::Owned(cooked),
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(token.error(CompilationErrorKind::Internal {
|
Err(token.error(CompilationErrorKind::Internal {
|
||||||
message: "cook_string() called on non-string token".to_string(),
|
message: "cook_string() called on non-string token".to_string(),
|
||||||
@ -49,3 +52,12 @@ impl<'a> CookedString<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> Display for CookedString<'a> {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
match self.cooked {
|
||||||
|
Cow::Borrowed(raw) => write!(f, "'{}'", raw),
|
||||||
|
Cow::Owned(_) => write!(f, "\"{}\"", self.raw),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -22,6 +22,9 @@ pub enum Expression<'a> {
|
|||||||
name: &'a str,
|
name: &'a str,
|
||||||
token: Token<'a>,
|
token: Token<'a>,
|
||||||
},
|
},
|
||||||
|
Group {
|
||||||
|
expression: Box<Expression<'a>>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Expression<'a> {
|
impl<'a> Expression<'a> {
|
||||||
@ -39,7 +42,7 @@ impl<'a> Display for Expression<'a> {
|
|||||||
match *self {
|
match *self {
|
||||||
Expression::Backtick { raw, .. } => write!(f, "`{}`", raw)?,
|
Expression::Backtick { raw, .. } => write!(f, "`{}`", raw)?,
|
||||||
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)?,
|
||||||
Expression::Variable { name, .. } => write!(f, "{}", name)?,
|
Expression::Variable { name, .. } => write!(f, "{}", name)?,
|
||||||
Expression::Call {
|
Expression::Call {
|
||||||
name,
|
name,
|
||||||
@ -56,6 +59,7 @@ impl<'a> Display for Expression<'a> {
|
|||||||
}
|
}
|
||||||
write!(f, ")")?;
|
write!(f, ")")?;
|
||||||
}
|
}
|
||||||
|
Expression::Group { ref expression } => write!(f, "({})", expression)?,
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -71,15 +75,19 @@ 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
|
None
|
||||||
| Some(&Expression::String { .. })
|
| Some(Expression::String { .. })
|
||||||
| Some(&Expression::Backtick { .. })
|
| Some(Expression::Backtick { .. })
|
||||||
| Some(&Expression::Call { .. }) => None,
|
| Some(Expression::Call { .. }) => None,
|
||||||
Some(&Expression::Variable { ref token, .. }) => Some(token),
|
Some(Expression::Variable { token, .. }) => Some(token),
|
||||||
Some(&Expression::Concatination { ref lhs, ref rhs }) => {
|
Some(Expression::Concatination { lhs, rhs }) => {
|
||||||
self.stack.push(lhs);
|
self.stack.push(lhs);
|
||||||
self.stack.push(rhs);
|
self.stack.push(rhs);
|
||||||
self.next()
|
self.next()
|
||||||
}
|
}
|
||||||
|
Some(Expression::Group { expression }) => {
|
||||||
|
self.stack.push(expression);
|
||||||
|
self.next()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,19 +102,21 @@ impl<'a> Iterator for Functions<'a> {
|
|||||||
fn next(&mut self) -> Option<Self::Item> {
|
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 {
|
Some(Expression::Call {
|
||||||
ref token,
|
token, arguments, ..
|
||||||
ref arguments,
|
|
||||||
..
|
|
||||||
}) => Some((token, arguments.len())),
|
}) => Some((token, arguments.len())),
|
||||||
Some(&Expression::Concatination { ref lhs, ref rhs }) => {
|
Some(Expression::Concatination { lhs, rhs }) => {
|
||||||
self.stack.push(lhs);
|
self.stack.push(lhs);
|
||||||
self.stack.push(rhs);
|
self.stack.push(rhs);
|
||||||
self.next()
|
self.next()
|
||||||
}
|
}
|
||||||
|
Some(Expression::Group { expression }) => {
|
||||||
|
self.stack.push(expression);
|
||||||
|
self.next()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Justfile<'a> {
|
pub struct Justfile<'a> {
|
||||||
pub recipes: BTreeMap<&'a str, Recipe<'a>>,
|
pub recipes: BTreeMap<&'a str, Recipe<'a>>,
|
||||||
pub assignments: BTreeMap<&'a str, Expression<'a>>,
|
pub assignments: BTreeMap<&'a str, Expression<'a>>,
|
||||||
|
@ -617,6 +617,12 @@ c: b
|
|||||||
"#$#$.",
|
"#$#$.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
summary_test! {
|
||||||
|
multiple_recipes,
|
||||||
|
"a:\n foo\nb:",
|
||||||
|
"N:$>^_$<N:.",
|
||||||
|
}
|
||||||
|
|
||||||
error_test! {
|
error_test! {
|
||||||
name: tokenize_space_then_tab,
|
name: tokenize_space_then_tab,
|
||||||
input: "a:
|
input: "a:
|
||||||
|
@ -2,7 +2,7 @@ use crate::common::*;
|
|||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
pub struct Parameter<'a> {
|
pub struct Parameter<'a> {
|
||||||
pub default: Option<String>,
|
pub default: Option<Expression<'a>>,
|
||||||
pub name: &'a str,
|
pub name: &'a str,
|
||||||
pub token: Token<'a>,
|
pub token: Token<'a>,
|
||||||
pub variadic: bool,
|
pub variadic: bool,
|
||||||
@ -16,11 +16,7 @@ impl<'a> Display for Parameter<'a> {
|
|||||||
}
|
}
|
||||||
write!(f, "{}", color.parameter().paint(self.name))?;
|
write!(f, "{}", color.parameter().paint(self.name))?;
|
||||||
if let Some(ref default) = self.default {
|
if let Some(ref default) = self.default {
|
||||||
let escaped = default
|
write!(f, "={}", color.string().paint(&default.to_string()))?;
|
||||||
.chars()
|
|
||||||
.flat_map(char::escape_default)
|
|
||||||
.collect::<String>();;
|
|
||||||
write!(f, r#"='{}'"#, color.string().paint(&escaped))?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
217
src/parser.rs
217
src/parser.rs
@ -49,15 +49,6 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accept_any(&mut self, kinds: &[TokenKind]) -> Option<Token<'a>> {
|
|
||||||
for kind in kinds {
|
|
||||||
if self.peek(*kind) {
|
|
||||||
return self.tokens.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accepted(&mut self, kind: TokenKind) -> bool {
|
fn accepted(&mut self, kind: TokenKind) -> bool {
|
||||||
self.accept(kind).is_some()
|
self.accept(kind).is_some()
|
||||||
}
|
}
|
||||||
@ -137,12 +128,7 @@ impl<'a> Parser<'a> {
|
|||||||
|
|
||||||
let default;
|
let default;
|
||||||
if self.accepted(Equals) {
|
if self.accepted(Equals) {
|
||||||
if let Some(string) = self.accept_any(&[StringToken, RawString]) {
|
default = Some(self.value()?);
|
||||||
default = Some(CookedString::new(&string)?.cooked);
|
|
||||||
} else {
|
|
||||||
let unexpected = self.tokens.next().unwrap();
|
|
||||||
return Err(self.unexpected_token(&unexpected, &[StringToken, RawString]));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
default = None
|
default = None
|
||||||
}
|
}
|
||||||
@ -243,6 +229,10 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
while lines.last().map(Vec::is_empty).unwrap_or(false) {
|
||||||
|
lines.pop();
|
||||||
|
}
|
||||||
|
|
||||||
self.recipes.insert(
|
self.recipes.insert(
|
||||||
name.lexeme,
|
name.lexeme,
|
||||||
Recipe {
|
Recipe {
|
||||||
@ -262,9 +252,10 @@ impl<'a> Parser<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expression(&mut self) -> CompilationResult<'a, Expression<'a>> {
|
fn value(&mut self) -> CompilationResult<'a, Expression<'a>> {
|
||||||
let first = self.tokens.next().unwrap();
|
let first = self.tokens.next().unwrap();
|
||||||
let lhs = match first.kind {
|
|
||||||
|
match first.kind {
|
||||||
Name => {
|
Name => {
|
||||||
if self.peek(ParenL) {
|
if self.peek(ParenL) {
|
||||||
if let Some(token) = self.expect(ParenL) {
|
if let Some(token) = self.expect(ParenL) {
|
||||||
@ -274,30 +265,46 @@ impl<'a> Parser<'a> {
|
|||||||
if let Some(token) = self.expect(ParenR) {
|
if let Some(token) = self.expect(ParenR) {
|
||||||
return Err(self.unexpected_token(&token, &[Name, StringToken, ParenR]));
|
return Err(self.unexpected_token(&token, &[Name, StringToken, ParenR]));
|
||||||
}
|
}
|
||||||
Expression::Call {
|
Ok(Expression::Call {
|
||||||
name: first.lexeme,
|
name: first.lexeme,
|
||||||
token: first,
|
token: first,
|
||||||
arguments,
|
arguments,
|
||||||
}
|
})
|
||||||
} else {
|
} else {
|
||||||
Expression::Variable {
|
Ok(Expression::Variable {
|
||||||
name: first.lexeme,
|
name: first.lexeme,
|
||||||
token: first,
|
token: first,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
Backtick => Ok(Expression::Backtick {
|
||||||
Backtick => Expression::Backtick {
|
|
||||||
raw: &first.lexeme[1..first.lexeme.len() - 1],
|
raw: &first.lexeme[1..first.lexeme.len() - 1],
|
||||||
token: first,
|
token: first,
|
||||||
},
|
}),
|
||||||
RawString | StringToken => Expression::String {
|
RawString | StringToken => Ok(Expression::String {
|
||||||
cooked_string: CookedString::new(&first)?,
|
cooked_string: CookedString::new(&first)?,
|
||||||
},
|
}),
|
||||||
_ => return Err(self.unexpected_token(&first, &[Name, StringToken])),
|
ParenL => {
|
||||||
};
|
let expression = self.expression()?;
|
||||||
|
|
||||||
|
if let Some(token) = self.expect(ParenR) {
|
||||||
|
return Err(self.unexpected_token(&token, &[ParenR]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Expression::Group {
|
||||||
|
expression: Box::new(expression),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err(self.unexpected_token(&first, &[Name, StringToken])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expression(&mut self) -> CompilationResult<'a, Expression<'a>> {
|
||||||
|
let lhs = self.value()?;
|
||||||
|
|
||||||
if self.accepted(Plus) {
|
if self.accepted(Plus) {
|
||||||
let rhs = self.expression()?;
|
let rhs = self.expression()?;
|
||||||
|
|
||||||
Ok(Expression::Concatination {
|
Ok(Expression::Concatination {
|
||||||
lhs: Box::new(lhs),
|
lhs: Box::new(lhs),
|
||||||
rhs: Box::new(rhs),
|
rhs: Box::new(rhs),
|
||||||
@ -463,6 +470,8 @@ impl<'a> Parser<'a> {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AssignmentResolver::resolve_assignments(&self.assignments, &self.assignment_tokens)?;
|
||||||
|
|
||||||
RecipeResolver::resolve_recipes(&self.recipes, &self.assignments, self.text)?;
|
RecipeResolver::resolve_recipes(&self.recipes, &self.assignments, self.text)?;
|
||||||
|
|
||||||
for recipe in self.recipes.values() {
|
for recipe in self.recipes.values() {
|
||||||
@ -486,8 +495,6 @@ impl<'a> Parser<'a> {
|
|||||||
|
|
||||||
AliasResolver::resolve_aliases(&self.aliases, &self.recipes, &self.alias_tokens)?;
|
AliasResolver::resolve_aliases(&self.aliases, &self.recipes, &self.alias_tokens)?;
|
||||||
|
|
||||||
AssignmentResolver::resolve_assignments(&self.assignments, &self.assignment_tokens)?;
|
|
||||||
|
|
||||||
Ok(Justfile {
|
Ok(Justfile {
|
||||||
recipes: self.recipes,
|
recipes: self.recipes,
|
||||||
assignments: self.assignments,
|
assignments: self.assignments,
|
||||||
@ -513,9 +520,17 @@ mod test {
|
|||||||
let actual = format!("{:#}", justfile);
|
let actual = format!("{:#}", justfile);
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
println!("got:\n\"{}\"\n", actual);
|
println!("got:\n\"{}\"\n", actual);
|
||||||
println!("\texpected:\n\"{}\"", expected);
|
println!("expected:\n\"{}\"", expected);
|
||||||
assert_eq!(actual, expected);
|
assert_eq!(actual, expected);
|
||||||
}
|
}
|
||||||
|
println!("Re-parsing...");
|
||||||
|
let reparsed = parse_success(&actual);
|
||||||
|
let redumped = format!("{:#}", reparsed);
|
||||||
|
if redumped != actual {
|
||||||
|
println!("reparsed:\n\"{}\"\n", redumped);
|
||||||
|
println!("expected:\n\"{}\"", actual);
|
||||||
|
assert_eq!(redumped, actual);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -539,7 +554,18 @@ foo a="b\t":
|
|||||||
|
|
||||||
|
|
||||||
"#,
|
"#,
|
||||||
r#"foo a='b\t':"#,
|
r#"foo a="b\t":"#,
|
||||||
|
}
|
||||||
|
|
||||||
|
summary_test! {
|
||||||
|
parse_multiple,
|
||||||
|
r#"
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
"#,
|
||||||
|
r#"a:
|
||||||
|
|
||||||
|
b:"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
summary_test! {
|
summary_test! {
|
||||||
@ -561,7 +587,7 @@ foo +a="Hello":
|
|||||||
|
|
||||||
|
|
||||||
"#,
|
"#,
|
||||||
r#"foo +a='Hello':"#,
|
r#"foo +a="Hello":"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
summary_test! {
|
summary_test! {
|
||||||
@ -572,7 +598,7 @@ foo a='b\t':
|
|||||||
|
|
||||||
|
|
||||||
"#,
|
"#,
|
||||||
r#"foo a='b\\t':"#,
|
r#"foo a='b\t':"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
summary_test! {
|
summary_test! {
|
||||||
@ -671,7 +697,7 @@ install:
|
|||||||
\t\treturn
|
\t\treturn
|
||||||
\tfi
|
\tfi
|
||||||
",
|
",
|
||||||
"practicum = \"hello\"
|
"practicum = 'hello'
|
||||||
|
|
||||||
install:
|
install:
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
@ -765,10 +791,76 @@ x = env_var('foo',)
|
|||||||
|
|
||||||
a:
|
a:
|
||||||
{{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var("baz"))}}"#,
|
{{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var("baz"))}}"#,
|
||||||
r#"x = env_var("foo")
|
r#"x = env_var('foo')
|
||||||
|
|
||||||
a:
|
a:
|
||||||
{{env_var_or_default("foo" + "bar", "baz")}} {{env_var(env_var("baz"))}}"#,
|
{{env_var_or_default('foo' + 'bar', 'baz')}} {{env_var(env_var("baz"))}}"#,
|
||||||
|
}
|
||||||
|
|
||||||
|
summary_test! {
|
||||||
|
parameter_default_string,
|
||||||
|
r#"
|
||||||
|
f x="abc":
|
||||||
|
"#,
|
||||||
|
r#"f x="abc":"#,
|
||||||
|
}
|
||||||
|
|
||||||
|
summary_test! {
|
||||||
|
parameter_default_raw_string,
|
||||||
|
r#"
|
||||||
|
f x='abc':
|
||||||
|
"#,
|
||||||
|
r#"f x='abc':"#,
|
||||||
|
}
|
||||||
|
|
||||||
|
summary_test! {
|
||||||
|
parameter_default_backtick,
|
||||||
|
r#"
|
||||||
|
f x=`echo hello`:
|
||||||
|
"#,
|
||||||
|
r#"f x=`echo hello`:"#,
|
||||||
|
}
|
||||||
|
|
||||||
|
summary_test! {
|
||||||
|
parameter_default_concatination_string,
|
||||||
|
r#"
|
||||||
|
f x=(`echo hello` + "foo"):
|
||||||
|
"#,
|
||||||
|
r#"f x=(`echo hello` + "foo"):"#,
|
||||||
|
}
|
||||||
|
|
||||||
|
summary_test! {
|
||||||
|
parameter_default_concatination_variable,
|
||||||
|
r#"
|
||||||
|
x = "10"
|
||||||
|
f y=(`echo hello` + x) +z="foo":
|
||||||
|
"#,
|
||||||
|
r#"x = "10"
|
||||||
|
|
||||||
|
f y=(`echo hello` + x) +z="foo":"#,
|
||||||
|
}
|
||||||
|
|
||||||
|
summary_test! {
|
||||||
|
parameter_default_multiple,
|
||||||
|
r#"
|
||||||
|
x = "10"
|
||||||
|
f y=(`echo hello` + x) +z=("foo" + "bar"):
|
||||||
|
"#,
|
||||||
|
r#"x = "10"
|
||||||
|
|
||||||
|
f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
|
||||||
|
}
|
||||||
|
|
||||||
|
summary_test! {
|
||||||
|
concatination_in_group,
|
||||||
|
"x = ('0' + '1')",
|
||||||
|
"x = ('0' + '1')",
|
||||||
|
}
|
||||||
|
|
||||||
|
summary_test! {
|
||||||
|
string_in_group,
|
||||||
|
"x = ('0' )",
|
||||||
|
"x = ('0')",
|
||||||
}
|
}
|
||||||
|
|
||||||
compilation_error_test! {
|
compilation_error_test! {
|
||||||
@ -848,7 +940,7 @@ a:
|
|||||||
line: 0,
|
line: 0,
|
||||||
column: 10,
|
column: 10,
|
||||||
width: Some(1),
|
width: Some(1),
|
||||||
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Eol},
|
kind: UnexpectedToken{expected: vec![Name, StringToken], found: Eol},
|
||||||
}
|
}
|
||||||
|
|
||||||
compilation_error_test! {
|
compilation_error_test! {
|
||||||
@ -858,27 +950,7 @@ a:
|
|||||||
line: 0,
|
line: 0,
|
||||||
column: 10,
|
column: 10,
|
||||||
width: Some(0),
|
width: Some(0),
|
||||||
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Eof},
|
kind: UnexpectedToken{expected: vec![Name, StringToken], found: Eof},
|
||||||
}
|
|
||||||
|
|
||||||
compilation_error_test! {
|
|
||||||
name: missing_default_colon,
|
|
||||||
input: "hello arg=:",
|
|
||||||
index: 10,
|
|
||||||
line: 0,
|
|
||||||
column: 10,
|
|
||||||
width: Some(1),
|
|
||||||
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Colon},
|
|
||||||
}
|
|
||||||
|
|
||||||
compilation_error_test! {
|
|
||||||
name: missing_default_backtick,
|
|
||||||
input: "hello arg=`hello`",
|
|
||||||
index: 10,
|
|
||||||
line: 0,
|
|
||||||
column: 10,
|
|
||||||
width: Some(7),
|
|
||||||
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Backtick},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
compilation_error_test! {
|
compilation_error_test! {
|
||||||
@ -1065,4 +1137,33 @@ a:
|
|||||||
parse_success(&justfile);
|
parse_success(&justfile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_recipe_lines() {
|
||||||
|
let text = "a:";
|
||||||
|
let justfile = parse_success(&text);
|
||||||
|
|
||||||
|
assert_eq!(justfile.recipes["a"].lines.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_recipe_lines() {
|
||||||
|
let text = "a:\n foo";
|
||||||
|
let justfile = parse_success(&text);
|
||||||
|
|
||||||
|
assert_eq!(justfile.recipes["a"].lines.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complex_recipe_lines() {
|
||||||
|
let text = "a:
|
||||||
|
foo
|
||||||
|
|
||||||
|
b:
|
||||||
|
";
|
||||||
|
|
||||||
|
let justfile = parse_success(&text);
|
||||||
|
|
||||||
|
assert_eq!(justfile.recipes["a"].lines.len(), 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,11 +86,24 @@ impl<'a> Recipe<'a> {
|
|||||||
|
|
||||||
let mut argument_map = BTreeMap::new();
|
let mut argument_map = BTreeMap::new();
|
||||||
|
|
||||||
|
let mut evaluator = AssignmentEvaluator {
|
||||||
|
assignments: &empty(),
|
||||||
|
dry_run: configuration.dry_run,
|
||||||
|
evaluated: empty(),
|
||||||
|
invocation_directory: context.invocation_directory,
|
||||||
|
overrides: &empty(),
|
||||||
|
quiet: configuration.quiet,
|
||||||
|
scope: &context.scope,
|
||||||
|
shell: configuration.shell,
|
||||||
|
dotenv,
|
||||||
|
exports,
|
||||||
|
};
|
||||||
|
|
||||||
let mut rest = arguments;
|
let mut rest = arguments;
|
||||||
for parameter in &self.parameters {
|
for parameter in &self.parameters {
|
||||||
let value = if rest.is_empty() {
|
let value = if rest.is_empty() {
|
||||||
match parameter.default {
|
match parameter.default {
|
||||||
Some(ref default) => Cow::Borrowed(default.as_str()),
|
Some(ref default) => Cow::Owned(evaluator.evaluate_expression(default, &empty())?),
|
||||||
None => {
|
None => {
|
||||||
return Err(RuntimeError::Internal {
|
return Err(RuntimeError::Internal {
|
||||||
message: "missing parameter without default".to_string(),
|
message: "missing parameter without default".to_string(),
|
||||||
@ -109,19 +122,6 @@ impl<'a> Recipe<'a> {
|
|||||||
argument_map.insert(parameter.name, value);
|
argument_map.insert(parameter.name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut evaluator = AssignmentEvaluator {
|
|
||||||
assignments: &empty(),
|
|
||||||
dry_run: configuration.dry_run,
|
|
||||||
evaluated: empty(),
|
|
||||||
invocation_directory: context.invocation_directory,
|
|
||||||
overrides: &empty(),
|
|
||||||
quiet: configuration.quiet,
|
|
||||||
scope: &context.scope,
|
|
||||||
shell: configuration.shell,
|
|
||||||
dotenv,
|
|
||||||
exports,
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.shebang {
|
if self.shebang {
|
||||||
let mut evaluated_lines = vec![];
|
let mut evaluated_lines = vec![];
|
||||||
for line in &self.lines {
|
for line in &self.lines {
|
||||||
|
@ -2,11 +2,23 @@ use crate::common::*;
|
|||||||
|
|
||||||
use CompilationErrorKind::*;
|
use CompilationErrorKind::*;
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
pub struct RecipeResolver<'a: 'b, 'b> {
|
pub struct RecipeResolver<'a: 'b, 'b> {
|
||||||
stack: Vec<&'a str>,
|
stack: Vec<&'a str>,
|
||||||
seen: BTreeSet<&'a str>,
|
seen: BTreeSet<&'a str>,
|
||||||
resolved: BTreeSet<&'a str>,
|
resolved: BTreeSet<&'a str>,
|
||||||
recipes: &'b BTreeMap<&'a str, Recipe<'a>>,
|
recipes: &'b BTreeMap<&'a str, Recipe<'a>>,
|
||||||
|
assignments: &'b BTreeMap<&'a str, Expression<'a>>,
|
||||||
|
text: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> RecipeResolver<'a, 'b> {
|
impl<'a, 'b> RecipeResolver<'a, 'b> {
|
||||||
@ -19,6 +31,8 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
|
|||||||
seen: empty(),
|
seen: empty(),
|
||||||
stack: empty(),
|
stack: empty(),
|
||||||
resolved: empty(),
|
resolved: empty(),
|
||||||
|
assignments,
|
||||||
|
text,
|
||||||
recipes,
|
recipes,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -27,38 +41,56 @@ 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 parameter in &recipe.parameters {
|
||||||
|
if let Some(expression) = ¶meter.default {
|
||||||
|
for (function, argc) in expression.functions() {
|
||||||
|
resolver.resolve_function(function, argc)?;
|
||||||
|
}
|
||||||
|
for variable in expression.variables() {
|
||||||
|
resolver.resolve_variable(variable, &[])?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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, argc) in expression.functions() {
|
for (function, argc) in expression.functions() {
|
||||||
if let Err(error) = resolve_function(function, argc) {
|
resolver.resolve_function(function, argc)?;
|
||||||
return Err(CompilationError {
|
}
|
||||||
|
for variable in expression.variables() {
|
||||||
|
resolver.resolve_variable(variable, &recipe.parameters)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_function(&self, function: &Token, argc: usize) -> CompilationResult<'a, ()> {
|
||||||
|
resolve_function(function, argc).map_err(|error| CompilationError {
|
||||||
index: error.index,
|
index: error.index,
|
||||||
line: error.line,
|
line: error.line,
|
||||||
column: error.column,
|
column: error.column,
|
||||||
width: error.width,
|
width: error.width,
|
||||||
kind: UnknownFunction {
|
kind: UnknownFunction {
|
||||||
function: &text[error.index..error.index + error.width.unwrap()],
|
function: &self.text[error.index..error.index + error.width.unwrap()],
|
||||||
},
|
},
|
||||||
text,
|
text: self.text,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
for variable in expression.variables() {
|
fn resolve_variable(
|
||||||
|
&self,
|
||||||
|
variable: &Token,
|
||||||
|
parameters: &[Parameter],
|
||||||
|
) -> CompilationResult<'a, ()> {
|
||||||
let name = variable.lexeme;
|
let name = variable.lexeme;
|
||||||
let undefined = !assignments.contains_key(name)
|
let undefined =
|
||||||
&& !recipe.parameters.iter().any(|p| p.name == name);
|
!self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name == name);
|
||||||
if undefined {
|
if undefined {
|
||||||
let error = variable.error(UndefinedVariable { variable: name });
|
let error = variable.error(UndefinedVariable { variable: name });
|
||||||
return Err(CompilationError {
|
return Err(CompilationError {
|
||||||
@ -67,16 +99,11 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
|
|||||||
column: error.column,
|
column: error.column,
|
||||||
width: error.width,
|
width: error.width,
|
||||||
kind: UndefinedVariable {
|
kind: UndefinedVariable {
|
||||||
variable: &text[error.index..error.index + error.width.unwrap()],
|
variable: &self.text[error.index..error.index + error.width.unwrap()],
|
||||||
},
|
},
|
||||||
text,
|
text: self.text,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -186,4 +213,24 @@ mod test {
|
|||||||
width: Some(3),
|
width: Some(3),
|
||||||
kind: UnknownFunction{function: "bar"},
|
kind: UnknownFunction{function: "bar"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compilation_error_test! {
|
||||||
|
name: unknown_function_in_default,
|
||||||
|
input: "a f=baz():",
|
||||||
|
index: 4,
|
||||||
|
line: 0,
|
||||||
|
column: 4,
|
||||||
|
width: Some(3),
|
||||||
|
kind: UnknownFunction{function: "baz"},
|
||||||
|
}
|
||||||
|
|
||||||
|
compilation_error_test! {
|
||||||
|
name: unknown_variable_in_default,
|
||||||
|
input: "a f=foo:",
|
||||||
|
index: 4,
|
||||||
|
line: 0,
|
||||||
|
column: 4,
|
||||||
|
width: Some(3),
|
||||||
|
kind: UndefinedVariable{variable: "foo"},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ use std::{
|
|||||||
path::Path,
|
path::Path,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{expression, fragment, justfile::Justfile, parser::Parser, recipe};
|
use crate::{expression, fragment, justfile::Justfile, parameter, parser::Parser, recipe};
|
||||||
|
|
||||||
pub fn summary(path: impl AsRef<Path>) -> Result<Result<Summary, String>, io::Error> {
|
pub fn summary(path: impl AsRef<Path>) -> Result<Result<Summary, String>, io::Error> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
@ -46,7 +46,7 @@ impl Summary {
|
|||||||
for alias in justfile.aliases.values() {
|
for alias in justfile.aliases.values() {
|
||||||
aliases
|
aliases
|
||||||
.entry(alias.target)
|
.entry(alias.target)
|
||||||
.or_insert(Vec::new())
|
.or_insert_with(Vec::new)
|
||||||
.push(alias.name.to_string());
|
.push(alias.name.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ impl Summary {
|
|||||||
.map(|(name, recipe)| {
|
.map(|(name, recipe)| {
|
||||||
(
|
(
|
||||||
name.to_string(),
|
name.to_string(),
|
||||||
Recipe::new(recipe, aliases.remove(name).unwrap_or(Vec::new())),
|
Recipe::new(recipe, aliases.remove(name).unwrap_or_default()),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
@ -83,6 +83,7 @@ pub struct Recipe {
|
|||||||
pub private: bool,
|
pub private: bool,
|
||||||
pub quiet: bool,
|
pub quiet: bool,
|
||||||
pub shebang: bool,
|
pub shebang: bool,
|
||||||
|
pub parameters: Vec<Parameter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Recipe {
|
impl Recipe {
|
||||||
@ -93,11 +94,31 @@ impl Recipe {
|
|||||||
quiet: recipe.quiet,
|
quiet: recipe.quiet,
|
||||||
dependencies: recipe.dependencies.into_iter().map(str::to_owned).collect(),
|
dependencies: recipe.dependencies.into_iter().map(str::to_owned).collect(),
|
||||||
lines: recipe.lines.into_iter().map(Line::new).collect(),
|
lines: recipe.lines.into_iter().map(Line::new).collect(),
|
||||||
|
parameters: recipe.parameters.into_iter().map(Parameter::new).collect(),
|
||||||
aliases,
|
aliases,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
||||||
|
pub struct Parameter {
|
||||||
|
pub variadic: bool,
|
||||||
|
pub name: String,
|
||||||
|
pub default: Option<Expression>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parameter {
|
||||||
|
fn new(parameter: parameter::Parameter) -> Parameter {
|
||||||
|
Parameter {
|
||||||
|
variadic: parameter.variadic,
|
||||||
|
name: parameter.name.to_owned(),
|
||||||
|
default: parameter
|
||||||
|
.default
|
||||||
|
.map(|expression| Expression::new(expression)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
||||||
pub struct Line {
|
pub struct Line {
|
||||||
pub fragments: Vec<Fragment>,
|
pub fragments: Vec<Fragment>,
|
||||||
@ -184,11 +205,12 @@ impl Expression {
|
|||||||
rhs: Box::new(Expression::new(*rhs)),
|
rhs: Box::new(Expression::new(*rhs)),
|
||||||
},
|
},
|
||||||
String { cooked_string } => Expression::String {
|
String { cooked_string } => Expression::String {
|
||||||
text: cooked_string.cooked,
|
text: cooked_string.cooked.to_string(),
|
||||||
},
|
},
|
||||||
Variable { name, .. } => Expression::Variable {
|
Variable { name, .. } => Expression::Variable {
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
},
|
},
|
||||||
|
Group { expression } => Expression::new(*expression),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
use executable_path::executable_path;
|
use executable_path::executable_path;
|
||||||
use libc::{EXIT_FAILURE, EXIT_SUCCESS};
|
use libc::{EXIT_FAILURE, EXIT_SUCCESS};
|
||||||
use std::env;
|
use std::{env, fs, process, str};
|
||||||
use std::process;
|
|
||||||
use std::str;
|
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
|
|
||||||
/// Instantiate integration tests for a given test case using
|
/// Instantiate integration tests for a given test case using
|
||||||
@ -93,6 +91,46 @@ fn integration_test(
|
|||||||
if failure {
|
if failure {
|
||||||
panic!("test failed");
|
panic!("test failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if expected_status == EXIT_SUCCESS {
|
||||||
|
println!("Reparsing...");
|
||||||
|
|
||||||
|
let output = process::Command::new(&executable_path("just"))
|
||||||
|
.current_dir(tmp.path())
|
||||||
|
.arg("--dump")
|
||||||
|
.output()
|
||||||
|
.expect("just invocation failed");
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
panic!("dump failed: {}", output.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dumped = String::from_utf8(output.stdout).unwrap();
|
||||||
|
|
||||||
|
let reparsed_path = tmp.path().join("reparsed.just");
|
||||||
|
|
||||||
|
fs::write(&reparsed_path, &dumped).unwrap();
|
||||||
|
|
||||||
|
let output = process::Command::new(&executable_path("just"))
|
||||||
|
.current_dir(tmp.path())
|
||||||
|
.arg("--justfile")
|
||||||
|
.arg(&reparsed_path)
|
||||||
|
.arg("--dump")
|
||||||
|
.output()
|
||||||
|
.expect("just invocation failed");
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
panic!("reparse failed: {}", output.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
let reparsed = String::from_utf8(output.stdout).unwrap();
|
||||||
|
|
||||||
|
if reparsed != dumped {
|
||||||
|
print!("expected:\n{}", reparsed);
|
||||||
|
print!("got:\n{}", dumped);
|
||||||
|
assert_eq!(reparsed, dumped);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
integration_test! {
|
integration_test! {
|
||||||
@ -1115,10 +1153,10 @@ a Z="\t z":
|
|||||||
_private-recipe:
|
_private-recipe:
|
||||||
"#,
|
"#,
|
||||||
args: ("--list"),
|
args: ("--list"),
|
||||||
stdout: r"Available recipes:
|
stdout: r#"Available recipes:
|
||||||
a Z='\t z'
|
a Z="\t z"
|
||||||
hello a b='B\t' c='C' # this does a thing
|
hello a b='B ' c='C' # this does a thing
|
||||||
",
|
"#,
|
||||||
stderr: "",
|
stderr: "",
|
||||||
status: EXIT_SUCCESS,
|
status: EXIT_SUCCESS,
|
||||||
}
|
}
|
||||||
@ -1138,10 +1176,10 @@ a Z="\t z":
|
|||||||
_private-recipe:
|
_private-recipe:
|
||||||
"#,
|
"#,
|
||||||
args: ("--list"),
|
args: ("--list"),
|
||||||
stdout: r"Available recipes:
|
stdout: r#"Available recipes:
|
||||||
a Z='\t z' # something else
|
a Z="\t z" # something else
|
||||||
hello a b='B\t' c='C' # this does a thing
|
hello a b='B ' c='C' # this does a thing
|
||||||
",
|
"#,
|
||||||
stderr: "",
|
stderr: "",
|
||||||
status: EXIT_SUCCESS,
|
status: EXIT_SUCCESS,
|
||||||
}
|
}
|
||||||
@ -1165,11 +1203,11 @@ this-recipe-is-very-very-very-important Z="\t z":
|
|||||||
_private-recipe:
|
_private-recipe:
|
||||||
"#,
|
"#,
|
||||||
args: ("--list"),
|
args: ("--list"),
|
||||||
stdout: r"Available recipes:
|
stdout: r#"Available recipes:
|
||||||
hello a b='B\t' c='C' # this does a thing
|
hello a b='B ' c='C' # this does a thing
|
||||||
this-recipe-is-very-very-very-important Z='\t z' # something else
|
this-recipe-is-very-very-very-important Z="\t z" # something else
|
||||||
x a b='B\t' c='C' # this does another thing
|
x a b='B ' c='C' # this does another thing
|
||||||
",
|
"#,
|
||||||
stderr: "",
|
stderr: "",
|
||||||
status: EXIT_SUCCESS,
|
status: EXIT_SUCCESS,
|
||||||
}
|
}
|
||||||
@ -1386,8 +1424,6 @@ b
|
|||||||
|
|
||||||
|
|
||||||
c
|
c
|
||||||
|
|
||||||
|
|
||||||
",
|
",
|
||||||
stderr: "",
|
stderr: "",
|
||||||
status: EXIT_SUCCESS,
|
status: EXIT_SUCCESS,
|
||||||
@ -1809,8 +1845,8 @@ a B C +D='hello':
|
|||||||
args: ("--color", "always", "--list"),
|
args: ("--color", "always", "--list"),
|
||||||
stdout: "Available recipes:\n a \
|
stdout: "Available recipes:\n a \
|
||||||
\u{1b}[36mB\u{1b}[0m \u{1b}[36mC\u{1b}[0m \u{1b}[35m+\
|
\u{1b}[36mB\u{1b}[0m \u{1b}[36mC\u{1b}[0m \u{1b}[35m+\
|
||||||
\u{1b}[0m\u{1b}[36mD\u{1b}[0m=\'\u{1b}[32mhello\u{1b}[0m\
|
\u{1b}[0m\u{1b}[36mD\u{1b}[0m=\u{1b}[32m'hello'\u{1b}[0m \
|
||||||
\' \u{1b}[34m#\u{1b}[0m \u{1b}[34mcomment\u{1b}[0m\n",
|
\u{1b}[34m#\u{1b}[0m \u{1b}[34mcomment\u{1b}[0m\n",
|
||||||
stderr: "",
|
stderr: "",
|
||||||
status: EXIT_SUCCESS,
|
status: EXIT_SUCCESS,
|
||||||
}
|
}
|
||||||
@ -1924,3 +1960,94 @@ X = "\'"
|
|||||||
"#,
|
"#,
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
integration_test! {
|
||||||
|
name: unknown_variable_in_default,
|
||||||
|
justfile: "
|
||||||
|
foo x=bar:
|
||||||
|
",
|
||||||
|
args: (),
|
||||||
|
stdout: "",
|
||||||
|
stderr: r#"error: Variable `bar` not defined
|
||||||
|
|
|
||||||
|
2 | foo x=bar:
|
||||||
|
| ^^^
|
||||||
|
"#,
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
integration_test! {
|
||||||
|
name: unknown_function_in_default,
|
||||||
|
justfile: "
|
||||||
|
foo x=bar():
|
||||||
|
",
|
||||||
|
args: (),
|
||||||
|
stdout: "",
|
||||||
|
stderr: r#"error: Call to unknown function `bar`
|
||||||
|
|
|
||||||
|
2 | foo x=bar():
|
||||||
|
| ^^^
|
||||||
|
"#,
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
integration_test! {
|
||||||
|
name: default_string,
|
||||||
|
justfile: "
|
||||||
|
foo x='bar':
|
||||||
|
echo {{x}}
|
||||||
|
",
|
||||||
|
args: (),
|
||||||
|
stdout: "bar\n",
|
||||||
|
stderr: "echo bar\n",
|
||||||
|
status: EXIT_SUCCESS,
|
||||||
|
}
|
||||||
|
|
||||||
|
integration_test! {
|
||||||
|
name: default_concatination,
|
||||||
|
justfile: "
|
||||||
|
foo x=(`echo foo` + 'bar'):
|
||||||
|
echo {{x}}
|
||||||
|
",
|
||||||
|
args: (),
|
||||||
|
stdout: "foobar\n",
|
||||||
|
stderr: "echo foobar\n",
|
||||||
|
status: EXIT_SUCCESS,
|
||||||
|
}
|
||||||
|
|
||||||
|
integration_test! {
|
||||||
|
name: default_backtick,
|
||||||
|
justfile: "
|
||||||
|
foo x=`echo foo`:
|
||||||
|
echo {{x}}
|
||||||
|
",
|
||||||
|
args: (),
|
||||||
|
stdout: "foo\n",
|
||||||
|
stderr: "echo foo\n",
|
||||||
|
status: EXIT_SUCCESS,
|
||||||
|
}
|
||||||
|
|
||||||
|
integration_test! {
|
||||||
|
name: default_variable,
|
||||||
|
justfile: "
|
||||||
|
y = 'foo'
|
||||||
|
foo x=y:
|
||||||
|
echo {{x}}
|
||||||
|
",
|
||||||
|
args: (),
|
||||||
|
stdout: "foo\n",
|
||||||
|
stderr: "echo foo\n",
|
||||||
|
status: EXIT_SUCCESS,
|
||||||
|
}
|
||||||
|
|
||||||
|
integration_test! {
|
||||||
|
name: test_os_arch_functions_in_default,
|
||||||
|
justfile: r#"
|
||||||
|
foo a=arch() o=os() f=os_family():
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
use executable_path::executable_path;
|
use executable_path::executable_path;
|
||||||
use std::{
|
use std::{
|
||||||
process::Command,
|
process::Command,
|
||||||
thread,
|
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
@ -33,7 +32,7 @@ fn interrupt_test(justfile: &str) {
|
|||||||
.spawn()
|
.spawn()
|
||||||
.expect("just invocation failed");
|
.expect("just invocation failed");
|
||||||
|
|
||||||
thread::sleep(Duration::new(1, 0));
|
while start.elapsed() < Duration::from_millis(500) {}
|
||||||
|
|
||||||
kill(child.id());
|
kill(child.id());
|
||||||
|
|
||||||
@ -41,11 +40,11 @@ fn interrupt_test(justfile: &str) {
|
|||||||
|
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
|
|
||||||
if elapsed > Duration::new(4, 0) {
|
if elapsed > Duration::from_secs(2) {
|
||||||
panic!("process returned too late: {:?}", elapsed);
|
panic!("process returned too late: {:?}", elapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
if elapsed < Duration::new(1, 0) {
|
if elapsed < Duration::from_millis(100) {
|
||||||
panic!("process returned too early : {:?}", elapsed);
|
panic!("process returned too early : {:?}", elapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +58,7 @@ fn interrupt_shebang() {
|
|||||||
"
|
"
|
||||||
default:
|
default:
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
sleep 2
|
sleep 1
|
||||||
",
|
",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -70,7 +69,7 @@ fn interrupt_line() {
|
|||||||
interrupt_test(
|
interrupt_test(
|
||||||
"
|
"
|
||||||
default:
|
default:
|
||||||
@sleep 2
|
@sleep 1
|
||||||
",
|
",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -80,7 +79,7 @@ default:
|
|||||||
fn interrupt_backtick() {
|
fn interrupt_backtick() {
|
||||||
interrupt_test(
|
interrupt_test(
|
||||||
"
|
"
|
||||||
foo = `sleep 2`
|
foo = `sleep 1`
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@echo hello
|
@echo hello
|
||||||
|
Loading…
Reference in New Issue
Block a user