Add variadic parameters (#127)
Recipes may now have a final variadic parameter: ```make foo bar+: @echo {{bar}} ``` Variadic parameters accept one or more arguments, and expand to a string containing those arguments separated by spaces: ```sh $ just foo a b c d e a b c d e ``` I elected to accept one or more arguments instead of zero or more arguments since unexpectedly empty arguments can sometimes be dangerous. ```make clean dir: rm -rf {{dir}}/bin ``` If `dir` is empty in the above recipe, you'll delete `/bin`, which is probably not what was intended.
This commit is contained in:
parent
9ece0b9a6b
commit
1ac5b4ea42
@ -4,7 +4,7 @@ justfile grammar
|
|||||||
Justfiles are processed by a mildly context-sensitive tokenizer
|
Justfiles are processed by a mildly context-sensitive tokenizer
|
||||||
and a recursive descent parser. The grammar is mostly LL(1),
|
and a recursive descent parser. The grammar is mostly LL(1),
|
||||||
although an extra token of lookahead is used to distinguish between
|
although an extra token of lookahead is used to distinguish between
|
||||||
export assignments and recipes with arguments.
|
export assignments and recipes with parameters.
|
||||||
|
|
||||||
tokens
|
tokens
|
||||||
------
|
------
|
||||||
@ -51,9 +51,9 @@ expression : STRING
|
|||||||
| BACKTICK
|
| BACKTICK
|
||||||
| expression '+' expression
|
| expression '+' expression
|
||||||
|
|
||||||
recipe : '@'? NAME argument* ':' dependencies? body?
|
recipe : '@'? NAME parameter* ('+' parameter)? ':' dependencies? body?
|
||||||
|
|
||||||
argument : NAME
|
parameter : NAME
|
||||||
| NAME '=' STRING
|
| NAME '=' STRING
|
||||||
| NAME '=' RAW_STRING
|
| NAME '=' RAW_STRING
|
||||||
|
|
||||||
|
16
README.md
16
README.md
@ -264,6 +264,22 @@ Testing server:unit...
|
|||||||
./test --tests unit server
|
./test --tests unit server
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The last parameter to a recipe may be variadic, indicated with a `+` before the argument name:
|
||||||
|
|
||||||
|
```make
|
||||||
|
backup +FILES:
|
||||||
|
scp {{FILES}} me@server.com:
|
||||||
|
```
|
||||||
|
|
||||||
|
Variadic parameters accept one or more arguments and expand to a string containing those arguments separated by spaces:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ just backup FAQ.md GRAMMAR.md
|
||||||
|
scp FAQ.md GRAMMAR.md me@server.com:
|
||||||
|
FAQ.md 100% 1831 1.8KB/s 00:00
|
||||||
|
GRAMMAR.md 100% 1666 1.6KB/s 00:00
|
||||||
|
```
|
||||||
|
|
||||||
Variables can be exported to recipes as environment variables:
|
Variables can be exported to recipes as environment variables:
|
||||||
|
|
||||||
```make
|
```make
|
||||||
|
@ -1038,16 +1038,16 @@ Recipe `recipe` failed on line 3 with exit code 100\u{1b}[0m\n",
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dump() {
|
fn dump() {
|
||||||
let text ="
|
let text = r#"
|
||||||
# this recipe does something
|
# this recipe does something
|
||||||
recipe:
|
recipe a b +d:
|
||||||
@exit 100";
|
@exit 100"#;
|
||||||
integration_test(
|
integration_test(
|
||||||
&["--dump"],
|
&["--dump"],
|
||||||
text,
|
text,
|
||||||
0,
|
0,
|
||||||
"# this recipe does something
|
"# this recipe does something
|
||||||
recipe:
|
recipe a b +d:
|
||||||
@exit 100
|
@exit 100
|
||||||
",
|
",
|
||||||
"",
|
"",
|
||||||
@ -1481,3 +1481,59 @@ a b=":
|
|||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variadic_recipe() {
|
||||||
|
integration_test(
|
||||||
|
&["a", "0", "1", "2", "3", " 4 "],
|
||||||
|
"
|
||||||
|
a x y +z:
|
||||||
|
echo {{x}} {{y}} {{z}}
|
||||||
|
",
|
||||||
|
0,
|
||||||
|
"0 1 2 3 4\n",
|
||||||
|
"echo 0 1 2 3 4 \n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variadic_ignore_default() {
|
||||||
|
integration_test(
|
||||||
|
&["a", "0", "1", "2", "3", " 4 "],
|
||||||
|
"
|
||||||
|
a x y +z='HELLO':
|
||||||
|
echo {{x}} {{y}} {{z}}
|
||||||
|
",
|
||||||
|
0,
|
||||||
|
"0 1 2 3 4\n",
|
||||||
|
"echo 0 1 2 3 4 \n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variadic_use_default() {
|
||||||
|
integration_test(
|
||||||
|
&["a", "0", "1"],
|
||||||
|
"
|
||||||
|
a x y +z='HELLO':
|
||||||
|
echo {{x}} {{y}} {{z}}
|
||||||
|
",
|
||||||
|
0,
|
||||||
|
"0 1 HELLO\n",
|
||||||
|
"echo 0 1 HELLO\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variadic_too_few() {
|
||||||
|
integration_test(
|
||||||
|
&["a", "0", "1"],
|
||||||
|
"
|
||||||
|
a x y +z:
|
||||||
|
echo {{x}} {{y}} {{z}}
|
||||||
|
",
|
||||||
|
255,
|
||||||
|
"",
|
||||||
|
"error: Recipe `a` got 2 arguments but takes at least 3\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
100
src/lib.rs
100
src/lib.rs
@ -19,6 +19,7 @@ pub use app::app;
|
|||||||
|
|
||||||
use app::UseColor;
|
use app::UseColor;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::collections::{BTreeMap as Map, BTreeSet as Set};
|
use std::collections::{BTreeMap as Map, BTreeSet as Set};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
@ -68,28 +69,33 @@ fn contains<T: PartialOrd>(range: &Range<T>, i: T) -> bool {
|
|||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
struct Recipe<'a> {
|
struct Recipe<'a> {
|
||||||
line_number: usize,
|
|
||||||
name: &'a str,
|
|
||||||
doc: Option<&'a str>,
|
|
||||||
lines: Vec<Vec<Fragment<'a>>>,
|
|
||||||
dependencies: Vec<&'a str>,
|
dependencies: Vec<&'a str>,
|
||||||
dependency_tokens: Vec<Token<'a>>,
|
dependency_tokens: Vec<Token<'a>>,
|
||||||
|
doc: Option<&'a str>,
|
||||||
|
line_number: usize,
|
||||||
|
lines: Vec<Vec<Fragment<'a>>>,
|
||||||
|
name: &'a str,
|
||||||
parameters: Vec<Parameter<'a>>,
|
parameters: Vec<Parameter<'a>>,
|
||||||
shebang: bool,
|
|
||||||
quiet: bool,
|
quiet: bool,
|
||||||
|
shebang: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
struct Parameter<'a> {
|
struct Parameter<'a> {
|
||||||
name: &'a str,
|
|
||||||
default: Option<String>,
|
default: Option<String>,
|
||||||
|
name: &'a str,
|
||||||
token: Token<'a>,
|
token: Token<'a>,
|
||||||
|
variadic: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Display for Parameter<'a> {
|
impl<'a> Display for Parameter<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
let green = maybe_green(f.alternate());
|
let green = maybe_green(f.alternate());
|
||||||
let cyan = maybe_cyan(f.alternate());
|
let cyan = maybe_cyan(f.alternate());
|
||||||
|
let purple = maybe_purple(f.alternate());
|
||||||
|
if self.variadic {
|
||||||
|
write!(f, "{}", purple.paint("+"))?;
|
||||||
|
}
|
||||||
write!(f, "{}", cyan.paint(self.name))?;
|
write!(f, "{}", cyan.paint(self.name))?;
|
||||||
if let Some(ref default) = self.default {
|
if let Some(ref default) = self.default {
|
||||||
let escaped = default.chars().flat_map(char::escape_default).collect::<String>();;
|
let escaped = default.chars().flat_map(char::escape_default).collect::<String>();;
|
||||||
@ -275,8 +281,12 @@ impl<'a> Recipe<'a> {
|
|||||||
fn argument_range(&self) -> Range<usize> {
|
fn argument_range(&self) -> Range<usize> {
|
||||||
self.parameters.iter().filter(|p| !p.default.is_some()).count()
|
self.parameters.iter().filter(|p| !p.default.is_some()).count()
|
||||||
..
|
..
|
||||||
|
if self.parameters.iter().any(|p| p.variadic) {
|
||||||
|
std::usize::MAX
|
||||||
|
} else {
|
||||||
self.parameters.len() + 1
|
self.parameters.len() + 1
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
&self,
|
&self,
|
||||||
@ -290,16 +300,30 @@ impl<'a> Recipe<'a> {
|
|||||||
warn!("{}===> Running recipe `{}`...{}", cyan.prefix(), self.name, cyan.suffix());
|
warn!("{}===> Running recipe `{}`...{}", cyan.prefix(), self.name, cyan.suffix());
|
||||||
}
|
}
|
||||||
|
|
||||||
let argument_map = self.parameters.iter().enumerate()
|
let mut argument_map = Map::new();
|
||||||
.map(|(i, parameter)| if i < arguments.len() {
|
|
||||||
Ok((parameter.name, arguments[i]))
|
let mut rest = arguments;
|
||||||
} else if let Some(ref default) = parameter.default {
|
for parameter in &self.parameters {
|
||||||
Ok((parameter.name, default.as_str()))
|
let value = if rest.is_empty() {
|
||||||
} else {
|
match parameter.default {
|
||||||
Err(RunError::InternalError{
|
Some(ref default) => Cow::Borrowed(default.as_str()),
|
||||||
|
None => return Err(RunError::InternalError{
|
||||||
message: "missing parameter without default".to_string()
|
message: "missing parameter without default".to_string()
|
||||||
})
|
}),
|
||||||
}).collect::<Result<Vec<_>, _>>()?.into_iter().collect();
|
}
|
||||||
|
} else {
|
||||||
|
if parameter.variadic {
|
||||||
|
let value = Cow::Owned(rest.to_vec().join(" "));
|
||||||
|
rest = &[];
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
let value = Cow::Borrowed(rest[0]);
|
||||||
|
rest = &rest[1..];
|
||||||
|
value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
argument_map.insert(parameter.name, value);
|
||||||
|
}
|
||||||
|
|
||||||
let mut evaluator = Evaluator {
|
let mut evaluator = Evaluator {
|
||||||
evaluated: empty(),
|
evaluated: empty(),
|
||||||
@ -689,7 +713,7 @@ impl<'a, 'b> Evaluator<'a, 'b> {
|
|||||||
fn evaluate_line(
|
fn evaluate_line(
|
||||||
&mut self,
|
&mut self,
|
||||||
line: &[Fragment<'a>],
|
line: &[Fragment<'a>],
|
||||||
arguments: &Map<&str, &str>
|
arguments: &Map<&str, Cow<str>>
|
||||||
) -> Result<String, RunError<'a>> {
|
) -> Result<String, RunError<'a>> {
|
||||||
let mut evaluated = String::new();
|
let mut evaluated = String::new();
|
||||||
for fragment in line {
|
for fragment in line {
|
||||||
@ -727,7 +751,7 @@ impl<'a, 'b> Evaluator<'a, 'b> {
|
|||||||
fn evaluate_expression(
|
fn evaluate_expression(
|
||||||
&mut self,
|
&mut self,
|
||||||
expression: &Expression<'a>,
|
expression: &Expression<'a>,
|
||||||
arguments: &Map<&str, &str>
|
arguments: &Map<&str, Cow<str>>
|
||||||
) -> Result<String, RunError<'a>> {
|
) -> Result<String, RunError<'a>> {
|
||||||
Ok(match *expression {
|
Ok(match *expression {
|
||||||
Expression::Variable{name, ..} => {
|
Expression::Variable{name, ..} => {
|
||||||
@ -786,6 +810,7 @@ enum ErrorKind<'a> {
|
|||||||
OuterShebang,
|
OuterShebang,
|
||||||
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},
|
||||||
@ -1002,6 +1027,14 @@ fn maybe_cyan(colors: bool) -> ansi_term::Style {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn maybe_purple(colors: bool) -> ansi_term::Style {
|
||||||
|
if colors {
|
||||||
|
ansi_term::Style::new().fg(ansi_term::Color::Purple)
|
||||||
|
} else {
|
||||||
|
ansi_term::Style::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn maybe_bold(colors: bool) -> ansi_term::Style {
|
fn maybe_bold(colors: bool) -> ansi_term::Style {
|
||||||
if colors {
|
if colors {
|
||||||
ansi_term::Style::new().bold()
|
ansi_term::Style::new().bold()
|
||||||
@ -1066,6 +1099,9 @@ impl<'a> Display for CompileError<'a> {
|
|||||||
RequiredParameterFollowsDefaultParameter{parameter} => {
|
RequiredParameterFollowsDefaultParameter{parameter} => {
|
||||||
writeln!(f, "non-default parameter `{}` follows default parameter", parameter)?;
|
writeln!(f, "non-default parameter `{}` follows default parameter", parameter)?;
|
||||||
}
|
}
|
||||||
|
ParameterFollowsVariadicParameter{parameter} => {
|
||||||
|
writeln!(f, "parameter `{}` follows a varidic parameter", parameter)?;
|
||||||
|
}
|
||||||
MixedLeadingWhitespace{whitespace} => {
|
MixedLeadingWhitespace{whitespace} => {
|
||||||
writeln!(f,
|
writeln!(f,
|
||||||
"found a mix of tabs and spaces in leading whitespace: `{}`\n\
|
"found a mix of tabs and spaces in leading whitespace: `{}`\n\
|
||||||
@ -1846,8 +1882,28 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut parsed_parameter_with_default = false;
|
let mut parsed_parameter_with_default = false;
|
||||||
|
let mut parsed_variadic_parameter = false;
|
||||||
let mut parameters: Vec<Parameter> = vec![];
|
let mut parameters: Vec<Parameter> = vec![];
|
||||||
while let Some(parameter) = self.accept(Name) {
|
loop {
|
||||||
|
let plus = self.accept(Plus);
|
||||||
|
|
||||||
|
let parameter = match self.accept(Name) {
|
||||||
|
Some(parameter) => parameter,
|
||||||
|
None => if let Some(plus) = plus {
|
||||||
|
return Err(self.unexpected_token(&plus, &[Name]));
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let variadic = plus.is_some();
|
||||||
|
|
||||||
|
if parsed_variadic_parameter {
|
||||||
|
return Err(parameter.error(ErrorKind::ParameterFollowsVariadicParameter {
|
||||||
|
parameter: parameter.lexeme,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
if parameters.iter().any(|p| p.name == parameter.lexeme) {
|
if parameters.iter().any(|p| p.name == parameter.lexeme) {
|
||||||
return Err(parameter.error(ErrorKind::DuplicateParameter {
|
return Err(parameter.error(ErrorKind::DuplicateParameter {
|
||||||
recipe: name.lexeme, parameter: parameter.lexeme
|
recipe: name.lexeme, parameter: parameter.lexeme
|
||||||
@ -1873,11 +1929,13 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parsed_parameter_with_default |= default.is_some();
|
parsed_parameter_with_default |= default.is_some();
|
||||||
|
parsed_variadic_parameter = variadic;
|
||||||
|
|
||||||
parameters.push(Parameter {
|
parameters.push(Parameter {
|
||||||
name: parameter.lexeme,
|
|
||||||
default: default,
|
default: default,
|
||||||
|
name: parameter.lexeme,
|
||||||
token: parameter,
|
token: parameter,
|
||||||
|
variadic: variadic,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1885,9 +1943,9 @@ impl<'a> Parser<'a> {
|
|||||||
// if we haven't accepted any parameters, an equals
|
// if we haven't accepted any parameters, an equals
|
||||||
// would have been fine as part of an assignment
|
// would have been fine as part of an assignment
|
||||||
if parameters.is_empty() {
|
if parameters.is_empty() {
|
||||||
return Err(self.unexpected_token(&token, &[Name, Colon, Equals]));
|
return Err(self.unexpected_token(&token, &[Name, Plus, Colon, Equals]));
|
||||||
} else {
|
} else {
|
||||||
return Err(self.unexpected_token(&token, &[Name, Colon]));
|
return Err(self.unexpected_token(&token, &[Name, Plus, Colon]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
61
src/unit.rs
61
src/unit.rs
@ -277,6 +277,26 @@ foo a="b\t":
|
|||||||
"#, r#"foo a='b\t':"#);
|
"#, r#"foo a='b\t':"#);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_variadic() {
|
||||||
|
parse_summary(r#"
|
||||||
|
|
||||||
|
foo +a:
|
||||||
|
|
||||||
|
|
||||||
|
"#, r#"foo +a:"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_variadic_string_default() {
|
||||||
|
parse_summary(r#"
|
||||||
|
|
||||||
|
foo +a="Hello":
|
||||||
|
|
||||||
|
|
||||||
|
"#, r#"foo +a='Hello':"#);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_raw_string_default() {
|
fn parse_raw_string_default() {
|
||||||
parse_summary(r#"
|
parse_summary(r#"
|
||||||
@ -400,7 +420,7 @@ fn missing_colon() {
|
|||||||
line: 0,
|
line: 0,
|
||||||
column: 5,
|
column: 5,
|
||||||
width: Some(1),
|
width: Some(1),
|
||||||
kind: ErrorKind::UnexpectedToken{expected: vec![Name, Colon], found: Eol},
|
kind: ErrorKind::UnexpectedToken{expected: vec![Name, Plus, Colon], found: Eol},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -456,6 +476,19 @@ fn missing_default_backtick() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parameter_after_variadic() {
|
||||||
|
let text = "foo +a bbb:";
|
||||||
|
parse_error(text, CompileError {
|
||||||
|
text: text,
|
||||||
|
index: 7,
|
||||||
|
line: 0,
|
||||||
|
column: 7,
|
||||||
|
width: Some(3),
|
||||||
|
kind: ErrorKind::ParameterFollowsVariadicParameter{parameter: "bbb"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn required_after_default() {
|
fn required_after_default() {
|
||||||
let text = "hello arg='foo' bar:";
|
let text = "hello arg='foo' bar:";
|
||||||
@ -825,6 +858,19 @@ fn unknown_second_interpolation_variable() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plus_following_parameter() {
|
||||||
|
let text = "a b c+:";
|
||||||
|
parse_error(text, CompileError {
|
||||||
|
text: text,
|
||||||
|
index: 5,
|
||||||
|
line: 0,
|
||||||
|
column: 5,
|
||||||
|
width: Some(1),
|
||||||
|
kind: ErrorKind::UnexpectedToken{expected: vec![Name], found: Plus},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tokenize_order() {
|
fn tokenize_order() {
|
||||||
let text = r"
|
let text = r"
|
||||||
@ -913,6 +959,19 @@ fn missing_some_arguments() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_some_arguments_variadic() {
|
||||||
|
match parse_success("a b c +d:").run(&["a", "B", "C"], &Default::default()).unwrap_err() {
|
||||||
|
RunError::ArgumentCountMismatch{recipe, found, min, max} => {
|
||||||
|
assert_eq!(recipe, "a");
|
||||||
|
assert_eq!(found, 2);
|
||||||
|
assert_eq!(min, 3);
|
||||||
|
assert_eq!(max, super::std::usize::MAX - 1);
|
||||||
|
},
|
||||||
|
other => panic!("expected an code run error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_all_arguments() {
|
fn missing_all_arguments() {
|
||||||
match parse_success("a b c d:\n echo {{b}}{{c}}{{d}}")
|
match parse_success("a b c d:\n echo {{b}}{{c}}{{d}}")
|
||||||
|
Loading…
Reference in New Issue
Block a user