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
|
||||
and a recursive descent parser. The grammar is mostly LL(1),
|
||||
although an extra token of lookahead is used to distinguish between
|
||||
export assignments and recipes with arguments.
|
||||
export assignments and recipes with parameters.
|
||||
|
||||
tokens
|
||||
------
|
||||
@ -51,9 +51,9 @@ expression : STRING
|
||||
| BACKTICK
|
||||
| expression '+' expression
|
||||
|
||||
recipe : '@'? NAME argument* ':' dependencies? body?
|
||||
recipe : '@'? NAME parameter* ('+' parameter)? ':' dependencies? body?
|
||||
|
||||
argument : NAME
|
||||
parameter : NAME
|
||||
| NAME '=' STRING
|
||||
| NAME '=' RAW_STRING
|
||||
|
||||
|
16
README.md
16
README.md
@ -264,6 +264,22 @@ Testing server:unit...
|
||||
./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:
|
||||
|
||||
```make
|
||||
|
@ -1038,16 +1038,16 @@ Recipe `recipe` failed on line 3 with exit code 100\u{1b}[0m\n",
|
||||
|
||||
#[test]
|
||||
fn dump() {
|
||||
let text ="
|
||||
let text = r#"
|
||||
# this recipe does something
|
||||
recipe:
|
||||
@exit 100";
|
||||
recipe a b +d:
|
||||
@exit 100"#;
|
||||
integration_test(
|
||||
&["--dump"],
|
||||
text,
|
||||
0,
|
||||
"# this recipe does something
|
||||
recipe:
|
||||
recipe a b +d:
|
||||
@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 regex::Regex;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{BTreeMap as Map, BTreeSet as Set};
|
||||
use std::fmt::Display;
|
||||
use std::io::prelude::*;
|
||||
@ -68,28 +69,33 @@ fn contains<T: PartialOrd>(range: &Range<T>, i: T) -> bool {
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
struct Recipe<'a> {
|
||||
line_number: usize,
|
||||
name: &'a str,
|
||||
doc: Option<&'a str>,
|
||||
lines: Vec<Vec<Fragment<'a>>>,
|
||||
dependencies: Vec<&'a str>,
|
||||
dependency_tokens: Vec<Token<'a>>,
|
||||
doc: Option<&'a str>,
|
||||
line_number: usize,
|
||||
lines: Vec<Vec<Fragment<'a>>>,
|
||||
name: &'a str,
|
||||
parameters: Vec<Parameter<'a>>,
|
||||
shebang: bool,
|
||||
quiet: bool,
|
||||
shebang: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
struct Parameter<'a> {
|
||||
name: &'a str,
|
||||
default: Option<String>,
|
||||
name: &'a str,
|
||||
token: Token<'a>,
|
||||
variadic: bool,
|
||||
}
|
||||
|
||||
impl<'a> Display for Parameter<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
let green = maybe_green(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))?;
|
||||
if let Some(ref default) = self.default {
|
||||
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> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
@ -290,16 +300,30 @@ impl<'a> Recipe<'a> {
|
||||
warn!("{}===> Running recipe `{}`...{}", cyan.prefix(), self.name, cyan.suffix());
|
||||
}
|
||||
|
||||
let argument_map = self.parameters.iter().enumerate()
|
||||
.map(|(i, parameter)| if i < arguments.len() {
|
||||
Ok((parameter.name, arguments[i]))
|
||||
} else if let Some(ref default) = parameter.default {
|
||||
Ok((parameter.name, default.as_str()))
|
||||
} else {
|
||||
Err(RunError::InternalError{
|
||||
let mut argument_map = Map::new();
|
||||
|
||||
let mut rest = arguments;
|
||||
for parameter in &self.parameters {
|
||||
let value = if rest.is_empty() {
|
||||
match parameter.default {
|
||||
Some(ref default) => Cow::Borrowed(default.as_str()),
|
||||
None => return Err(RunError::InternalError{
|
||||
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 {
|
||||
evaluated: empty(),
|
||||
@ -689,7 +713,7 @@ impl<'a, 'b> Evaluator<'a, 'b> {
|
||||
fn evaluate_line(
|
||||
&mut self,
|
||||
line: &[Fragment<'a>],
|
||||
arguments: &Map<&str, &str>
|
||||
arguments: &Map<&str, Cow<str>>
|
||||
) -> Result<String, RunError<'a>> {
|
||||
let mut evaluated = String::new();
|
||||
for fragment in line {
|
||||
@ -727,7 +751,7 @@ impl<'a, 'b> Evaluator<'a, 'b> {
|
||||
fn evaluate_expression(
|
||||
&mut self,
|
||||
expression: &Expression<'a>,
|
||||
arguments: &Map<&str, &str>
|
||||
arguments: &Map<&str, Cow<str>>
|
||||
) -> Result<String, RunError<'a>> {
|
||||
Ok(match *expression {
|
||||
Expression::Variable{name, ..} => {
|
||||
@ -786,6 +810,7 @@ enum ErrorKind<'a> {
|
||||
OuterShebang,
|
||||
ParameterShadowsVariable{parameter: &'a str},
|
||||
RequiredParameterFollowsDefaultParameter{parameter: &'a str},
|
||||
ParameterFollowsVariadicParameter{parameter: &'a str},
|
||||
UndefinedVariable{variable: &'a str},
|
||||
UnexpectedToken{expected: Vec<TokenKind>, found: TokenKind},
|
||||
UnknownDependency{recipe: &'a str, unknown: &'a str},
|
||||
@ -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 {
|
||||
if colors {
|
||||
ansi_term::Style::new().bold()
|
||||
@ -1066,6 +1099,9 @@ impl<'a> Display for CompileError<'a> {
|
||||
RequiredParameterFollowsDefaultParameter{parameter} => {
|
||||
writeln!(f, "non-default parameter `{}` follows default parameter", parameter)?;
|
||||
}
|
||||
ParameterFollowsVariadicParameter{parameter} => {
|
||||
writeln!(f, "parameter `{}` follows a varidic parameter", parameter)?;
|
||||
}
|
||||
MixedLeadingWhitespace{whitespace} => {
|
||||
writeln!(f,
|
||||
"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_variadic_parameter = false;
|
||||
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) {
|
||||
return Err(parameter.error(ErrorKind::DuplicateParameter {
|
||||
recipe: name.lexeme, parameter: parameter.lexeme
|
||||
@ -1873,11 +1929,13 @@ impl<'a> Parser<'a> {
|
||||
}
|
||||
|
||||
parsed_parameter_with_default |= default.is_some();
|
||||
parsed_variadic_parameter = variadic;
|
||||
|
||||
parameters.push(Parameter {
|
||||
name: parameter.lexeme,
|
||||
default: default,
|
||||
name: parameter.lexeme,
|
||||
token: parameter,
|
||||
variadic: variadic,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1885,9 +1943,9 @@ impl<'a> Parser<'a> {
|
||||
// if we haven't accepted any parameters, an equals
|
||||
// would have been fine as part of an assignment
|
||||
if parameters.is_empty() {
|
||||
return Err(self.unexpected_token(&token, &[Name, Colon, Equals]));
|
||||
return Err(self.unexpected_token(&token, &[Name, Plus, Colon, Equals]));
|
||||
} 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':"#);
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn parse_raw_string_default() {
|
||||
parse_summary(r#"
|
||||
@ -400,7 +420,7 @@ fn missing_colon() {
|
||||
line: 0,
|
||||
column: 5,
|
||||
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]
|
||||
fn required_after_default() {
|
||||
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]
|
||||
fn tokenize_order() {
|
||||
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]
|
||||
fn missing_all_arguments() {
|
||||
match parse_success("a b c d:\n echo {{b}}{{c}}{{d}}")
|
||||
|
Loading…
Reference in New Issue
Block a user