Sitelet https://github.com/casey/just/commit/bda64471d67ab148cb8bee5fd7335bcaceaf6c0d
Skip to content

Commit bda6447

Browse files
authored
Allow [arg(pattern)] to be a list (#3484)
1 parent cc164fe commit bda6447

10 files changed

Lines changed: 126 additions & 65 deletions

File tree

‎README.md‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,10 +1357,16 @@ commands earlier in the list.
13571357

13581358
##### Attributes
13591359

1360-
The `[arg]` `flag` attribute makes the parameter a flag which does not take a
1360+
The `[arg(flag)]` attribute makes the parameter a flag which does not take a
13611361
value on the command line. For example, with `[arg('foo', long, flag)]`, `foo`
1362-
will be `"true"` when `--foo` is passed, and `[]` otherwise. Flag parameters may
1363-
not have a default.
1362+
will be `"true"` when `--foo` is passed, and `[]` otherwise. Flag parameters
1363+
may not have a default.
1364+
1365+
The value of `[arg(pattern)]` may be a list, in which case the argument is
1366+
accepted if it matches any pattern in the list. If the value is the empty list,
1367+
any argument is accepted. For example, with
1368+
`[arg('foo', pattern=['--help', '--version'])]`, `foo` may be `--help` or
1369+
`--version`.
13641370

13651371
In `[env(variable, value)]` if `value` is `[]`, `variable` is not set.
13661372
Otherwise it is set to `value` joined with spaces.

‎src/error.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -498,8 +498,8 @@ impl ColorDisplay for Error<'_> {
498498
} => {
499499
write!(
500500
f,
501-
"argument `{argument}` passed to recipe `{recipe}` parameter `{parameter}` does not match pattern '{}'",
502-
pattern.original(),
501+
"argument `{argument}` passed to recipe `{recipe}` parameter `{parameter}` does not match pattern {}",
502+
List::or_ticked(pattern.originals()),
503503
)?;
504504
}
505505
Assert { message, .. } => {

‎src/pattern.rs‎

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,63 @@ use super::*;
22

33
#[derive(Debug, Clone)]
44
pub(crate) struct Pattern {
5-
pub(crate) regex: Regex,
5+
pub(crate) regexes: Vec<Regex>,
66
}
77

88
impl Pattern {
99
pub(crate) fn is_match(&self, haystack: &str) -> bool {
10-
self.regex.is_match(haystack)
10+
self.regexes.iter().any(|regex| regex.is_match(haystack))
1111
}
1212

13-
pub(crate) fn new<'src>(value: &str, key: Name<'src>) -> Result<Self, CompileError<'src>> {
14-
value
15-
.parse::<Regex>()
16-
.map_err(|source| key.error(CompileErrorKind::ArgumentPatternRegex { source }))?;
17-
13+
pub(crate) fn new<'src>(value: &Value, key: Name<'src>) -> Result<Self, CompileError<'src>> {
1814
Ok(Self {
19-
regex: format!("^(?:{value})$")
20-
.parse()
21-
.map_err(|source| key.error(CompileErrorKind::ArgumentPatternRegex { source }))?,
15+
regexes: value
16+
.elements()
17+
.iter()
18+
.map(|element| {
19+
element
20+
.parse::<Regex>()
21+
.map_err(|source| key.error(CompileErrorKind::ArgumentPatternRegex { source }))?;
22+
23+
format!("^(?:{element})$")
24+
.parse()
25+
.map_err(|source| key.error(CompileErrorKind::ArgumentPatternRegex { source }))
26+
})
27+
.collect::<Result<Vec<Regex>, CompileError>>()?,
2228
})
2329
}
2430

25-
pub(crate) fn original(&self) -> &str {
26-
self
27-
.regex
28-
.as_str()
29-
.strip_prefix("^(?:")
30-
.unwrap()
31-
.strip_suffix(")$")
32-
.unwrap()
31+
pub(crate) fn originals(&self) -> impl Iterator<Item = &str> + Clone {
32+
self.regexes.iter().map(|regex| {
33+
regex
34+
.as_str()
35+
.strip_prefix("^(?:")
36+
.unwrap()
37+
.strip_suffix(")$")
38+
.unwrap()
39+
})
3340
}
3441
}
3542

3643
impl Eq for Pattern {}
3744

3845
impl Ord for Pattern {
3946
fn cmp(&self, other: &pattern::Pattern) -> Ordering {
40-
self.regex.as_str().cmp(other.regex.as_str())
47+
self
48+
.regexes
49+
.iter()
50+
.map(Regex::as_str)
51+
.cmp(other.regexes.iter().map(Regex::as_str))
4152
}
4253
}
4354

4455
impl PartialEq for Pattern {
4556
fn eq(&self, other: &pattern::Pattern) -> bool {
46-
self.regex.as_str() == other.regex.as_str()
57+
self
58+
.regexes
59+
.iter()
60+
.map(Regex::as_str)
61+
.eq(other.regexes.iter().map(Regex::as_str))
4762
}
4863
}
4964

@@ -58,6 +73,6 @@ impl Serialize for Pattern {
5873
where
5974
S: Serializer,
6075
{
61-
serializer.serialize_str(self.original())
76+
serializer.collect_seq(self.originals())
6277
}
6378
}

‎src/string_context.rs‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use super::*;
22

33
#[derive(Clone, Copy, Debug, PartialEq)]
44
pub(crate) enum StringContext<'src> {
5-
ArgPattern(Name<'src>),
65
EnvKey(Name<'src>),
76
Function(Name<'src>),
87
Setting(Name<'src>),
@@ -12,8 +11,7 @@ pub(crate) enum StringContext<'src> {
1211
impl<'src> StringContext<'src> {
1312
pub(crate) fn token(&self) -> Token<'src> {
1413
match self {
15-
Self::ArgPattern(name)
16-
| Self::EnvKey(name)
14+
Self::EnvKey(name)
1715
| Self::Function(name)
1816
| Self::Setting(name)
1917
| Self::WorkingDirectoryAttribute(name) => name.token,
@@ -24,7 +22,6 @@ impl<'src> StringContext<'src> {
2422
impl Display for StringContext<'_> {
2523
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
2624
match self {
27-
Self::ArgPattern(_) => write!(f, "used as `arg` attribute pattern"),
2825
Self::EnvKey(_) => write!(f, "used as `env` attribute name"),
2926
Self::Function(name) => write!(f, "passed to `{name}()`"),
3027
Self::Setting(name) => write!(f, "assigned to `{name}` setting"),

‎src/unresolved_recipe.rs‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,17 @@ impl<'src> UnresolvedRecipe<'src> {
130130
..
131131
} = &mut attribute
132132
{
133-
let value =
134-
evaluator.evaluate_string_const(expression, StringContext::ArgPattern(*key))?;
135-
let compiled = Pattern::new(&value, *key)?;
136-
self
137-
.parameters
138-
.iter_mut()
139-
.find(|parameter| parameter.name.lexeme() == arg.cooked)
140-
.unwrap()
141-
.pattern = Some(compiled.clone());
142-
*pattern = Some(compiled);
133+
let value = evaluator.evaluate_value_const(expression)?;
134+
if !value.is_empty() {
135+
let compiled = Pattern::new(&value, *key)?;
136+
self
137+
.parameters
138+
.iter_mut()
139+
.find(|parameter| parameter.name.lexeme() == arg.cooked)
140+
.unwrap()
141+
.pattern = Some(compiled.clone());
142+
*pattern = Some(compiled);
143+
}
143144
}
144145
Ok((attribute, name))
145146
})

‎src/usage.rs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,16 @@ impl ColorDisplay for UsageParameter<'_> {
181181
}
182182

183183
if let Some(pattern) = &self.parameter.pattern {
184-
write!(f, " [pattern: '{}']", pattern.original())?;
184+
write!(f, " [pattern: ")?;
185+
186+
for (i, original) in pattern.originals().enumerate() {
187+
if i > 0 {
188+
write!(f, " | ")?;
189+
}
190+
write!(f, "'{original}'")?;
191+
}
192+
193+
write!(f, "]")?;
185194
}
186195

187196
Ok(())

‎tests/arg_attribute.rs‎

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ fn pattern_mismatch() {
2525
.args(["foo", "bar"])
2626
.stderr(
2727
"
28-
error: argument `bar` passed to recipe `foo` parameter `bar` does not match pattern 'BAR'
28+
error: argument `bar` passed to recipe `foo` parameter `bar` does not match pattern `BAR`
2929
",
3030
)
3131
.failure();
@@ -43,7 +43,7 @@ fn patterns_are_regular_expressions() {
4343
.args(["foo", r"\d+"])
4444
.stderr(
4545
r"
46-
error: argument `\d+` passed to recipe `foo` parameter `bar` does not match pattern '\d+'
46+
error: argument `\d+` passed to recipe `foo` parameter `bar` does not match pattern `\d+`
4747
",
4848
)
4949
.failure();
@@ -61,7 +61,7 @@ fn pattern_must_match_entire_string() {
6161
.args(["foo", "xbarx"])
6262
.stderr(
6363
"
64-
error: argument `xbarx` passed to recipe `foo` parameter `bar` does not match pattern 'bar'
64+
error: argument `xbarx` passed to recipe `foo` parameter `bar` does not match pattern `bar`
6565
",
6666
)
6767
.failure();
@@ -105,7 +105,7 @@ fn pattern_may_be_expression() {
105105
.args(["foo", "bar"])
106106
.stderr(
107107
"
108-
error: argument `bar` passed to recipe `foo` parameter `bar` does not match pattern 'BAR'
108+
error: argument `bar` passed to recipe `foo` parameter `bar` does not match pattern `BAR`
109109
",
110110
)
111111
.failure();
@@ -154,28 +154,59 @@ fn pattern_cannot_reference_undefined_variable() {
154154
}
155155

156156
#[test]
157-
fn pattern_cannot_be_list() {
157+
fn pattern_list_match() {
158+
Test::new()
159+
.justfile(
160+
"
161+
set lists
162+
[arg('bar', pattern=['A', 'B'])]
163+
foo bar:
164+
",
165+
)
166+
.env("JUST_UNSTABLE", "1")
167+
.args(["foo", "A"])
168+
.success()
169+
.test()
170+
.env("JUST_UNSTABLE", "1")
171+
.args(["foo", "B"])
172+
.success();
173+
}
174+
175+
#[test]
176+
fn pattern_list_mismatch() {
158177
Test::new()
159178
.justfile(
160179
"
161180
set lists
162-
[arg('bar', pattern=['a', 'b'])]
181+
[arg('bar', pattern=['A', 'B'])]
163182
foo bar:
164183
",
165184
)
166185
.env("JUST_UNSTABLE", "1")
186+
.args(["foo", "C"])
167187
.stderr(
168-
r#"
169-
error: list value ["a", "b"] used as `arg` attribute pattern
170-
——▶ justfile:2:13
171-
│
172-
2 │ [arg('bar', pattern=['a', 'b'])]
173-
│ ^^^^^^^
174-
"#,
188+
"
189+
error: argument `C` passed to recipe `foo` parameter `bar` does not match pattern `A` or `B`
190+
",
175191
)
176192
.failure();
177193
}
178194

195+
#[test]
196+
fn pattern_empty_list_accepts_all_arguments() {
197+
Test::new()
198+
.justfile(
199+
"
200+
set lists
201+
[arg('bar', pattern=[])]
202+
foo bar:
203+
",
204+
)
205+
.env("JUST_UNSTABLE", "1")
206+
.args(["foo", "anything"])
207+
.success();
208+
}
209+
179210
#[test]
180211
fn pattern_cannot_reference_non_const_variable() {
181212
Test::new()
@@ -277,7 +308,7 @@ fn pattern_mismatches_are_caught_before_running_dependencies() {
277308
.args(["foo", "bar"])
278309
.stderr(
279310
"
280-
error: argument `bar` passed to recipe `foo` parameter `bar` does not match pattern 'BAR'
311+
error: argument `bar` passed to recipe `foo` parameter `bar` does not match pattern `BAR`
281312
",
282313
)
283314
.failure();
@@ -298,7 +329,7 @@ fn pattern_mismatches_are_caught_before_running_invocation() {
298329
.args(["baz", "foo", "bar"])
299330
.stderr(
300331
"
301-
error: argument `bar` passed to recipe `foo` parameter `bar` does not match pattern 'BAR'
332+
error: argument `bar` passed to recipe `foo` parameter `bar` does not match pattern `BAR`
302333
",
303334
)
304335
.failure();
@@ -317,7 +348,7 @@ fn pattern_mismatches_are_caught_in_evaluated_arguments() {
317348
)
318349
.stderr(
319350
"
320-
error: argument `bar` passed to recipe `foo` parameter `bar` does not match pattern 'BAR'
351+
error: argument `bar` passed to recipe `foo` parameter `bar` does not match pattern `BAR`
321352
",
322353
)
323354
.failure();
@@ -335,7 +366,7 @@ fn alternates_do_not_bind_to_anchors() {
335366
.args(["foo", "aa"])
336367
.stderr(
337368
"
338-
error: argument `aa` passed to recipe `foo` parameter `bar` does not match pattern 'a|b'
369+
error: argument `aa` passed to recipe `foo` parameter `bar` does not match pattern `a|b`
339370
",
340371
)
341372
.failure();
@@ -366,7 +397,7 @@ fn pattern_mismatch_variadic() {
366397
.args(["foo", "BAR", "BAR"])
367398
.stderr(
368399
"
369-
error: argument `BAR` passed to recipe `foo` parameter `bar` does not match pattern 'BAR BAR'
400+
error: argument `BAR` passed to recipe `foo` parameter `bar` does not match pattern `BAR BAR`
370401
",
371402
)
372403
.failure();

‎tests/json.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ struct Parameter<'a> {
6060
kind: &'a str,
6161
long: Option<&'a str>,
6262
name: &'a str,
63-
pattern: Option<&'a str>,
63+
pattern: Option<Vec<&'a str>>,
6464
short: Option<char>,
6565
value: Option<&'a str>,
6666
}
@@ -1258,7 +1258,7 @@ fn arg_pattern() {
12581258
"help": null,
12591259
"long": null,
12601260
"name": "bar",
1261-
"pattern": "BAR",
1261+
"pattern": ["BAR"],
12621262
"short": null,
12631263
"value": null,
12641264
}
@@ -1267,7 +1267,7 @@ fn arg_pattern() {
12671267
parameters: [Parameter {
12681268
kind: "singular",
12691269
name: "bar",
1270-
pattern: Some("BAR"),
1270+
pattern: Some(vec!["BAR"]),
12711271
..default()
12721272
}]
12731273
.into(),

‎tests/options.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,7 @@ fn value_expression_is_pattern_checked() {
755755
)
756756
.args(["foo", "--bar"])
757757
.stderr(
758-
"error: argument `baz` passed to recipe `foo` parameter `bar` does not match pattern '[0-9]+'\n",
758+
"error: argument `baz` passed to recipe `foo` parameter `bar` does not match pattern `[0-9]+`\n",
759759
)
760760
.failure();
761761
}

0 commit comments

Comments
 (0)