Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -933,18 +933,20 @@ test +FLAGS='-q':
```

The number of arguments a variadic parameter accepts may be limited with the
`[arg(ARG, max=MAX)]` attribute<sup>master</sup>, which requires lists to be
enabled:
`[arg(ARG, min=MIN)]` and `[arg(ARG, max=MAX)]` attributes<sup>master</sup>,
which require lists to be enabled:

```just
set unstable
set lists

[arg('FILES', max='2')]
[arg('FILES', min='2', max='4')]
backup +FILES:
scp {{FILES}} me@server.com:
```

`min` and `max` also apply to default values.

`{{…}}` substitutions may need to be quoted if they contain spaces. For
example, if you have the following recipe:

Expand Down Expand Up @@ -2210,8 +2212,8 @@ once, assigning the list of passed values to the parameter. When combined with
`flag` or `value=VALUE`, `"true"` or `VALUE`, respectively, are repeated for
each occurance of the flag.

The `[arg(max=MAX)]` attribute<sup>master</sup> can be used to limit the number
of times an option or flag may be passed.
The `[arg(min=MIN)]` and `[arg(max=MAX)]` attributes<sup>master</sup> can be
used to limit the number of times an option or flag may be passed.

The value of `[arg(help)]` may be a list, in which case the help string is the
elements of the list joined with spaces. If the list is empty, the argument has
Expand Down Expand Up @@ -4700,6 +4702,7 @@ change their behavior.
| `[arg(ARG, help="HELP")]`<sup>1.46.0</sup> | recipe | Print help string `HELP` for `ARG` in usage messages. May be a const expression<sup>1.55.0</sup>. |
| `[arg(ARG, long="LONG")]`<sup>1.46.0</sup> | recipe | Require values of argument `ARG` to be passed as `--LONG` option. If the parameter is variadic, the option is repeatable<sup>1.55.0master</sup>. |
| `[arg(ARG, max="MAX")]`<sup>master</sup> | recipe | Allow at most `MAX` values to be passed to argument `ARG`. Requires `multiple` or a variadic parameter. |
| `[arg(ARG, min="MIN")]`<sup>master</sup> | recipe | Require at least `MIN` values to be passed to argument `ARG`. Requires `multiple` or a variadic parameter. |
| `[arg(ARG, pattern="PATTERN")]`<sup>1.45.0</sup> | recipe | Require values of argument `ARG` to match regular expression `PATTERN`. May be a const expression<sup>1.55.0</sup>. |
| `[arg(ARG, short="S")]`<sup>1.46.0</sup> | recipe | Require values of argument `ARG` to be passed as short `-S` option. If the parameter is variadic, the option is repeatable<sup>1.55.0</sup>. |
| `[arg(ARG, value=VALUE)]`<sup>1.46.0</sup> | recipe | Makes option `ARG` a flag which does not take a value. |
Expand Down
1 change: 1 addition & 0 deletions src/arg_attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub(crate) struct ArgAttribute<'src> {
pub(crate) flag: bool,
pub(crate) long: Option<String>,
pub(crate) max: Option<(Name<'src>, u64)>,
pub(crate) min: Option<(Name<'src>, u64)>,
pub(crate) multiple: bool,
pub(crate) name: Token<'src>,
pub(crate) short: Option<char>,
Expand Down
82 changes: 59 additions & 23 deletions src/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ pub(crate) enum Attribute<'src> {
max: Option<u64>,
#[serde(skip)]
max_key: Option<Name<'src>>,
min: Option<u64>,
#[serde(skip)]
min_key: Option<Name<'src>>,
#[serde(skip)]
multiple: Option<Token<'src>>,
name: StringLiteral<'src>,
Expand Down Expand Up @@ -199,16 +202,18 @@ impl<'src> Attribute<'src> {
.into_iter()
.map(|(token, argument)| {
let Expression::StringLiteral { string_literal } = argument else {
return Err(token.error(CompileErrorKind::AttributeArgumentExpression {
attribute: name.lexeme(),
}));
return Err(
token.error(CompileErrorKind::AttributeArgumentExpression { attribute: name }),
);
};
Ok(string_literal)
})
.collect::<CompileResult<Vec<StringLiteral>>>()?;

let attribute = match kind {
AttributeKind::Arg => {
static NUMBER: LazyLock<Regex> = LazyLock::new(|| Regex::new("^(0|[1-9][0-9]*)$").unwrap());

let arg = arguments.into_iter().next().unwrap();

let (long, long_key) = keyword_arguments
Expand Down Expand Up @@ -254,9 +259,7 @@ impl<'src> Attribute<'src> {
let value = Self::remove_required(&mut keyword_arguments, "value")?
.map(|(key, expression)| {
if long.is_none() && short.is_none() {
return Err(
key.error(CompileErrorKind::ArgAttributeRequiresOption { key: key.lexeme() }),
);
return Err(key.error(CompileErrorKind::ArgAttributeRequiresOption { key }));
}
Ok(expression)
})
Expand All @@ -271,9 +274,7 @@ impl<'src> Attribute<'src> {
}));
}
if long.is_none() && short.is_none() {
return Err(
key.error(CompileErrorKind::ArgAttributeRequiresOption { key: key.lexeme() }),
);
return Err(key.error(CompileErrorKind::ArgAttributeRequiresOption { key }));
}
if value.is_some() {
return Err(key.error(CompileErrorKind::FlagAndValueArgAttribute {
Expand All @@ -288,34 +289,29 @@ impl<'src> Attribute<'src> {
.remove("multiple")
.map(|(key, expression)| {
if expression.is_some() {
return Err(
key.error(CompileErrorKind::AttributeKeyTakesNoValue { key: key.lexeme() }),
);
return Err(key.error(CompileErrorKind::AttributeKeyTakesNoValue { key }));
}
if long.is_none() && short.is_none() {
return Err(
key.error(CompileErrorKind::ArgAttributeRequiresOption { key: key.lexeme() }),
);
return Err(key.error(CompileErrorKind::ArgAttributeRequiresOption { key }));
}
Ok(*key)
})
.transpose()?;

let (max, max_key) = Self::remove_required(&mut keyword_arguments, "max")?
.map(|(key, expression)| {
static NUMBER: LazyLock<Regex> =
LazyLock::new(|| Regex::new("^(0|[1-9][0-9]*)$").unwrap());

let literal = Self::require_string_literal(name, key, expression)?;

if !NUMBER.is_match(&literal.cooked) {
return Err(literal.token.error(CompileErrorKind::ArgumentMaxValue {
return Err(literal.token.error(CompileErrorKind::ArgumentCountValue {
key,
value: literal.cooked.clone(),
}));
}

let max = literal.cooked.parse::<u64>().map_err(|source| {
literal.token.error(CompileErrorKind::ArgumentMaxParse {
literal.token.error(CompileErrorKind::ArgumentCountParse {
key,
value: literal.cooked.clone(),
source,
})
Expand All @@ -326,6 +322,40 @@ impl<'src> Attribute<'src> {
.transpose()?
.unwrap_or_default();

let (min, min_key) = Self::remove_required(&mut keyword_arguments, "min")?
.map(|(key, expression)| {
let literal = Self::require_string_literal(name, key, expression)?;

if !NUMBER.is_match(&literal.cooked) {
return Err(literal.token.error(CompileErrorKind::ArgumentCountValue {
key,
value: literal.cooked.clone(),
}));
}

let min = literal.cooked.parse::<u64>().map_err(|source| {
literal.token.error(CompileErrorKind::ArgumentCountParse {
key,
value: literal.cooked.clone(),
source,
})
})?;

Ok((Some(min), Some(key)))
})
.transpose()?
.unwrap_or_default();

if let (Some(min), Some(max)) = (min, max)
&& min > max
{
return Err(
min_key
.unwrap()
.error(CompileErrorKind::ArgAttributeMinExceedsMax { min, max }),
);
}

let help_property = Self::remove_required(&mut keyword_arguments, "help")?;

Self::Arg {
Expand All @@ -336,6 +366,8 @@ impl<'src> Attribute<'src> {
long_key,
max,
max_key,
min,
min_key,
multiple,
name: arg,
pattern: None,
Expand Down Expand Up @@ -431,9 +463,7 @@ impl<'src> Attribute<'src> {
expression: Expression<'src>,
) -> CompileResult<'src, StringLiteral<'src>> {
let Expression::StringLiteral { string_literal } = expression else {
return Err(key.error(CompileErrorKind::AttributeArgumentExpression {
attribute: attribute.lexeme(),
}));
return Err(key.error(CompileErrorKind::AttributeArgumentExpression { attribute }));
};

Ok(string_literal)
Expand Down Expand Up @@ -468,6 +498,8 @@ impl Display for Attribute<'_> {
long_key,
max,
max_key: _,
min,
min_key: _,
multiple,
name,
pattern: _,
Expand Down Expand Up @@ -506,6 +538,10 @@ impl Display for Attribute<'_> {
write!(f, ", multiple")?;
}

if let Some(min) = min {
write!(f, ", min='{min}'")?;
}

if let Some(max) = max {
write!(f, ", max='{max}'")?;
}
Expand Down
15 changes: 9 additions & 6 deletions src/compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@ impl Display for CompileError<'_> {
use CompileErrorKind::*;

match &*self.kind {
ArgAttributeMaxRequiresMultipleOrVariadic => {
ArgAttributeMinExceedsMax { min, max } => {
write!(f, "argument attribute `min` `{min}` exceeds `max` `{max}`")
}
ArgAttributeRequiresMultipleOrVariadic { key } => {
write!(
f,
"argument attribute `max` only valid with `multiple` or a variadic parameter"
"argument attribute `{key}` only valid with `multiple` or a variadic parameter"
)
}
ArgAttributeRequiresOption { key } => {
Expand All @@ -43,11 +46,11 @@ impl Display for CompileError<'_> {
"argument attribute `{key}` only valid with `long` or `short`"
)
}
ArgumentMaxParse { value, source } => {
write!(f, "invalid `max` value `{value}`: {source}")
ArgumentCountParse { key, value, source } => {
write!(f, "invalid `{key}` value `{value}`: {source}")
}
ArgumentMaxValue { value } => {
write!(f, "invalid `max` value `{value}`")
ArgumentCountValue { key, value } => {
write!(f, "invalid `{key}` value `{value}`")
}
ArgumentPatternRegex { .. } => {
write!(f, "failed to parse argument pattern")
Expand Down
22 changes: 15 additions & 7 deletions src/compile_error_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@ use super::*;

#[derive(Debug, PartialEq)]
pub(crate) enum CompileErrorKind<'src> {
ArgAttributeMaxRequiresMultipleOrVariadic,
ArgAttributeMinExceedsMax {
min: u64,
max: u64,
},
ArgAttributeRequiresMultipleOrVariadic {
key: Name<'src>,
},
ArgAttributeRequiresOption {
key: &'src str,
key: Name<'src>,
},
ArgumentMaxParse {
ArgumentCountParse {
key: Name<'src>,
value: String,
source: ParseIntError,
},
ArgumentMaxValue {
ArgumentCountValue {
key: Name<'src>,
value: String,
},
ArgumentPatternRegex {
Expand All @@ -23,13 +31,13 @@ pub(crate) enum CompileErrorKind<'src> {
max: usize,
},
AttributeArgumentExpression {
attribute: &'src str,
attribute: Name<'src>,
},
AttributeKeyMissingValue {
key: Name<'src>,
},
AttributeKeyTakesNoValue {
key: &'src str,
key: Name<'src>,
},
AttributePositionalFollowsKeyword,
BacktickShebang,
Expand All @@ -53,7 +61,7 @@ pub(crate) enum CompileErrorKind<'src> {
first: usize,
},
DuplicateAttribute {
attribute: &'src str,
attribute: Name<'src>,
first: usize,
},
DuplicateAttributeKey {
Expand Down
18 changes: 18 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ pub(crate) enum Error<'src> {
pattern: Box<Pattern>,
recipe: &'src str,
},
ArgumentTooFewValues {
recipe: &'src str,
parameter: &'src str,
found: usize,
min: u64,
},
ArgumentTooManyValues {
recipe: &'src str,
parameter: &'src str,
Expand Down Expand Up @@ -512,6 +518,18 @@ impl ColorDisplay for Error<'_> {
List::or_ticked(pattern.originals()),
)?;
}
ArgumentTooFewValues {
recipe,
parameter,
found,
min,
} => {
write!(
f,
"recipe `{recipe}` parameter `{parameter}` got {} but takes at least {min}",
Count::numbered("value", found),
)?;
}
ArgumentTooManyValues {
recipe,
parameter,
Expand Down
4 changes: 2 additions & 2 deletions src/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -769,8 +769,6 @@ impl<'src, 'run> Evaluator<'src, 'run> {
}

for (parameter, argument) in parameters.iter().zip(arguments) {
parameter.check_value_count(recipe, argument)?;

let value = if argument.elements().is_empty() {
if let Some(default) = &parameter.default {
evaluator.evaluate_value(default)?
Expand All @@ -796,6 +794,8 @@ impl<'src, 'run> Evaluator<'src, 'run> {
argument.clone()
};

parameter.check_value_count(recipe, &value)?;

for element in &value {
parameter.check_pattern_match(recipe, element)?;
}
Expand Down
6 changes: 4 additions & 2 deletions src/invocation_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,14 @@ impl<'src: 'run, 'run> InvocationParser<'src, 'run> {
}

for (group, parameter) in arguments.iter().zip(&recipe.parameters) {
parameter.check_value_count(recipe, group)?;

if parameter.value.is_some() {
continue;
}

if !group.is_empty() {
parameter.check_value_count(recipe, group)?;
}

for element in group {
parameter.check_pattern_match(recipe, element)?;
}
Expand Down
Loading
Loading