Skip to content

Commit

Permalink
Add variadic parameters that accept zero or more arguments (#645)
Browse files Browse the repository at this point in the history
Add "star" variadic parameters that accept zero or more arguments,
distinguished with a `*` in front of the parameter name.
  • Loading branch information
rjsberry authored Jun 13, 2020
1 parent 63f51b5 commit 1ff6192
Show file tree
Hide file tree
Showing 14 changed files with 182 additions and 33 deletions.
5 changes: 4 additions & 1 deletion GRAMMAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -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* ')
Expand Down
15 changes: 11 additions & 4 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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}} [email protected]:
```

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
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion src/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -806,6 +807,7 @@ mod tests {
fn default_lexeme(kind: TokenKind) -> &'static str {
match kind {
// Fixed lexemes
Asterisk => "*",
At => "@",
BracketL => "[",
BracketR => "]",
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ mod ordinal;
mod output;
mod output_error;
mod parameter;
mod parameter_kind;
mod parser;
mod platform;
mod platform_interface;
Expand Down
4 changes: 2 additions & 2 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
12 changes: 6 additions & 6 deletions src/parameter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 26 additions & 0 deletions src/parameter_kind.rs
Original file line number Diff line number Diff line change
@@ -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
}
}
26 changes: 20 additions & 6 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)? {
Expand All @@ -571,8 +579,8 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {

Ok(Parameter {
name,
kind,
default,
variadic,
})
}

Expand Down Expand Up @@ -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":"#,
Expand Down
4 changes: 2 additions & 2 deletions src/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions src/token_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::common::*;

#[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]
pub(crate) enum TokenKind {
Asterisk,
At,
Backtick,
BracketL,
Expand Down Expand Up @@ -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 => "'['",
Expand Down
6 changes: 6 additions & 0 deletions src/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ macro_rules! tree {
} => {
$crate::tree::Tree::atom("+")
};

{
*
} => {
$crate::tree::Tree::atom("*")
};
}

/// A `Tree` is either…
Expand Down
Loading

0 comments on commit 1ff6192

Please sign in to comment.