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

Reply via email to