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:
parent
63f51b5b48
commit
1ff619295c
@ -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* ')
|
||||
|
||||
|
15
README.adoc
15
README.adoc
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 => "]",
|
||||
|
@ -91,6 +91,7 @@ mod ordinal;
|
||||
mod output;
|
||||
mod output_error;
|
||||
mod parameter;
|
||||
mod parameter_kind;
|
||||
mod parser;
|
||||
mod platform;
|
||||
mod platform_interface;
|
||||
|
@ -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());
|
||||
|
@ -5,8 +5,8 @@ use crate::common::*;
|
||||
pub(crate) struct Parameter<'src> {
|
||||
/// The parameter name
|
||||
pub(crate) name: Name<'src>,
|
||||
/// Parameter is variadic
|
||||
pub(crate) variadic: bool,
|
||||
/// The kind of parameter
|
||||
pub(crate) kind: ParameterKind,
|
||||
/// An optional default expression
|
||||
pub(crate) default: Option<Expression<'src>>,
|
||||
}
|
||||
@ -14,8 +14,8 @@ pub(crate) struct Parameter<'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
26
src/parameter_kind.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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":"#,
|
||||
|
@ -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()
|
||||
|
@ -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 => "'['",
|
||||
|
@ -35,6 +35,12 @@ macro_rules! tree {
|
||||
} => {
|
||||
$crate::tree::Tree::atom("+")
|
||||
};
|
||||
|
||||
{
|
||||
*
|
||||
} => {
|
||||
$crate::tree::Tree::atom("*")
|
||||
};
|
||||
}
|
||||
|
||||
/// A `Tree` is either…
|
||||
|
@ -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')
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user