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 sequence : expression ',' sequence
| expression ','? | expression ','?
recipe : '@'? NAME parameter* ('+' parameter)? ':' dependency* body? recipe : '@'? NAME parameter* variadic? ':' dependency* body?
parameter : NAME parameter : NAME
| NAME '=' value | NAME '=' value
variadic : '*' parameter
| '+' parameter
dependency : NAME dependency : NAME
| '(' NAME expression* ') | '(' NAME expression* ')

View File

@ -572,14 +572,14 @@ test triple=(arch + "-unknown-unknown"):
./test {{triple}} ./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 ```make
backup +FILES: backup +FILES:
scp {{FILES}} me@server.com: 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 ```sh
$ just backup FAQ.md GRAMMAR.md $ 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 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 ```make
commit MESSAGE +FLAGS='': commit MESSAGE *FLAGS:
git commit {{FLAGS}} -m "{{MESSAGE}}" 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: `{{...}}` substitutions may need to be quoted if they contains spaces. For example, if you have the following recipe:
```make ```make

View File

@ -56,11 +56,11 @@ pub(crate) use crate::{
fragment::Fragment, function::Function, function_context::FunctionContext, fragment::Fragment, function::Function, function_context::FunctionContext,
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item, interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
justfile::Justfile, lexer::Lexer, line::Line, list::List, load_error::LoadError, module::Module, justfile::Justfile, lexer::Lexer, line::Line, list::List, load_error::LoadError, module::Module,
name::Name, output_error::OutputError, parameter::Parameter, parser::Parser, platform::Platform, name::Name, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind,
position::Position, positional::Positional, recipe::Recipe, recipe_context::RecipeContext, parser::Parser, platform::Platform, position::Position, positional::Positional, recipe::Recipe,
recipe_resolver::RecipeResolver, runtime_error::RuntimeError, scope::Scope, search::Search, recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError,
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting, scope::Scope, search::Search, search_config::SearchConfig, search_error::SearchError, set::Set,
settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, setting::Setting, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace,
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table, string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency, thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables, 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() { let value = if rest.is_empty() {
if let Some(ref default) = parameter.default { if let Some(ref default) = parameter.default {
evaluator.evaluate_expression(default)? evaluator.evaluate_expression(default)?
} else if parameter.kind == ParameterKind::Star {
String::new()
} else { } else {
return Err(RuntimeError::Internal { return Err(RuntimeError::Internal {
message: "missing parameter without default".to_string(), message: "missing parameter without default".to_string(),
}); });
} }
} else if parameter.variadic { } else if parameter.kind.is_variadic() {
let value = rest.to_vec().join(" "); let value = rest.to_vec().join(" ");
rest = &[]; rest = &[];
value value

View File

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

View File

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

View File

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

View File

@ -4,18 +4,18 @@ use crate::common::*;
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub(crate) struct Parameter<'src> { pub(crate) struct Parameter<'src> {
/// The parameter name /// The parameter name
pub(crate) name: Name<'src>, pub(crate) name: Name<'src>,
/// Parameter is variadic /// The kind of parameter
pub(crate) variadic: bool, pub(crate) kind: ParameterKind,
/// An optional default expression /// An optional default expression
pub(crate) default: Option<Expression<'src>>, pub(crate) default: Option<Expression<'src>>,
} }
impl<'src> Display for Parameter<'src> { impl<'src> Display for Parameter<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
let color = Color::fmt(f); let color = Color::fmt(f);
if self.variadic { if let Some(prefix) = self.kind.prefix() {
write!(f, "{}", color.annotation().paint("+"))?; write!(f, "{}", color.annotation().paint(prefix))?;
} }
write!(f, "{}", color.parameter().paint(self.name.lexeme()))?; write!(f, "{}", color.parameter().paint(self.name.lexeme()))?;
if let Some(ref default) = self.default { 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(); let mut positional = Vec::new();
while self.next_is(Identifier) { 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 kind = if self.accepted(Plus)? {
let variadic = self.parse_parameter(true)?; 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)? { if let Some(identifier) = self.accept(Identifier)? {
return Err( return Err(
@ -560,7 +568,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
} }
/// Parse a recipe parameter /// 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 name = self.parse_name()?;
let default = if self.accepted(Equals)? { let default = if self.accepted(Equals)? {
@ -571,8 +579,8 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
Ok(Parameter { Ok(Parameter {
name, name,
kind,
default, default,
variadic,
}) })
} }
@ -917,11 +925,17 @@ mod tests {
} }
test! { test! {
name: recipe_variadic, name: recipe_plus_variadic,
text: r#"foo +bar:"#, text: r#"foo +bar:"#,
tree: (justfile (recipe foo (params +(bar)))), tree: (justfile (recipe foo (params +(bar)))),
} }
test! {
name: recipe_star_variadic,
text: r#"foo *bar:"#,
tree: (justfile (recipe foo (params *(bar)))),
}
test! { test! {
name: recipe_variadic_string_default, name: recipe_variadic_string_default,
text: r#"foo +bar="baz":"#, text: r#"foo +bar="baz":"#,

View File

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

View File

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

View File

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

View File

@ -1081,7 +1081,7 @@ test! {
} }
test! { test! {
name: required_after_variadic, name: required_after_plus_variadic,
justfile: "bar:\nhello baz +arg bar:", justfile: "bar:\nhello baz +arg bar:",
stdout: "", stdout: "",
stderr: "error: Parameter `bar` follows variadic parameter stderr: "error: Parameter `bar` follows variadic parameter
@ -1092,6 +1092,18 @@ test! {
status: EXIT_FAILURE, 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! { test! {
name: use_string_default, name: use_string_default,
justfile: r#" justfile: r#"
@ -1781,7 +1793,7 @@ a b= ":
} }
test! { test! {
name: variadic_recipe, name: plus_variadic_recipe,
justfile: " justfile: "
a x y +z: a x y +z:
echo {{x}} {{y}} {{z}} echo {{x}} {{y}} {{z}}
@ -1792,7 +1804,7 @@ a x y +z:
} }
test! { test! {
name: variadic_ignore_default, name: plus_variadic_ignore_default,
justfile: " justfile: "
a x y +z='HELLO': a x y +z='HELLO':
echo {{x}} {{y}} {{z}} echo {{x}} {{y}} {{z}}
@ -1803,7 +1815,7 @@ a x y +z='HELLO':
} }
test! { test! {
name: variadic_use_default, name: plus_variadic_use_default,
justfile: " justfile: "
a x y +z='HELLO': a x y +z='HELLO':
echo {{x}} {{y}} {{z}} echo {{x}} {{y}} {{z}}
@ -1814,7 +1826,7 @@ a x y +z='HELLO':
} }
test! { test! {
name: variadic_too_few, name: plus_variadic_too_few,
justfile: " justfile: "
a x y +z: a x y +z:
echo {{x}} {{y}} {{z}} echo {{x}} {{y}} {{z}}
@ -1825,6 +1837,80 @@ a x y +z:
status: EXIT_FAILURE, 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! { test! {
name: argument_grouping, name: argument_grouping,
justfile: " justfile: "
@ -2429,7 +2515,7 @@ test! {
} }
test! { test! {
name: dependency_argument_variadic, name: dependency_argument_plus_variadic,
justfile: " justfile: "
foo: (bar 'A' 'B' 'C') foo: (bar 'A' 'B' 'C')