Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package just for openSUSE:Factory checked in at 2025-12-12 21:42:54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/just (Old) and /work/SRC/openSUSE:Factory/.just.new.1939 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "just" Fri Dec 12 21:42:54 2025 rev:35 rq:1322618 version:1.45.0 Changes: -------- --- /work/SRC/openSUSE:Factory/just/just.changes 2025-12-10 15:33:54.222790298 +0100 +++ /work/SRC/openSUSE:Factory/.just.new.1939/just.changes 2025-12-12 21:44:20.008820453 +0100 @@ -1,0 +2,10 @@ +Fri Dec 12 13:42:28 UTC 2025 - Richard Rahl <[email protected]> + +- Update to version 1.45.0: + * Allow requiring recipe arguments to match regular expression patterns + * Allow shell-expanded strings in attributes + * Fix arg pattern anchoring + * Use non-capturing group in arg pattern regex + * Remove redundant type annotation + +------------------------------------------------------------------- Old: ---- just-1.44.1.tar.gz New: ---- just-1.45.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ just.spec ++++++ --- /var/tmp/diff_new_pack.ioIdEU/_old 2025-12-12 21:44:20.744851507 +0100 +++ /var/tmp/diff_new_pack.ioIdEU/_new 2025-12-12 21:44:20.748851676 +0100 @@ -18,7 +18,7 @@ %bcond_with tests Name: just -Version: 1.44.1 +Version: 1.45.0 Release: 0 Summary: Commmand runner License: (Apache-2.0 OR MIT) AND Unicode-DFS-2016 AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT) AND (MIT OR Unlicense) AND Apache-2.0 AND BSD-3-Clause AND CC0-1.0 AND MIT AND CC0-1.0 ++++++ just-1.44.1.tar.gz -> just-1.45.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/.github/workflows/release.yaml new/just-1.45.0/.github/workflows/release.yaml --- old/just-1.44.1/.github/workflows/release.yaml 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/.github/workflows/release.yaml 2025-12-10 21:46:07.000000000 +0100 @@ -207,6 +207,6 @@ uses: peaceiris/actions-gh-pages@v4 if: ${{ !needs.prerelease.outputs.value }} with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} publish_branch: gh-pages publish_dir: www diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/CHANGELOG.md new/just-1.45.0/CHANGELOG.md --- old/just-1.44.1/CHANGELOG.md 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/CHANGELOG.md 2025-12-10 21:46:07.000000000 +0100 @@ -1,6 +1,20 @@ Changelog ========= +[1.45.0](https://github.com/casey/just/releases/tag/1.45.0) - 2025-12-10 +------------------------------------------------------------------------ + +### Added +- Allow requiring recipe arguments to match regular expression patterns ([#3000](https://github.com/casey/just/pull/3000) by [casey](https://github.com/casey)) + +### Fixed +- Allow shell-expanded strings in attributes ([#3007](https://github.com/casey/just/pull/3007) by [casey](https://github.com/casey)) +- Fix arg pattern anchoring ([#3002](https://github.com/casey/just/pull/3002) by [casey](https://github.com/casey)) + +### Misc +- Use non-capturing group in arg pattern regex ([#3006](https://github.com/casey/just/pull/3006) by [casey](https://github.com/casey)) +- Remove redundant type annotation ([#3004](https://github.com/casey/just/pull/3004) by [casey](https://github.com/casey)) + [1.44.1](https://github.com/casey/just/releases/tag/1.44.1) - 2025-12-09 ------------------------------------------------------------------------ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/Cargo.lock new/just-1.45.0/Cargo.lock --- old/just-1.44.1/Cargo.lock 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/Cargo.lock 2025-12-10 21:46:07.000000000 +0100 @@ -552,7 +552,7 @@ [[package]] name = "just" -version = "1.44.1" +version = "1.45.0" dependencies = [ "ansi_term", "blake3", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/Cargo.toml new/just-1.45.0/Cargo.toml --- old/just-1.44.1/Cargo.toml 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/Cargo.toml 2025-12-10 21:46:07.000000000 +0100 @@ -1,6 +1,6 @@ [package] name = "just" -version = "1.44.1" +version = "1.45.0" authors = ["Casey Rodarmor <[email protected]>"] autotests = false categories = ["command-line-utilities", "development-tools"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/README.md new/just-1.45.0/README.md --- old/just-1.44.1/README.md 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/README.md 2025-12-10 21:46:07.000000000 +0100 @@ -2730,6 +2730,32 @@ echo $bar ``` +Parameters may be constrained to match regular expression patterns using the +`[arg("name", pattern="pattern")]` attribute<sup>1.45.0</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: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/src/arg_attribute.rs new/just-1.45.0/src/arg_attribute.rs --- old/just-1.44.1/src/arg_attribute.rs 1970-01-01 01:00:00.000000000 +0100 +++ new/just-1.45.0/src/arg_attribute.rs 2025-12-10 21:46:07.000000000 +0100 @@ -0,0 +1,6 @@ +use super::*; + +pub(crate) struct ArgAttribute<'src> { + pub(crate) name: Token<'src>, + pub(crate) pattern: Option<Pattern>, +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/src/argument_parser.rs new/just-1.45.0/src/argument_parser.rs --- old/just-1.44.1/src/argument_parser.rs 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/src/argument_parser.rs 2025-12-10 21:46:07.000000000 +0100 @@ -91,6 +91,10 @@ let arguments = rest[..argument_count].to_vec(); + for (argument, parameter) in arguments.iter().zip(&recipe.parameters) { + parameter.check_pattern_match(recipe, argument)?; + } + self.next += argument_count; Ok(ArgumentGroup { arguments, path }) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/src/attribute.rs new/just-1.45.0/src/attribute.rs --- old/just-1.44.1/src/attribute.rs 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/src/attribute.rs 2025-12-10 21:46:07.000000000 +0100 @@ -9,6 +9,12 @@ #[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 @@ 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 @@ | 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<'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 @@ ); } - 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 @@ 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 @@ } pub(crate) fn repeatable(&self) -> bool { - matches!(self, Attribute::Group(_) | Attribute::Metadata(_)) + matches!( + self, + Attribute::Arg { .. } | Attribute::Group(_) | Attribute::Metadata(_), + ) } } @@ -133,6 +171,15 @@ 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) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/src/compile_error.rs new/just-1.45.0/src/compile_error.rs --- old/just-1.44.1/src/compile_error.rs 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/src/compile_error.rs 2025-12-10 21:46:07.000000000 +0100 @@ -17,6 +17,13 @@ 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 @@ use CompileErrorKind::*; match &*self.kind { + ArgumentPatternRegex { .. } => { + write!(f, "Failed to parse argument pattern") + } AttributeArgumentCountMismatch { attribute, found, @@ -53,6 +63,9 @@ write!(f, "at most {max} {}", Count("argument", *max)) } } + AttributePositionalFollowsKeyword => { + write!(f, "Positional attribute arguments cannot follow keyword attribute arguments") + }, BacktickShebang => write!(f, "Backticks may not start with `#!`"), CircularRecipeDependency { recipe, circle } => { if circle.len() == 2 { @@ -97,6 +110,12 @@ 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 {}", @@ -246,6 +265,9 @@ f, "Non-default parameter `{parameter}` follows default parameter" ), + UndefinedArgAttribute { argument } => { + write!(f, "Argument attribute for undefined argument `{argument}`") + } UndefinedVariable { variable } => write!(f, "Variable `{variable}` not defined"), UnexpectedCharacter { expected } => { write!(f, "Expected character {}", List::or_ticked(expected)) @@ -285,6 +307,9 @@ 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}`") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/src/compile_error_kind.rs new/just-1.45.0/src/compile_error_kind.rs --- old/just-1.44.1/src/compile_error_kind.rs 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/src/compile_error_kind.rs 2025-12-10 21:46:07.000000000 +0100 @@ -2,12 +2,16 @@ #[derive(Debug, PartialEq)] pub(crate) enum CompileErrorKind<'src> { + ArgumentPatternRegex { + source: regex::Error, + }, AttributeArgumentCountMismatch { attribute: &'src str, found: usize, min: usize, max: usize, }, + AttributePositionalFollowsKeyword, BacktickShebang, CircularRecipeDependency { recipe: &'src str, @@ -23,6 +27,10 @@ min: usize, max: usize, }, + DuplicateArgAttribute { + arg: String, + first: usize, + }, DuplicateAttribute { attribute: &'src str, first: usize, @@ -106,6 +114,9 @@ ShellExpansion { err: shellexpand::LookupError<env::VarError>, }, + UndefinedArgAttribute { + argument: String, + }, UndefinedVariable { variable: &'src str, }, @@ -143,6 +154,10 @@ UnknownAttribute { attribute: &'src str, }, + UnknownAttributeKeyword { + attribute: &'src str, + keyword: &'src str, + }, UnknownDependency { recipe: &'src str, unknown: Namepath<'src>, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/src/error.rs new/just-1.45.0/src/error.rs --- old/just-1.44.1/src/error.rs 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/src/error.rs 2025-12-10 21:46:07.000000000 +0100 @@ -13,6 +13,12 @@ min: usize, max: usize, }, + ArgumentPatternMismatch { + argument: String, + parameter: &'src str, + pattern: Pattern, + recipe: &'src str, + }, Assert { message: String, }, @@ -260,6 +266,13 @@ } ) } + + 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 @@ 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 @@ write!(f, "{}", token.color_display(color.error()))?; } + if let Some(source) = self.source() { + writeln!(f)?; + write!(f, "caused by: {source}")?; + } + Ok(()) } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/src/evaluator.rs new/just-1.45.0/src/evaluator.rs --- old/just-1.44.1/src/evaluator.rs 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/src/evaluator.rs 2025-12-10 21:46:07.000000000 +0100 @@ -336,10 +336,11 @@ } 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 @@ rest = &rest[1..]; value }; + + parameter.check_pattern_match(recipe, &value)?; + evaluator.scope.bind(Binding { constant: false, export: parameter.export, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/src/justfile.rs new/just-1.45.0/src/justfile.rs --- old/just-1.44.1/src/justfile.rs 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/src/justfile.rs 2025-12-10 21:46:07.000000000 +0100 @@ -224,7 +224,7 @@ let groups = ArgumentParser::parse_arguments(self, &arguments)?; - let mut invocations = Vec::<Invocation>::new(); + let mut invocations = Vec::new(); for group in &groups { invocations.push(self.invocation(&group.arguments, &group.path, 0)?); @@ -344,10 +344,11 @@ }; let (outer, positional) = Evaluator::evaluate_parameters( + arguments, &context, is_dependency, - arguments, &recipe.parameters, + recipe, scope, )?; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/src/lib.rs new/just-1.45.0/src/lib.rs --- old/just-1.44.1/src/lib.rs 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/src/lib.rs 2025-12-10 21:46:07.000000000 +0100 @@ -9,6 +9,7 @@ alias::Alias, alias_style::AliasStyle, analyzer::Analyzer, + arg_attribute::ArgAttribute, argument_parser::ArgumentParser, assignment::Assignment, assignment_resolver::AssignmentResolver, @@ -60,6 +61,7 @@ parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, + pattern::Pattern, platform::Platform, platform_interface::PlatformInterface, position::Position, @@ -115,7 +117,7 @@ snafu::{ResultExt, Snafu}, std::{ borrow::Cow, - cmp, + cmp::{self, Ordering}, collections::{BTreeMap, BTreeSet, HashMap, HashSet}, env, ffi::OsString, @@ -186,6 +188,7 @@ mod alias; mod alias_style; mod analyzer; +mod arg_attribute; mod argument_parser; mod assignment; mod assignment_resolver; @@ -238,6 +241,7 @@ mod parameter; mod parameter_kind; mod parser; +mod pattern; mod platform; mod platform_interface; mod position; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/src/parameter.rs new/just-1.45.0/src/parameter.rs --- old/just-1.44.1/src/parameter.rs 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/src/parameter.rs 2025-12-10 21:46:07.000000000 +0100 @@ -1,22 +1,39 @@ use super::*; -/// A single function parameter #[derive(PartialEq, Debug, Clone, Serialize)] pub(crate) struct Parameter<'src> { - /// An optional default expression pub(crate) default: Option<Expression<'src>>, - /// Export parameter as environment variable pub(crate) export: bool, - /// The kind of parameter pub(crate) kind: ParameterKind, - /// The parameter name pub(crate) name: Name<'src>, + 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 check_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<'_> { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/src/parser.rs new/just-1.45.0/src/parser.rs --- old/just-1.44.1/src/parser.rs 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/src/parser.rs 2025-12-10 21:46:07.000000000 +0100 @@ -1026,8 +1026,31 @@ 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 @@ }; 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 @@ 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 @@ } /// 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 @@ export, kind, name, + pattern: arg_attributes + .remove(name.lexeme()) + .and_then(|attribute| attribute.pattern), }) } @@ -1295,7 +1329,8 @@ /// 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,45 @@ 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) && !self.next_is_shell_expanded_string() { + let name = self.parse_name()?; + + 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::AttributePositionalFollowsKeyword)); + } - if !self.accepted(Comma)? { + 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 +1389,20 @@ })); } + 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 +1415,7 @@ if attributes.is_empty() { Ok(None) } else { - Ok(Some((token.unwrap(), attributes.into_keys().collect()))) + Ok(Some((token.unwrap(), attributes.into_iter().collect()))) } } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/src/pattern.rs new/just-1.45.0/src/pattern.rs --- old/just-1.44.1/src/pattern.rs 1970-01-01 01:00:00.000000000 +0100 +++ new/just-1.45.0/src/pattern.rs 2025-12-10 21:46:07.000000000 +0100 @@ -0,0 +1,61 @@ +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>> { + literal + .cooked + .parse::<Regex>() + .map_err(|source| token.error(CompileErrorKind::ArgumentPatternRegex { source }))?; + + 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()[4..self.0.as_str().len() - 2]) + } +} + +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()) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/tests/arg_attribute.rs new/just-1.45.0/tests/arg_attribute.rs --- old/just-1.44.1/tests/arg_attribute.rs 1970-01-01 01:00:00.000000000 +0100 +++ new/just-1.45.0/tests/arg_attribute.rs 2025-12-10 21:46:07.000000000 +0100 @@ -0,0 +1,324 @@ +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 patterns_are_regulare_expressions() { + Test::new() + .justfile( + r" + [arg('bar', pattern='\d+')] + foo bar: + ", + ) + .args(["foo", r"\d+"]) + .stderr( + r" + error: Argument `\d+` passed to recipe `foo` parameter `bar` does not match pattern '\d+' + ", + ) + .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 operator missing expression + ", + ) + .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 undefined 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(); +} + +#[test] +fn alternates_do_not_bind_to_anchors() { + Test::new() + .justfile( + " + [arg('bar', pattern='a|b')] + foo bar: + ", + ) + .args(["foo", "aa"]) + .stderr( + " + error: Argument `aa` passed to recipe `foo` parameter `bar` does not match pattern 'a|b' + ", + ) + .status(EXIT_FAILURE) + .run(); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/tests/attributes.rs new/just-1.45.0/tests/attributes.rs --- old/just-1.44.1/tests/attributes.rs 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/tests/attributes.rs 2025-12-10 21:46:07.000000000 +0100 @@ -311,3 +311,15 @@ .status(EXIT_FAILURE) .run(); } + +#[test] +fn shell_expanded_strings_can_be_used_in_attributes() { + Test::new() + .justfile( + " + [doc(x'foo')] + bar: + ", + ) + .run(); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/tests/json.rs new/just-1.45.0/tests/json.rs --- old/just-1.44.1/tests/json.rs 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/tests/json.rs 2025-12-10 21:46:07.000000000 +0100 @@ -54,6 +54,7 @@ export: bool, kind: &'a str, name: &'a str, + pattern: Option<&'a str>, } #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/just-1.44.1/tests/lib.rs new/just-1.45.0/tests/lib.rs --- old/just-1.44.1/tests/lib.rs 2025-12-09 09:30:03.000000000 +0100 +++ new/just-1.45.0/tests/lib.rs 2025-12-10 21:46:07.000000000 +0100 @@ -45,6 +45,7 @@ mod allow_duplicate_recipes; mod allow_duplicate_variables; mod allow_missing; +mod arg_attribute; mod assert_stdout; mod assert_success; mod assertions; ++++++ vendor.tar.zst ++++++ /work/SRC/openSUSE:Factory/just/vendor.tar.zst /work/SRC/openSUSE:Factory/.just.new.1939/vendor.tar.zst differ: char 7, line 1
