From 1ff619295cc15d93bddd0c5d3e29f40c95124a62 Mon Sep 17 00:00:00 2001 From: Richard Berry <17755880+rjsberry@users.noreply.github.com> Date: Sat, 13 Jun 2020 09:49:13 +0100 Subject: [PATCH] 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. --- GRAMMAR.md | 5 ++- README.adoc | 15 +++++-- src/common.rs | 10 ++--- src/evaluator.rs | 4 +- src/lexer.rs | 2 + src/lib.rs | 1 + src/node.rs | 4 +- src/parameter.rs | 12 +++--- src/parameter_kind.rs | 26 ++++++++++++ src/parser.rs | 26 +++++++++--- src/recipe.rs | 4 +- src/token_kind.rs | 2 + src/tree.rs | 6 +++ tests/integration.rs | 98 ++++++++++++++++++++++++++++++++++++++++--- 14 files changed, 182 insertions(+), 33 deletions(-) create mode 100644 src/parameter_kind.rs diff --git a/GRAMMAR.md b/GRAMMAR.md index fc851f2..73ecdb0 100644 --- a/GRAMMAR.md +++ b/GRAMMAR.md @@ -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* ') diff --git a/README.adoc b/README.adoc index edc2ac7..807eae9 100644 --- a/README.adoc +++ b/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 diff --git a/src/common.rs b/src/common.rs index 1ada469..c9ddb1b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -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, diff --git a/src/evaluator.rs b/src/evaluator.rs index dddbeee..9db8327 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -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 diff --git a/src/lexer.rs b/src/lexer.rs index eeca8b6..bfc3f04 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -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 => "]", diff --git a/src/lib.rs b/src/lib.rs index 398ba50..db2f44d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,6 +91,7 @@ mod ordinal; mod output; mod output_error; mod parameter; +mod parameter_kind; mod parser; mod platform; mod platform_interface; diff --git a/src/node.rs b/src/node.rs index c55d018..4a65dff 100644 --- a/src/node.rs +++ b/src/node.rs @@ -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()); diff --git a/src/parameter.rs b/src/parameter.rs index 2854ffc..b140f8b 100644 --- a/src/parameter.rs +++ b/src/parameter.rs @@ -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>, + pub(crate) default: Option>, } 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 { diff --git a/src/parameter_kind.rs b/src/parameter_kind.rs new file mode 100644 index 0000000..0161ae1 --- /dev/null +++ b/src/parameter_kind.rs @@ -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 + } +} diff --git a/src/parser.rs b/src/parser.rs index fed78cd..11104fd 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -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":"#, diff --git a/src/recipe.rs b/src/recipe.rs index 50c2d39..2436e08 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -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() diff --git a/src/token_kind.rs b/src/token_kind.rs index 38a4944..30dd34d 100644 --- a/src/token_kind.rs +++ b/src/token_kind.rs @@ -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 => "'['", diff --git a/src/tree.rs b/src/tree.rs index f7821cf..9ddfa61 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -35,6 +35,12 @@ macro_rules! tree { } => { $crate::tree::Tree::atom("+") }; + + { + * + } => { + $crate::tree::Tree::atom("*") + }; } /// A `Tree` is either… diff --git a/tests/integration.rs b/tests/integration.rs index beacdd6..1ebd70a 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -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')