Add variadic parameters that accept zero or more arguments (#645)

Add "star" variadic parameters that accept zero or more arguments,
distinguished with a `*` in front of the parameter name.
This commit is contained in:
Richard Berry 2020-06-13 09:49:13 +01:00 committed by GitHub
parent 63f51b5b48
commit 1ff619295c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 182 additions and 33 deletions

View File

@ -73,11 +73,14 @@ string : STRING
sequence : expression ',' sequence
| expression ','?
recipe : '@'? NAME parameter* ('+' parameter)? ':' dependency* body?
recipe : '@'? NAME parameter* variadic? ':' dependency* body?
parameter : NAME
| NAME '=' value
variadic : '*' parameter
| '+' parameter
dependency : NAME
| '(' NAME expression* ')

View File

@ -572,14 +572,14 @@ 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 either a `+` or 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:
Variadic parameters prefixed with `+` accept _one or more_ arguments and expand to a string containing those arguments separated by spaces:
```sh
$ just backup FAQ.md GRAMMAR.md
@ -588,13 +588,20 @@ FAQ.md 100% 1831 1.8KB/s 00:00
GRAMMAR.md 100% 1666 1.6KB/s 00:00
```
A variadic parameter with a default argument will accept zero or more arguments:
Variadic parameters prefixed with `*` accept _zero or more_ arguments and expand to a string containing those arguments separated by spaces, or an empty string if no arguments are present:
```make
commit MESSAGE +FLAGS='':
commit MESSAGE *FLAGS:
git commit {{FLAGS}} -m "{{MESSAGE}}"
```
Variadic parameters prefixed by `+` can be assigned default values. These are overridden by arguments passed on the command line:
```make
test +FLAGS='-q':
cargo test {{FLAGS}}
```
`{{...}}` substitutions may need to be quoted if they contains spaces. For example, if you have the following recipe:
```make

View File

@ -56,11 +56,11 @@ pub(crate) use crate::{
fragment::Fragment, function::Function, function_context::FunctionContext,
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
justfile::Justfile, lexer::Lexer, line::Line, list::List, load_error::LoadError, module::Module,
name::Name, output_error::OutputError, parameter::Parameter, parser::Parser, platform::Platform,
position::Position, positional::Positional, recipe::Recipe, recipe_context::RecipeContext,
recipe_resolver::RecipeResolver, runtime_error::RuntimeError, scope::Scope, search::Search,
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace,
name::Name, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind,
parser::Parser, platform::Platform, position::Position, positional::Positional, recipe::Recipe,
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError,
scope::Scope, search::Search, search_config::SearchConfig, search_error::SearchError, set::Set,
setting::Setting, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace,
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,

View File

@ -192,12 +192,14 @@ impl<'src, 'run> Evaluator<'src, 'run> {
let value = if rest.is_empty() {
if let Some(ref default) = parameter.default {
evaluator.evaluate_expression(default)?
} else if parameter.kind == ParameterKind::Star {
String::new()
} else {
return Err(RuntimeError::Internal {
message: "missing parameter without default".to_string(),
});
}
} else if parameter.variadic {
} else if parameter.kind.is_variadic() {
let value = rest.to_vec().join(" ");
rest = &[];
value

View File

@ -436,6 +436,7 @@ impl<'src> Lexer<'src> {
/// Lex token beginning with `start` outside of a recipe body
fn lex_normal(&mut self, start: char) -> CompilationResult<'src, ()> {
match start {
'*' => self.lex_single(Asterisk),
'@' => self.lex_single(At),
'[' => self.lex_single(BracketL),
']' => self.lex_single(BracketR),
@ -806,6 +807,7 @@ mod tests {
fn default_lexeme(kind: TokenKind) -> &'static str {
match kind {
// Fixed lexemes
Asterisk => "*",
At => "@",
BracketL => "[",
BracketR => "]",

View File

@ -91,6 +91,7 @@ mod ordinal;
mod output;
mod output_error;
mod parameter;
mod parameter_kind;
mod parser;
mod platform;
mod platform_interface;

View File

@ -100,8 +100,8 @@ impl<'src> Node<'src> for UnresolvedRecipe<'src> {
let mut params = Tree::atom("params");
for parameter in &self.parameters {
if parameter.variadic {
params.push_mut("+");
if let Some(prefix) = parameter.kind.prefix() {
params.push_mut(prefix);
}
params.push_mut(parameter.tree());

View File

@ -4,18 +4,18 @@ use crate::common::*;
#[derive(PartialEq, Debug)]
pub(crate) struct Parameter<'src> {
/// The parameter name
pub(crate) name: Name<'src>,
/// Parameter is variadic
pub(crate) variadic: bool,
pub(crate) name: Name<'src>,
/// The kind of parameter
pub(crate) kind: ParameterKind,
/// An optional default expression
pub(crate) default: Option<Expression<'src>>,
pub(crate) default: Option<Expression<'src>>,
}
impl<'src> Display for Parameter<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
let color = Color::fmt(f);
if self.variadic {
write!(f, "{}", color.annotation().paint("+"))?;
if let Some(prefix) = self.kind.prefix() {
write!(f, "{}", color.annotation().paint(prefix))?;
}
write!(f, "{}", color.parameter().paint(self.name.lexeme()))?;
if let Some(ref default) = self.default {

26
src/parameter_kind.rs Normal file
View File

@ -0,0 +1,26 @@
use crate::common::*;
/// Parameters can either be…
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) enum ParameterKind {
/// …singular, accepting a single argument
Singular,
/// …variadic, accepting one or more arguments
Plus,
/// …variadic, accepting zero or more arguments
Star,
}
impl ParameterKind {
pub(crate) fn prefix(self) -> Option<&'static str> {
match self {
Self::Singular => None,
Self::Plus => Some("+"),
Self::Star => Some("*"),
}
}
pub(crate) fn is_variadic(self) -> bool {
self != Self::Singular
}
}

View File

@ -494,11 +494,19 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
let mut positional = Vec::new();
while self.next_is(Identifier) {
positional.push(self.parse_parameter(false)?);
positional.push(self.parse_parameter(ParameterKind::Singular)?);
}
let variadic = if self.accepted(Plus)? {
let variadic = self.parse_parameter(true)?;
let kind = if self.accepted(Plus)? {
ParameterKind::Plus
} else if self.accepted(Asterisk)? {
ParameterKind::Star
} else {
ParameterKind::Singular
};
let variadic = if kind.is_variadic() {
let variadic = self.parse_parameter(kind)?;
if let Some(identifier) = self.accept(Identifier)? {
return Err(
@ -560,7 +568,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
}
/// Parse a recipe parameter
fn parse_parameter(&mut self, variadic: bool) -> CompilationResult<'src, Parameter<'src>> {
fn parse_parameter(&mut self, kind: ParameterKind) -> CompilationResult<'src, Parameter<'src>> {
let name = self.parse_name()?;
let default = if self.accepted(Equals)? {
@ -571,8 +579,8 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
Ok(Parameter {
name,
kind,
default,
variadic,
})
}
@ -917,11 +925,17 @@ mod tests {
}
test! {
name: recipe_variadic,
name: recipe_plus_variadic,
text: r#"foo +bar:"#,
tree: (justfile (recipe foo (params +(bar)))),
}
test! {
name: recipe_star_variadic,
text: r#"foo *bar:"#,
tree: (justfile (recipe foo (params *(bar)))),
}
test! {
name: recipe_variadic_string_default,
text: r#"foo +bar="baz":"#,

View File

@ -44,12 +44,12 @@ impl<'src, D> Recipe<'src, D> {
self
.parameters
.iter()
.filter(|p| p.default.is_none())
.filter(|p| p.default.is_none() && p.kind != ParameterKind::Star)
.count()
}
pub(crate) fn max_arguments(&self) -> usize {
if self.parameters.iter().any(|p| p.variadic) {
if self.parameters.iter().any(|p| p.kind.is_variadic()) {
usize::max_value() - 1
} else {
self.parameters.len()

View File

@ -2,6 +2,7 @@ use crate::common::*;
#[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]
pub(crate) enum TokenKind {
Asterisk,
At,
Backtick,
BracketL,
@ -32,6 +33,7 @@ impl Display for TokenKind {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
use TokenKind::*;
write!(f, "{}", match *self {
Asterisk => "'*'",
At => "'@'",
Backtick => "backtick",
BracketL => "'['",

View File

@ -35,6 +35,12 @@ macro_rules! tree {
} => {
$crate::tree::Tree::atom("+")
};
{
*
} => {
$crate::tree::Tree::atom("*")
};
}
/// A `Tree` is either…

View File

@ -1081,7 +1081,7 @@ test! {
}
test! {
name: required_after_variadic,
name: required_after_plus_variadic,
justfile: "bar:\nhello baz +arg bar:",
stdout: "",
stderr: "error: Parameter `bar` follows variadic parameter
@ -1092,6 +1092,18 @@ test! {
status: EXIT_FAILURE,
}
test! {
name: required_after_star_variadic,
justfile: "bar:\nhello baz *arg bar:",
stdout: "",
stderr: "error: Parameter `bar` follows variadic parameter
|
2 | hello baz *arg bar:
| ^^^
",
status: EXIT_FAILURE,
}
test! {
name: use_string_default,
justfile: r#"
@ -1781,7 +1793,7 @@ a b= ":
}
test! {
name: variadic_recipe,
name: plus_variadic_recipe,
justfile: "
a x y +z:
echo {{x}} {{y}} {{z}}
@ -1792,7 +1804,7 @@ a x y +z:
}
test! {
name: variadic_ignore_default,
name: plus_variadic_ignore_default,
justfile: "
a x y +z='HELLO':
echo {{x}} {{y}} {{z}}
@ -1803,7 +1815,7 @@ a x y +z='HELLO':
}
test! {
name: variadic_use_default,
name: plus_variadic_use_default,
justfile: "
a x y +z='HELLO':
echo {{x}} {{y}} {{z}}
@ -1814,7 +1826,7 @@ a x y +z='HELLO':
}
test! {
name: variadic_too_few,
name: plus_variadic_too_few,
justfile: "
a x y +z:
echo {{x}} {{y}} {{z}}
@ -1825,6 +1837,80 @@ a x y +z:
status: EXIT_FAILURE,
}
test! {
name: star_variadic_recipe,
justfile: "
a x y *z:
echo {{x}} {{y}} {{z}}
",
args: ("a", "0", "1", "2", "3", " 4 "),
stdout: "0 1 2 3 4\n",
stderr: "echo 0 1 2 3 4 \n",
}
test! {
name: star_variadic_none,
justfile: "
a x y *z:
echo {{x}} {{y}} {{z}}
",
args: ("a", "0", "1"),
stdout: "0 1\n",
stderr: "echo 0 1 \n",
}
test! {
name: star_variadic_ignore_default,
justfile: "
a x y *z='HELLO':
echo {{x}} {{y}} {{z}}
",
args: ("a", "0", "1", "2", "3", " 4 "),
stdout: "0 1 2 3 4\n",
stderr: "echo 0 1 2 3 4 \n",
}
test! {
name: star_variadic_use_default,
justfile: "
a x y *z='HELLO':
echo {{x}} {{y}} {{z}}
",
args: ("a", "0", "1"),
stdout: "0 1 HELLO\n",
stderr: "echo 0 1 HELLO\n",
}
test! {
name: star_then_plus_variadic,
justfile: "
foo *a +b:
echo {{a}} {{b}}
",
stdout: "",
stderr: "error: Expected \':\' or \'=\', but found \'+\'
|
2 | foo *a +b:
| ^
",
status: EXIT_FAILURE,
}
test! {
name: plus_then_star_variadic,
justfile: "
foo +a *b:
echo {{a}} {{b}}
",
stdout: "",
stderr: "error: Expected \':\' or \'=\', but found \'*\'
|
2 | foo +a *b:
| ^
",
status: EXIT_FAILURE,
}
test! {
name: argument_grouping,
justfile: "
@ -2429,7 +2515,7 @@ test! {
}
test! {
name: dependency_argument_variadic,
name: dependency_argument_plus_variadic,
justfile: "
foo: (bar 'A' 'B' 'C')