mirror of
https://github.com/casey/just.git
synced 2025-12-23 11:37:29 +00:00
Allow requiring arguments to match patterns
This commit is contained in:
parent
db93841ea5
commit
64e7a15882
16 changed files with 620 additions and 28 deletions
26
README.md
26
README.md
|
|
@ -2730,6 +2730,32 @@ foo $bar:
|
|||
echo $bar
|
||||
```
|
||||
|
||||
Parameters may be constrained to match regular expression patterns using the
|
||||
`[arg("name", pattern="pattern")]` attribute<sup>master</sup>:
|
||||
|
||||
```just
|
||||
[arg('n', pattern='\d+')]
|
||||
double n:
|
||||
echo $(({{n}} * 2))
|
||||
```
|
||||
|
||||
A leading `^` and trailing `$` are added to the pattern, so it must match the
|
||||
entire argument value.
|
||||
|
||||
You may constrain the pattern to a number of alternatives using the `|`
|
||||
operator:
|
||||
|
||||
```just
|
||||
[arg('flag', pattern='--help|--version')]
|
||||
info flag:
|
||||
just {{flag}}
|
||||
```
|
||||
|
||||
Regular expressions are provided by the
|
||||
[Rust `regex` crate](https://docs.rs/regex/latest/regex/). See the
|
||||
[syntax documentation](https://docs.rs/regex/latest/regex/#syntax) for usage
|
||||
examples.
|
||||
|
||||
### Dependencies
|
||||
|
||||
Dependencies run before recipes that depend on them:
|
||||
|
|
|
|||
6
src/arg_attribute.rs
Normal file
6
src/arg_attribute.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use super::*;
|
||||
|
||||
pub(crate) struct ArgAttribute<'src> {
|
||||
pub(crate) name: Token<'src>,
|
||||
pub(crate) pattern: Option<Pattern>,
|
||||
}
|
||||
|
|
@ -91,6 +91,10 @@ impl<'src: 'run, 'run> ArgumentParser<'src, 'run> {
|
|||
|
||||
let arguments = rest[..argument_count].to_vec();
|
||||
|
||||
for (argument, parameter) in arguments.iter().zip(&recipe.parameters) {
|
||||
parameter.is_pattern_match(recipe, argument)?;
|
||||
}
|
||||
|
||||
self.next += argument_count;
|
||||
|
||||
Ok(ArgumentGroup { arguments, path })
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ use super::*;
|
|||
#[strum_discriminants(derive(EnumString, Ord, PartialOrd))]
|
||||
#[strum_discriminants(strum(serialize_all = "kebab-case"))]
|
||||
pub(crate) enum Attribute<'src> {
|
||||
Arg {
|
||||
name: StringLiteral<'src>,
|
||||
#[serde(skip)]
|
||||
name_token: Token<'src>,
|
||||
pattern: Option<(StringLiteral<'src>, Pattern)>,
|
||||
},
|
||||
Confirm(Option<StringLiteral<'src>>),
|
||||
Default,
|
||||
Doc(Option<StringLiteral<'src>>),
|
||||
|
|
@ -34,7 +40,6 @@ pub(crate) enum Attribute<'src> {
|
|||
impl AttributeDiscriminant {
|
||||
fn argument_range(self) -> RangeInclusive<usize> {
|
||||
match self {
|
||||
Self::Confirm | Self::Doc => 0..=1,
|
||||
Self::Default
|
||||
| Self::ExitMessage
|
||||
| Self::Linux
|
||||
|
|
@ -48,9 +53,10 @@ impl AttributeDiscriminant {
|
|||
| Self::Private
|
||||
| Self::Unix
|
||||
| Self::Windows => 0..=0,
|
||||
Self::Extension | Self::Group | Self::WorkingDirectory => 1..=1,
|
||||
Self::Metadata => 1..=usize::MAX,
|
||||
Self::Confirm | Self::Doc => 0..=1,
|
||||
Self::Script => 0..=usize::MAX,
|
||||
Self::Arg | Self::Extension | Self::Group | Self::WorkingDirectory => 1..=1,
|
||||
Self::Metadata => 1..=usize::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,7 +64,8 @@ impl AttributeDiscriminant {
|
|||
impl<'src> Attribute<'src> {
|
||||
pub(crate) fn new(
|
||||
name: Name<'src>,
|
||||
arguments: Vec<StringLiteral<'src>>,
|
||||
arguments: Vec<(Token<'src>, StringLiteral<'src>)>,
|
||||
mut keyword_arguments: BTreeMap<&'src str, (Name<'src>, Token<'src>, StringLiteral<'src>)>,
|
||||
) -> CompileResult<'src, Self> {
|
||||
let discriminant = name
|
||||
.lexeme()
|
||||
|
|
@ -83,7 +90,24 @@ impl<'src> Attribute<'src> {
|
|||
);
|
||||
}
|
||||
|
||||
Ok(match discriminant {
|
||||
let (tokens, arguments): (Vec<Token>, Vec<StringLiteral>) = arguments.into_iter().unzip();
|
||||
|
||||
let attribute = match discriminant {
|
||||
AttributeDiscriminant::Arg => {
|
||||
let pattern = keyword_arguments
|
||||
.remove("pattern")
|
||||
.map(|(_name, token, literal)| {
|
||||
let pattern = Pattern::new(token, &literal)?;
|
||||
Ok((literal, pattern))
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
Self::Arg {
|
||||
name: arguments.into_iter().next().unwrap(),
|
||||
name_token: tokens.into_iter().next().unwrap(),
|
||||
pattern,
|
||||
}
|
||||
}
|
||||
AttributeDiscriminant::Confirm => Self::Confirm(arguments.into_iter().next()),
|
||||
AttributeDiscriminant::Default => Self::Default,
|
||||
AttributeDiscriminant::Doc => Self::Doc(arguments.into_iter().next()),
|
||||
|
|
@ -112,7 +136,18 @@ impl<'src> Attribute<'src> {
|
|||
AttributeDiscriminant::WorkingDirectory => {
|
||||
Self::WorkingDirectory(arguments.into_iter().next().unwrap())
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
if let Some((_name, (keyword_name, _token, _literal))) = keyword_arguments.into_iter().next() {
|
||||
return Err(
|
||||
keyword_name.error(CompileErrorKind::UnknownAttributeKeyword {
|
||||
attribute: name.lexeme(),
|
||||
keyword: keyword_name.lexeme(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(attribute)
|
||||
}
|
||||
|
||||
pub(crate) fn discriminant(&self) -> AttributeDiscriminant {
|
||||
|
|
@ -124,7 +159,10 @@ impl<'src> Attribute<'src> {
|
|||
}
|
||||
|
||||
pub(crate) fn repeatable(&self) -> bool {
|
||||
matches!(self, Attribute::Group(_) | Attribute::Metadata(_))
|
||||
matches!(
|
||||
self,
|
||||
Attribute::Arg { .. } | Attribute::Group(_) | Attribute::Metadata(_),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,6 +171,15 @@ impl Display for Attribute<'_> {
|
|||
write!(f, "{}", self.name())?;
|
||||
|
||||
match self {
|
||||
Self::Arg { name, pattern, .. } => {
|
||||
write!(f, "({name}")?;
|
||||
|
||||
if let Some((literal, _pattern)) = pattern {
|
||||
write!(f, ", pattern={literal}")?;
|
||||
}
|
||||
|
||||
write!(f, ")")?;
|
||||
}
|
||||
Self::Confirm(None)
|
||||
| Self::Default
|
||||
| Self::Doc(None)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,13 @@ impl<'src> CompileError<'src> {
|
|||
kind: kind.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn source(&self) -> Option<&dyn std::error::Error> {
|
||||
match &*self.kind {
|
||||
CompileErrorKind::ArgumentPatternRegex { source } => Some(source),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn capitalize(s: &str) -> String {
|
||||
|
|
@ -32,6 +39,9 @@ impl Display for CompileError<'_> {
|
|||
use CompileErrorKind::*;
|
||||
|
||||
match &*self.kind {
|
||||
ArgumentPatternRegex { .. } => {
|
||||
write!(f, "Failed to parse argument pattern")
|
||||
}
|
||||
AttributeArgumentCountMismatch {
|
||||
attribute,
|
||||
found,
|
||||
|
|
@ -97,6 +107,12 @@ impl Display for CompileError<'_> {
|
|||
write!(f, "at most {max} {}", Count("argument", *max))
|
||||
}
|
||||
}
|
||||
DuplicateArgAttribute { arg, first } => write!(
|
||||
f,
|
||||
"Recipe attribute for argument `{arg}` first used on line {} is duplicated on line {}",
|
||||
first.ordinal(),
|
||||
self.token.line.ordinal(),
|
||||
),
|
||||
DuplicateAttribute { attribute, first } => write!(
|
||||
f,
|
||||
"Recipe attribute `{attribute}` first used on line {} is duplicated on line {}",
|
||||
|
|
@ -216,6 +232,9 @@ impl Display for CompileError<'_> {
|
|||
write!(f, "Parameter `{parameter}` follows variadic parameter")
|
||||
}
|
||||
ParsingRecursionDepthExceeded => write!(f, "Parsing recursion depth exceeded"),
|
||||
PositionalAttributeArgumentFollowsKeywordAttributeArgument => {
|
||||
write!(f, "Positional attribute arguments cannot follow keyword attribute arguments")
|
||||
},
|
||||
Redefinition {
|
||||
first,
|
||||
first_type,
|
||||
|
|
@ -246,6 +265,9 @@ impl Display for CompileError<'_> {
|
|||
f,
|
||||
"Non-default parameter `{parameter}` follows default parameter"
|
||||
),
|
||||
UndefinedArgAttribute { argument } => {
|
||||
write!(f, "Argument attribute for unknown argument `{argument}`")
|
||||
}
|
||||
UndefinedVariable { variable } => write!(f, "Variable `{variable}` not defined"),
|
||||
UnexpectedCharacter { expected } => {
|
||||
write!(f, "Expected character {}", List::or_ticked(expected))
|
||||
|
|
@ -285,6 +307,9 @@ impl Display for CompileError<'_> {
|
|||
UnknownAliasTarget { alias, target } => {
|
||||
write!(f, "Alias `{alias}` has an unknown target `{target}`")
|
||||
}
|
||||
UnknownAttributeKeyword { attribute, keyword, } => {
|
||||
write!(f, "Unknown keyword `{keyword}` for `{attribute}` attribute")
|
||||
}
|
||||
UnknownAttribute { attribute } => write!(f, "Unknown attribute `{attribute}`"),
|
||||
UnknownDependency { recipe, unknown } => {
|
||||
write!(f, "Recipe `{recipe}` has unknown dependency `{unknown}`")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ use super::*;
|
|||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum CompileErrorKind<'src> {
|
||||
ArgumentPatternRegex {
|
||||
source: regex::Error,
|
||||
},
|
||||
AttributeArgumentCountMismatch {
|
||||
attribute: &'src str,
|
||||
found: usize,
|
||||
|
|
@ -23,6 +26,10 @@ pub(crate) enum CompileErrorKind<'src> {
|
|||
min: usize,
|
||||
max: usize,
|
||||
},
|
||||
DuplicateArgAttribute {
|
||||
arg: String,
|
||||
first: usize,
|
||||
},
|
||||
DuplicateAttribute {
|
||||
attribute: &'src str,
|
||||
first: usize,
|
||||
|
|
@ -94,6 +101,7 @@ pub(crate) enum CompileErrorKind<'src> {
|
|||
parameter: &'src str,
|
||||
},
|
||||
ParsingRecursionDepthExceeded,
|
||||
PositionalAttributeArgumentFollowsKeywordAttributeArgument,
|
||||
Redefinition {
|
||||
first: usize,
|
||||
first_type: &'static str,
|
||||
|
|
@ -106,6 +114,9 @@ pub(crate) enum CompileErrorKind<'src> {
|
|||
ShellExpansion {
|
||||
err: shellexpand::LookupError<env::VarError>,
|
||||
},
|
||||
UndefinedArgAttribute {
|
||||
argument: String,
|
||||
},
|
||||
UndefinedVariable {
|
||||
variable: &'src str,
|
||||
},
|
||||
|
|
@ -143,6 +154,10 @@ pub(crate) enum CompileErrorKind<'src> {
|
|||
UnknownAttribute {
|
||||
attribute: &'src str,
|
||||
},
|
||||
UnknownAttributeKeyword {
|
||||
attribute: &'src str,
|
||||
keyword: &'src str,
|
||||
},
|
||||
UnknownDependency {
|
||||
recipe: &'src str,
|
||||
unknown: Namepath<'src>,
|
||||
|
|
|
|||
29
src/error.rs
29
src/error.rs
|
|
@ -13,6 +13,12 @@ pub(crate) enum Error<'src> {
|
|||
min: usize,
|
||||
max: usize,
|
||||
},
|
||||
ArgumentPatternMismatch {
|
||||
argument: String,
|
||||
parameter: &'src str,
|
||||
pattern: Pattern,
|
||||
recipe: &'src str,
|
||||
},
|
||||
Assert {
|
||||
message: String,
|
||||
},
|
||||
|
|
@ -260,6 +266,13 @@ impl<'src> Error<'src> {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn source(&self) -> Option<&dyn std::error::Error> {
|
||||
match self {
|
||||
Self::Compile { compile_error } => compile_error.source(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> From<CompileError<'src>> for Error<'src> {
|
||||
|
|
@ -312,6 +325,17 @@ impl ColorDisplay for Error<'_> {
|
|||
write!(f, "Recipe `{recipe}` got {found} {count} but takes at most {max}")?;
|
||||
}
|
||||
}
|
||||
ArgumentPatternMismatch {
|
||||
argument,
|
||||
parameter,
|
||||
pattern,
|
||||
recipe,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"Argument `{argument}` passed to recipe `{recipe}` parameter `{parameter}` does not match pattern '{pattern}'",
|
||||
)?;
|
||||
}
|
||||
Assert { message }=> {
|
||||
write!(f, "Assert failed: {message}")?;
|
||||
}
|
||||
|
|
@ -546,6 +570,11 @@ impl ColorDisplay for Error<'_> {
|
|||
write!(f, "{}", token.color_display(color.error()))?;
|
||||
}
|
||||
|
||||
if let Some(source) = self.source() {
|
||||
writeln!(f)?;
|
||||
write!(f, "caused by: {source}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -336,10 +336,11 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||
}
|
||||
|
||||
pub(crate) fn evaluate_parameters(
|
||||
arguments: &[String],
|
||||
context: &ExecutionContext<'src, 'run>,
|
||||
is_dependency: bool,
|
||||
arguments: &[String],
|
||||
parameters: &[Parameter<'src>],
|
||||
recipe: &Recipe<'src>,
|
||||
scope: &'run Scope<'src, 'run>,
|
||||
) -> RunResult<'src, (Scope<'src, 'run>, Vec<String>)> {
|
||||
let mut evaluator = Self::new(context, is_dependency, scope);
|
||||
|
|
@ -373,6 +374,9 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||
rest = &rest[1..];
|
||||
value
|
||||
};
|
||||
|
||||
parameter.is_pattern_match(recipe, &value)?;
|
||||
|
||||
evaluator.scope.bind(Binding {
|
||||
constant: false,
|
||||
export: parameter.export,
|
||||
|
|
|
|||
|
|
@ -344,10 +344,11 @@ impl<'src> Justfile<'src> {
|
|||
};
|
||||
|
||||
let (outer, positional) = Evaluator::evaluate_parameters(
|
||||
arguments,
|
||||
&context,
|
||||
is_dependency,
|
||||
arguments,
|
||||
&recipe.parameters,
|
||||
recipe,
|
||||
scope,
|
||||
)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ pub(crate) use {
|
|||
alias::Alias,
|
||||
alias_style::AliasStyle,
|
||||
analyzer::Analyzer,
|
||||
arg_attribute::ArgAttribute,
|
||||
argument_parser::ArgumentParser,
|
||||
assignment::Assignment,
|
||||
assignment_resolver::AssignmentResolver,
|
||||
|
|
@ -60,6 +61,7 @@ pub(crate) use {
|
|||
parameter::Parameter,
|
||||
parameter_kind::ParameterKind,
|
||||
parser::Parser,
|
||||
pattern::Pattern,
|
||||
platform::Platform,
|
||||
platform_interface::PlatformInterface,
|
||||
position::Position,
|
||||
|
|
@ -115,7 +117,7 @@ pub(crate) use {
|
|||
snafu::{ResultExt, Snafu},
|
||||
std::{
|
||||
borrow::Cow,
|
||||
cmp,
|
||||
cmp::{self, Ordering},
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||
env,
|
||||
ffi::OsString,
|
||||
|
|
@ -186,6 +188,7 @@ pub mod request;
|
|||
mod alias;
|
||||
mod alias_style;
|
||||
mod analyzer;
|
||||
mod arg_attribute;
|
||||
mod argument_parser;
|
||||
mod assignment;
|
||||
mod assignment_resolver;
|
||||
|
|
@ -238,6 +241,7 @@ mod output_error;
|
|||
mod parameter;
|
||||
mod parameter_kind;
|
||||
mod parser;
|
||||
mod pattern;
|
||||
mod platform;
|
||||
mod platform_interface;
|
||||
mod position;
|
||||
|
|
|
|||
|
|
@ -11,12 +11,35 @@ pub(crate) struct Parameter<'src> {
|
|||
pub(crate) kind: ParameterKind,
|
||||
/// The parameter name
|
||||
pub(crate) name: Name<'src>,
|
||||
/// The parameter pattern
|
||||
pub(crate) pattern: Option<Pattern>,
|
||||
}
|
||||
|
||||
impl Parameter<'_> {
|
||||
impl<'src> Parameter<'src> {
|
||||
pub(crate) fn is_required(&self) -> bool {
|
||||
self.default.is_none() && self.kind != ParameterKind::Star
|
||||
}
|
||||
|
||||
pub(crate) fn is_pattern_match(
|
||||
&self,
|
||||
recipe: &Recipe<'src>,
|
||||
value: &str,
|
||||
) -> Result<(), Error<'src>> {
|
||||
let Some(pattern) = &self.pattern else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if pattern.is_match(value) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(Error::ArgumentPatternMismatch {
|
||||
argument: value.into(),
|
||||
parameter: self.name.lexeme(),
|
||||
pattern: pattern.clone(),
|
||||
recipe: recipe.name(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorDisplay for Parameter<'_> {
|
||||
|
|
|
|||
|
|
@ -1026,8 +1026,31 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||
|
||||
let mut positional = Vec::new();
|
||||
|
||||
let mut arg_attributes = attributes
|
||||
.iter()
|
||||
.filter_map(|attribute| {
|
||||
if let Attribute::Arg {
|
||||
name,
|
||||
name_token,
|
||||
pattern,
|
||||
..
|
||||
} = attribute
|
||||
{
|
||||
Some((
|
||||
name.cooked.clone(),
|
||||
ArgAttribute {
|
||||
name: *name_token,
|
||||
pattern: pattern.as_ref().map(|(_literal, pattern)| pattern.clone()),
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeMap<String, ArgAttribute>>();
|
||||
|
||||
while self.next_is(Identifier) || self.next_is(Dollar) {
|
||||
positional.push(self.parse_parameter(ParameterKind::Singular)?);
|
||||
positional.push(self.parse_parameter(&mut arg_attributes, ParameterKind::Singular)?);
|
||||
}
|
||||
|
||||
let kind = if self.accepted(Plus)? {
|
||||
|
|
@ -1039,7 +1062,7 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||
};
|
||||
|
||||
let variadic = if kind.is_variadic() {
|
||||
let variadic = self.parse_parameter(kind)?;
|
||||
let variadic = self.parse_parameter(&mut arg_attributes, kind)?;
|
||||
|
||||
self.forbid(Identifier, |token| {
|
||||
token.error(CompileErrorKind::ParameterFollowsVariadicParameter {
|
||||
|
|
@ -1054,6 +1077,10 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||
|
||||
self.expect(Colon)?;
|
||||
|
||||
if let Some((argument, ArgAttribute { name, .. })) = arg_attributes.into_iter().next() {
|
||||
return Err(name.error(CompileErrorKind::UndefinedArgAttribute { argument }));
|
||||
}
|
||||
|
||||
let mut dependencies = Vec::new();
|
||||
|
||||
while let Some(dependency) = self.accept_dependency()? {
|
||||
|
|
@ -1132,7 +1159,11 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||
}
|
||||
|
||||
/// Parse a recipe parameter
|
||||
fn parse_parameter(&mut self, kind: ParameterKind) -> CompileResult<'src, Parameter<'src>> {
|
||||
fn parse_parameter(
|
||||
&mut self,
|
||||
arg_attributes: &mut BTreeMap<String, ArgAttribute>,
|
||||
kind: ParameterKind,
|
||||
) -> CompileResult<'src, Parameter<'src>> {
|
||||
let export = self.accepted(Dollar)?;
|
||||
|
||||
let name = self.parse_name()?;
|
||||
|
|
@ -1148,6 +1179,9 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||
export,
|
||||
kind,
|
||||
name,
|
||||
pattern: arg_attributes
|
||||
.remove(name.lexeme())
|
||||
.and_then(|attribute| attribute.pattern),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1295,7 +1329,8 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||
|
||||
/// Item attributes, i.e., `[macos]` or `[confirm: "warning!"]`
|
||||
fn parse_attributes(&mut self) -> CompileResult<'src, Option<(Token<'src>, AttributeSet<'src>)>> {
|
||||
let mut attributes = BTreeMap::new();
|
||||
let mut arg_attributes = BTreeMap::new();
|
||||
let mut attributes = Vec::new();
|
||||
let mut discriminants = BTreeMap::new();
|
||||
|
||||
let mut token = None;
|
||||
|
|
@ -1307,29 +1342,47 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||
let name = self.parse_name()?;
|
||||
|
||||
let mut arguments = Vec::new();
|
||||
let mut keyword_arguments = BTreeMap::new();
|
||||
|
||||
if self.accepted(Colon)? {
|
||||
arguments.push(self.parse_string_literal()?);
|
||||
arguments.push(self.parse_string_literal_token()?);
|
||||
} else if self.accepted(ParenL)? {
|
||||
loop {
|
||||
arguments.push(self.parse_string_literal()?);
|
||||
if self.next_is(Identifier) {
|
||||
let name = self.parse_name()?;
|
||||
|
||||
if !self.accepted(Comma)? {
|
||||
self.expect(Equals)?;
|
||||
|
||||
let (token, value) = self.parse_string_literal_token()?;
|
||||
|
||||
keyword_arguments.insert(name.lexeme(), (name, token, value));
|
||||
} else {
|
||||
let (token, literal) = self.parse_string_literal_token()?;
|
||||
|
||||
if !keyword_arguments.is_empty() {
|
||||
return Err(token.error(
|
||||
CompileErrorKind::PositionalAttributeArgumentFollowsKeywordAttributeArgument,
|
||||
));
|
||||
}
|
||||
|
||||
arguments.push((token, literal));
|
||||
}
|
||||
|
||||
if !self.accepted(Comma)? || self.next_is(ParenR) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.expect(ParenR)?;
|
||||
}
|
||||
|
||||
let attribute = Attribute::new(name, arguments)?;
|
||||
let attribute = Attribute::new(name, arguments, keyword_arguments)?;
|
||||
|
||||
let first = attributes.get(&attribute).or_else(|| {
|
||||
if attribute.repeatable() {
|
||||
None
|
||||
} else {
|
||||
discriminants.get(&attribute.discriminant())
|
||||
}
|
||||
});
|
||||
let first = if attribute.repeatable() {
|
||||
None
|
||||
} else {
|
||||
discriminants.get(&attribute.discriminant())
|
||||
};
|
||||
|
||||
if let Some(&first) = first {
|
||||
return Err(name.error(CompileErrorKind::DuplicateAttribute {
|
||||
|
|
@ -1338,9 +1391,20 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||
}));
|
||||
}
|
||||
|
||||
if let Attribute::Arg { name: arg, .. } = &attribute {
|
||||
if let Some(&first) = arg_attributes.get(&arg.cooked) {
|
||||
return Err(name.error(CompileErrorKind::DuplicateArgAttribute {
|
||||
arg: arg.cooked.clone(),
|
||||
first,
|
||||
}));
|
||||
}
|
||||
|
||||
arg_attributes.insert(arg.cooked.clone(), name.line);
|
||||
}
|
||||
|
||||
discriminants.insert(attribute.discriminant(), name.line);
|
||||
|
||||
attributes.insert(attribute, name.line);
|
||||
attributes.push(attribute);
|
||||
|
||||
if !self.accepted(Comma)? {
|
||||
break;
|
||||
|
|
@ -1353,7 +1417,7 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||
if attributes.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some((token.unwrap(), attributes.into_keys().collect())))
|
||||
Ok(Some((token.unwrap(), attributes.into_iter().collect())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
56
src/pattern.rs
Normal file
56
src/pattern.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Pattern(pub(crate) Regex);
|
||||
|
||||
impl Pattern {
|
||||
pub(crate) fn is_match(&self, haystack: &str) -> bool {
|
||||
self.0.is_match(haystack)
|
||||
}
|
||||
|
||||
pub(crate) fn new<'src>(
|
||||
token: Token<'src>,
|
||||
literal: &StringLiteral,
|
||||
) -> Result<Self, CompileError<'src>> {
|
||||
Ok(Self(
|
||||
format!("^{}$", literal.cooked)
|
||||
.parse::<Regex>()
|
||||
.map_err(|source| token.error(CompileErrorKind::ArgumentPatternRegex { source }))?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Pattern {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{}", &self.0.as_str()[1..self.0.as_str().len() - 1])
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Pattern {}
|
||||
|
||||
impl Ord for Pattern {
|
||||
fn cmp(&self, other: &pattern::Pattern) -> Ordering {
|
||||
self.0.as_str().cmp(other.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Pattern {
|
||||
fn eq(&self, other: &pattern::Pattern) -> bool {
|
||||
self.0.as_str() == other.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Pattern {
|
||||
fn partial_cmp(&self, other: &pattern::Pattern) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Pattern {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.0.as_str())
|
||||
}
|
||||
}
|
||||
286
tests/arg_attribute.rs
Normal file
286
tests/arg_attribute.rs
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pattern_match() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[arg('bar', pattern='BAR')]
|
||||
foo bar:
|
||||
",
|
||||
)
|
||||
.args(["foo", "BAR"])
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_mismatch() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[arg('bar', pattern='BAR')]
|
||||
foo bar:
|
||||
",
|
||||
)
|
||||
.args(["foo", "bar"])
|
||||
.stderr(
|
||||
"
|
||||
error: Argument `bar` passed to recipe `foo` parameter `bar` does not match pattern 'BAR'
|
||||
",
|
||||
)
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_must_match_entire_string() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[arg('bar', pattern='bar')]
|
||||
foo bar:
|
||||
",
|
||||
)
|
||||
.args(["foo", "xbarx"])
|
||||
.stderr(
|
||||
"
|
||||
error: Argument `xbarx` passed to recipe `foo` parameter `bar` does not match pattern 'bar'
|
||||
",
|
||||
)
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_invalid_regex_error() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[arg('bar', pattern='{')]
|
||||
foo bar:
|
||||
",
|
||||
)
|
||||
.stderr(
|
||||
"
|
||||
error: Failed to parse argument pattern
|
||||
——▶ justfile:1:21
|
||||
│
|
||||
1 │ [arg('bar', pattern='{')]
|
||||
│ ^^^
|
||||
caused by: regex parse error:
|
||||
^{$
|
||||
^
|
||||
error: repetition quantifier expects a valid decimal
|
||||
",
|
||||
)
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dump() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[arg('bar', pattern='BAR')]
|
||||
foo bar:
|
||||
",
|
||||
)
|
||||
.arg("--dump")
|
||||
.stdout(
|
||||
"
|
||||
[arg('bar', pattern='BAR')]
|
||||
foo bar:
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_attribute_error() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[arg('bar', pattern='BAR')]
|
||||
[arg('bar', pattern='BAR')]
|
||||
foo bar:
|
||||
",
|
||||
)
|
||||
.args(["foo", "BAR"])
|
||||
.stderr(
|
||||
"
|
||||
error: Recipe attribute for argument `bar` first used on line 1 is duplicated on line 2
|
||||
——▶ justfile:2:2
|
||||
│
|
||||
2 │ [arg('bar', pattern='BAR')]
|
||||
│ ^^^
|
||||
",
|
||||
)
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_keyword_error() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[arg('bar', pattern='BAR', foo='foo')]
|
||||
foo bar:
|
||||
",
|
||||
)
|
||||
.args(["foo", "BAR"])
|
||||
.stderr(
|
||||
"
|
||||
error: Unknown keyword `foo` for `arg` attribute
|
||||
——▶ justfile:1:28
|
||||
│
|
||||
1 │ [arg('bar', pattern='BAR', foo='foo')]
|
||||
│ ^^^
|
||||
",
|
||||
)
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_argument_error() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[arg('bar', pattern='BAR')]
|
||||
foo:
|
||||
",
|
||||
)
|
||||
.arg("foo")
|
||||
.stderr(
|
||||
"
|
||||
error: Argument attribute for unknown argument `bar`
|
||||
——▶ justfile:1:6
|
||||
│
|
||||
1 │ [arg('bar', pattern='BAR')]
|
||||
│ ^^^^^
|
||||
",
|
||||
)
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_across_multiple_lines() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[arg(
|
||||
'bar',
|
||||
pattern='BAR'
|
||||
)]
|
||||
foo bar:
|
||||
",
|
||||
)
|
||||
.args(["foo", "BAR"])
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_trailing_comma() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[arg(
|
||||
'bar',
|
||||
pattern='BAR',
|
||||
)]
|
||||
foo bar:
|
||||
",
|
||||
)
|
||||
.args(["foo", "BAR"])
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positional_arguments_cannot_follow_keyword_arguments() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[arg(pattern='BAR', 'bar')]
|
||||
foo bar:
|
||||
",
|
||||
)
|
||||
.args(["foo", "BAR"])
|
||||
.stderr(
|
||||
"
|
||||
error: Positional attribute arguments cannot follow keyword attribute arguments
|
||||
——▶ justfile:1:21
|
||||
│
|
||||
1 │ [arg(pattern='BAR', 'bar')]
|
||||
│ ^^^^^
|
||||
",
|
||||
)
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_mismatches_are_caught_before_running_dependencies() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
baz:
|
||||
exit 1
|
||||
|
||||
[arg('bar', pattern='BAR')]
|
||||
foo bar: baz
|
||||
",
|
||||
)
|
||||
.args(["foo", "bar"])
|
||||
.stderr(
|
||||
"
|
||||
error: Argument `bar` passed to recipe `foo` parameter `bar` does not match pattern 'BAR'
|
||||
",
|
||||
)
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_mismatches_are_caught_before_running_invocation() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
baz:
|
||||
exit 1
|
||||
|
||||
[arg('bar', pattern='BAR')]
|
||||
foo bar: baz
|
||||
",
|
||||
)
|
||||
.args(["baz", "foo", "bar"])
|
||||
.stderr(
|
||||
"
|
||||
error: Argument `bar` passed to recipe `foo` parameter `bar` does not match pattern 'BAR'
|
||||
",
|
||||
)
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_mismatches_are_caught_in_evaluated_arguments() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
bar: (foo 'ba' + 'r')
|
||||
|
||||
[arg('bar', pattern='BAR')]
|
||||
foo bar:
|
||||
",
|
||||
)
|
||||
.stderr(
|
||||
"
|
||||
error: Argument `bar` passed to recipe `foo` parameter `bar` does not match pattern 'BAR'
|
||||
",
|
||||
)
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
|
@ -54,6 +54,7 @@ struct Parameter<'a> {
|
|||
export: bool,
|
||||
kind: &'a str,
|
||||
name: &'a str,
|
||||
pattern: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ mod alias_style;
|
|||
mod allow_duplicate_recipes;
|
||||
mod allow_duplicate_variables;
|
||||
mod allow_missing;
|
||||
mod arg_attribute;
|
||||
mod assert_stdout;
|
||||
mod assert_success;
|
||||
mod assertions;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue