Skip to content

Commit c172bd5

Browse files
authored
Allow long defaults to parameter name (#3041)
1 parent f15825b commit c172bd5

File tree

7 files changed

+181
-32
lines changed

7 files changed

+181
-32
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2842,6 +2842,14 @@ $ just foo --bar=hello
28422842
bar=hello
28432843
```
28442844

2845+
The value of `long` can be omitted, in which case the option defaults to the
2846+
name of the parameter:
2847+
2848+
```just
2849+
[arg("bar", long)]
2850+
foo bar:
2851+
```
2852+
28452853
The `[arg(ARG, short=OPTION)]`<sup>master</sup> attribute can be used to make a
28462854
parameter a short option.
28472855

src/attribute.rs

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub(crate) enum Attribute<'src> {
1313
Arg {
1414
help: Option<StringLiteral<'src>>,
1515
long: Option<StringLiteral<'src>>,
16+
#[serde(skip)]
17+
long_key: Option<Token<'src>>,
1618
name: StringLiteral<'src>,
1719
pattern: Option<Pattern<'src>>,
1820
short: Option<StringLiteral<'src>>,
@@ -91,7 +93,7 @@ impl<'src> Attribute<'src> {
9193
pub(crate) fn new(
9294
name: Name<'src>,
9395
arguments: Vec<StringLiteral<'src>>,
94-
mut keyword_arguments: BTreeMap<&'src str, (Name<'src>, StringLiteral<'src>)>,
96+
mut keyword_arguments: BTreeMap<&'src str, (Name<'src>, Option<StringLiteral<'src>>)>,
9597
) -> CompileResult<'src, Self> {
9698
let discriminant = name
9799
.lexeme()
@@ -117,25 +119,29 @@ impl<'src> Attribute<'src> {
117119

118120
let attribute = match discriminant {
119121
AttributeDiscriminant::Arg => {
120-
let name = arguments.into_iter().next().unwrap();
122+
let arg = arguments.into_iter().next().unwrap();
121123

122-
let long = keyword_arguments
124+
let (long, long_key) = keyword_arguments
123125
.remove("long")
124-
.map(|(_name, literal)| {
125-
Self::check_option_name(&name, &literal)?;
126-
Ok(literal)
126+
.map(|(name, literal)| {
127+
if let Some(literal) = literal {
128+
Self::check_option_name(&arg, &literal)?;
129+
Ok((Some(literal), None))
130+
} else {
131+
Ok((Some(arg.clone()), Some(*name)))
132+
}
127133
})
128-
.transpose()?;
134+
.transpose()?
135+
.unwrap_or((None, None));
129136

130-
let short = keyword_arguments
131-
.remove("short")
132-
.map(|(_name, literal)| {
133-
Self::check_option_name(&name, &literal)?;
137+
let short = Self::remove_required(&mut keyword_arguments, "short")?
138+
.map(|(_key, literal)| {
139+
Self::check_option_name(&arg, &literal)?;
134140

135141
if literal.cooked.chars().count() != 1 {
136142
return Err(literal.token.error(
137143
CompileErrorKind::ShortOptionWithMultipleCharacters {
138-
parameter: name.cooked.clone(),
144+
parameter: arg.cooked.clone(),
139145
},
140146
));
141147
}
@@ -144,29 +150,27 @@ impl<'src> Attribute<'src> {
144150
})
145151
.transpose()?;
146152

147-
let pattern = keyword_arguments
148-
.remove("pattern")
149-
.map(|(_name, literal)| Pattern::new(&literal))
153+
let pattern = Self::remove_required(&mut keyword_arguments, "pattern")?
154+
.map(|(_key, literal)| Pattern::new(&literal))
150155
.transpose()?;
151156

152-
let value = keyword_arguments
153-
.remove("value")
154-
.map(|(name, literal)| {
157+
let value = Self::remove_required(&mut keyword_arguments, "value")?
158+
.map(|(key, literal)| {
155159
if long.is_none() && short.is_none() {
156-
return Err(name.error(CompileErrorKind::ArgAttributeValueRequiresOption));
160+
return Err(key.error(CompileErrorKind::ArgAttributeValueRequiresOption));
157161
}
158162
Ok(literal)
159163
})
160164
.transpose()?;
161165

162-
let help = keyword_arguments
163-
.remove("help")
164-
.map(|(_name, literal)| literal);
166+
let help =
167+
Self::remove_required(&mut keyword_arguments, "help")?.map(|(_key, literal)| literal);
165168

166169
Self::Arg {
167170
help,
168171
long,
169-
name,
172+
long_key,
173+
name: arg,
170174
pattern,
171175
short,
172176
value,
@@ -214,6 +218,20 @@ impl<'src> Attribute<'src> {
214218
Ok(attribute)
215219
}
216220

221+
fn remove_required(
222+
keyword_arguments: &mut BTreeMap<&'src str, (Name<'src>, Option<StringLiteral<'src>>)>,
223+
key: &'src str,
224+
) -> CompileResult<'src, Option<(Name<'src>, StringLiteral<'src>)>> {
225+
let Some((key, literal)) = keyword_arguments.remove(key) else {
226+
return Ok(None);
227+
};
228+
229+
let literal =
230+
literal.ok_or_else(|| key.error(CompileErrorKind::AttributeKeyMissingValue { key }))?;
231+
232+
Ok(Some((key, literal)))
233+
}
234+
217235
pub(crate) fn discriminant(&self) -> AttributeDiscriminant {
218236
self.into()
219237
}
@@ -238,6 +256,7 @@ impl Display for Attribute<'_> {
238256
Self::Arg {
239257
help,
240258
long,
259+
long_key: _,
241260
name,
242261
pattern,
243262
short,

src/compile_error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,12 @@ impl Display for CompileError<'_> {
337337
UnknownAliasTarget { alias, target } => {
338338
write!(f, "Alias `{alias}` has an unknown target `{target}`")
339339
}
340+
AttributeKeyMissingValue { key } => {
341+
write!(
342+
f,
343+
"Attribute key `{key}` requires value",
344+
)
345+
}
340346
UnknownAttributeKeyword { attribute, keyword } => {
341347
write!(f, "Unknown keyword `{keyword}` for `{attribute}` attribute")
342348
}

src/compile_error_kind.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ pub(crate) enum CompileErrorKind<'src> {
1212
min: usize,
1313
max: usize,
1414
},
15+
AttributeKeyMissingValue {
16+
key: Name<'src>,
17+
},
1518
AttributePositionalFollowsKeyword,
1619
BacktickShebang,
1720
CircularRecipeDependency {

src/parser.rs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,7 @@ impl<'run, 'src> Parser<'run, 'src> {
10221022
let Attribute::Arg {
10231023
help,
10241024
long,
1025+
long_key,
10251026
name: arg,
10261027
pattern,
10271028
short,
@@ -1034,10 +1035,14 @@ impl<'run, 'src> Parser<'run, 'src> {
10341035

10351036
if let Some(option) = long {
10361037
if !longs.insert(&option.cooked) {
1037-
return Err(option.token.error(CompileErrorKind::DuplicateOption {
1038-
option: Switch::Long(option.cooked.clone()),
1039-
recipe: name.lexeme(),
1040-
}));
1038+
return Err(
1039+
long_key
1040+
.unwrap_or(option.token)
1041+
.error(CompileErrorKind::DuplicateOption {
1042+
option: Switch::Long(option.cooked.clone()),
1043+
recipe: name.lexeme(),
1044+
}),
1045+
);
10411046
}
10421047
}
10431048

@@ -1385,13 +1390,14 @@ impl<'run, 'src> Parser<'run, 'src> {
13851390
} else if self.accepted(ParenL)? {
13861391
loop {
13871392
if self.next_is(Identifier) && !self.next_is_shell_expanded_string() {
1388-
let name = self.parse_name()?;
1389-
1390-
self.expect(Equals)?;
1393+
let key = self.parse_name()?;
13911394

1392-
let value = self.parse_string_literal()?;
1395+
let value = self
1396+
.accepted(Equals)?
1397+
.then(|| self.parse_string_literal())
1398+
.transpose()?;
13931399

1394-
keyword_arguments.insert(name.lexeme(), (name, value));
1400+
keyword_arguments.insert(key.lexeme(), (key, value));
13951401
} else {
13961402
let literal = self.parse_string_literal()?;
13971403

tests/arg_attribute.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,3 +354,69 @@ fn pattern_mismatch_variadic() {
354354
.status(EXIT_FAILURE)
355355
.run();
356356
}
357+
358+
#[test]
359+
fn pattern_requires_value() {
360+
Test::new()
361+
.justfile(
362+
"
363+
[arg('bar', pattern)]
364+
foo bar:
365+
",
366+
)
367+
.stderr(
368+
"
369+
error: Attribute key `pattern` requires value
370+
——▶ justfile:1:13
371+
372+
1 │ [arg('bar', pattern)]
373+
│ ^^^^^^^
374+
",
375+
)
376+
.status(EXIT_FAILURE)
377+
.run();
378+
}
379+
380+
#[test]
381+
fn short_requires_value() {
382+
Test::new()
383+
.justfile(
384+
"
385+
[arg('bar', short)]
386+
foo bar:
387+
",
388+
)
389+
.stderr(
390+
"
391+
error: Attribute key `short` requires value
392+
——▶ justfile:1:13
393+
394+
1 │ [arg('bar', short)]
395+
│ ^^^^^
396+
",
397+
)
398+
.status(EXIT_FAILURE)
399+
.run();
400+
}
401+
402+
#[test]
403+
fn value_requires_value() {
404+
Test::new()
405+
.justfile(
406+
"
407+
[arg('bar', long, value)]
408+
foo bar:
409+
",
410+
)
411+
.stderr(
412+
"
413+
error: Attribute key `value` requires value
414+
——▶ justfile:1:19
415+
416+
1 │ [arg('bar', long, value)]
417+
│ ^^^^^
418+
",
419+
)
420+
.status(EXIT_FAILURE)
421+
.run();
422+
}

tests/options.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,21 @@ fn parameters_may_be_passed_with_long_options() {
8484
.run();
8585
}
8686

87+
#[test]
88+
fn long_option_defaults_to_parameter_name() {
89+
Test::new()
90+
.justfile(
91+
"
92+
[arg('bar', long)]
93+
@foo bar:
94+
echo bar={{bar}}
95+
",
96+
)
97+
.args(["foo", "--bar", "baz"])
98+
.stdout("bar=baz\n")
99+
.run();
100+
}
101+
87102
#[test]
88103
fn parameters_may_be_passed_with_short_options() {
89104
Test::new()
@@ -172,6 +187,32 @@ fn duplicate_long_option_attributes_are_forbidden() {
172187
.run();
173188
}
174189

190+
#[test]
191+
fn defaulted_duplicate_long_option() {
192+
Test::new()
193+
.justfile(
194+
"
195+
[arg(
196+
'aaa',
197+
long='bar'
198+
)]
199+
[arg( 'bar', long)]
200+
foo aaa bar:
201+
",
202+
)
203+
.stderr(
204+
"
205+
error: Recipe `foo` defines option `--bar` multiple times
206+
——▶ justfile:5:19
207+
208+
5 │ [arg( 'bar', long)]
209+
│ ^^^^
210+
",
211+
)
212+
.status(EXIT_FAILURE)
213+
.run();
214+
}
215+
175216
#[test]
176217
fn duplicate_short_option_attributes_are_forbidden() {
177218
Test::new()

0 commit comments

Comments
 (0)