Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2225,7 +2225,7 @@ change their behavior.
| `[arg(ARG, pattern="PATTERN")]`<sup>1.45.0</sup> | recipe | Require values of argument `ARG` to match regular expression `PATTERN`. |
| `[arg(ARG, short="S")]`<sup>1.46.0</sup> | recipe | Require values of argument `ARG` to be passed as short `-S` option. |
| `[arg(ARG, value="VALUE")]`<sup>1.46.0</sup> | recipe | Makes option `ARG` a flag which does not take a value. |
| `[confirm(PROMPT)]`<sup>1.23.0</sup> | recipe | Require confirmation prior to executing recipe with a custom prompt. |
| `[confirm(EXPRESSION)]`<sup>1.23.0</sup> | recipe | Require confirmation prior to executing recipe with a custom prompt expression. |
| `[confirm]`<sup>1.17.0</sup> | recipe | Require confirmation prior to executing recipe. |
| `[default]`<sup>1.43.0</sup> | recipe | Use recipe as module's default recipe. |
| `[doc(DOC)]`<sup>1.27.0</sup> | module, recipe | Set recipe or module's [documentation comment](#documentation-comments) to `DOC`. |
Expand Down Expand Up @@ -2340,14 +2340,33 @@ delete-all:
#### Custom Confirmation Prompt

The default confirmation prompt can be overridden with
`[confirm(PROMPT)]`<sup>1.23.0</sup>:
`[confirm(EXPRESSION)]`<sup>1.23.0</sup>:

```just
[confirm("Are you sure you want to delete everything?")]
delete-everything:
rm -rf *
```

The confirmation prompt accepts any expression, so you can use variables,
concatenation, and function calls:

```just
target := "production"

[confirm("Deploy to " + target + "?")]
deploy:
echo 'Deploying...'
```

Recipe parameters can also be used in confirmation prompts:

```just
[confirm("Deploy to " + env + "?")]
deploy env:
echo 'Deploying to {{env}}...'
```

#### Metadata

Metadata in the form of lists of strings may be attached to recipes with the
Expand Down
47 changes: 40 additions & 7 deletions src/attribute.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
use super::*;

#[allow(clippy::large_enum_variant)]
#[derive(
EnumDiscriminants, PartialEq, Debug, Clone, Serialize, Ord, PartialOrd, Eq, IntoStaticStr,
)]
#[derive(EnumDiscriminants, PartialEq, Debug, Clone, Serialize, IntoStaticStr)]
#[strum(serialize_all = "kebab-case")]
#[serde(rename_all = "kebab-case")]
#[strum_discriminants(name(AttributeDiscriminant))]
Expand All @@ -20,7 +18,7 @@ pub(crate) enum Attribute<'src> {
short: Option<StringLiteral<'src>>,
value: Option<StringLiteral<'src>>,
},
Confirm(Option<StringLiteral<'src>>),
Confirm(Option<Expression<'src>>),
Default,
Doc(Option<StringLiteral<'src>>),
Dragonfly,
Expand Down Expand Up @@ -184,7 +182,12 @@ impl<'src> Attribute<'src> {
value,
}
}
AttributeDiscriminant::Confirm => Self::Confirm(arguments.into_iter().next()),
AttributeDiscriminant::Confirm => Self::Confirm(
arguments
.into_iter()
.next()
.map(|string_literal| Expression::StringLiteral { string_literal }),
),
AttributeDiscriminant::Default => Self::Default,
AttributeDiscriminant::Doc => Self::Doc(arguments.into_iter().next()),
AttributeDiscriminant::Dragonfly => Self::Dragonfly,
Expand Down Expand Up @@ -320,8 +323,8 @@ impl Display for Attribute<'_> {
| Self::Script(None)
| Self::Unix
| Self::Windows => {}
Self::Confirm(Some(argument))
| Self::Doc(Some(argument))
Self::Confirm(Some(argument)) => write!(f, "({argument})")?,
Self::Doc(Some(argument))
| Self::Extension(argument)
| Self::Group(argument)
| Self::WorkingDirectory(argument) => write!(f, "({argument})")?,
Expand All @@ -343,6 +346,36 @@ impl Display for Attribute<'_> {
}
}

impl Eq for Attribute<'_> {}

impl PartialOrd for Attribute<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}

impl Ord for Attribute<'_> {
fn cmp(&self, other: &Self) -> Ordering {
let disc_cmp = self.discriminant().cmp(&other.discriminant());

if disc_cmp != Ordering::Equal {
return disc_cmp;
}

// For repeatable attributes, compare inner values to distinguish them
// in BTreeSet. Non-repeatable attributes (including Confirm) appear at
// most once, so Equal is correct.
match (self, other) {
(Self::Env(k1, v1), Self::Env(k2, v2)) => (k1, v1).cmp(&(k2, v2)),
(Self::Arg { name: a, .. }, Self::Arg { name: b, .. }) | (Self::Group(a), Self::Group(b)) => {
a.cmp(b)
}
(Self::Metadata(a), Self::Metadata(b)) => a.cmp(b),
_ => Ordering::Equal,
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
12 changes: 6 additions & 6 deletions src/justfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,12 +350,6 @@ impl<'src> Justfile<'src> {
return Ok(());
}

if !config.yes && !recipe.confirm()? {
return Err(Error::NotConfirmed {
recipe: recipe.name(),
});
}

let (module, scope) = scopes
.get(recipe.module_path())
.expect("failed to retrieve scope for module");
Expand All @@ -380,6 +374,12 @@ impl<'src> Justfile<'src> {

let mut evaluator = Evaluator::new(&context, BTreeMap::new(), true, &scope);

if !config.yes && !recipe.confirm(&mut evaluator)? {
return Err(Error::NotConfirmed {
recipe: recipe.name(),
});
}

Self::run_dependencies(
config,
&context,
Expand Down
81 changes: 49 additions & 32 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1437,45 +1437,62 @@ impl<'run, 'src> Parser<'run, 'src> {
loop {
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()?);
} else if self.accepted(ParenL)? {
loop {
if self.next_is(Identifier) && !self.next_is_shell_expanded_string() {
let key = self.parse_name()?;

let value = self
.accepted(Equals)?
.then(|| self.parse_string_literal())
.transpose()?;

keyword_arguments.insert(key.lexeme(), (key, value));
let attribute = if name.lexeme() == "confirm" {
let expression = if self.accepted(Colon)? {
Some(self.parse_expression()?)
} else if self.accepted(ParenL)? {
let expr = if self.next_is(ParenR) {
None
} else {
let literal = self.parse_string_literal()?;

if !keyword_arguments.is_empty() {
return Err(
literal
.token
.error(CompileErrorKind::AttributePositionalFollowsKeyword),
);
Some(self.parse_expression()?)
};
self.expect(ParenR)?;
expr
} else {
None
};
Attribute::Confirm(expression)
} else {
let mut arguments = Vec::new();
let mut keyword_arguments = BTreeMap::new();

if self.accepted(Colon)? {
arguments.push(self.parse_string_literal()?);
} else if self.accepted(ParenL)? {
loop {
if self.next_is(Identifier) && !self.next_is_shell_expanded_string() {
let key = self.parse_name()?;

let value = self
.accepted(Equals)?
.then(|| self.parse_string_literal())
.transpose()?;

keyword_arguments.insert(key.lexeme(), (key, value));
} else {
let literal = self.parse_string_literal()?;

if !keyword_arguments.is_empty() {
return Err(
literal
.token
.error(CompileErrorKind::AttributePositionalFollowsKeyword),
);
}

arguments.push(literal);
}

arguments.push(literal);
if !self.accepted(Comma)? || self.next_is(ParenR) {
break;
}
}

if !self.accepted(Comma)? || self.next_is(ParenR) {
break;
}
self.expect(ParenR)?;
}

self.expect(ParenR)?;
}

let attribute = Attribute::new(name, arguments, keyword_arguments)?;
Attribute::new(name, arguments, keyword_arguments)?
};

let first = if attribute.repeatable() {
None
Expand Down
7 changes: 4 additions & 3 deletions src/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,11 @@ impl<'src, D> Recipe<'src, D> {
self.name.line
}

pub(crate) fn confirm(&self) -> RunResult<'src, bool> {
pub(crate) fn confirm(&self, evaluator: &mut Evaluator<'src, '_>) -> RunResult<'src, bool> {
if let Some(Attribute::Confirm(prompt)) = self.attributes.get(AttributeDiscriminant::Confirm) {
if let Some(prompt) = prompt {
eprint!("{} ", prompt.cooked);
if let Some(expression) = prompt {
let message = evaluator.evaluate_expression(expression)?;
eprint!("{message} ");
} else {
eprint!("Run recipe `{}`? ", self.name);
}
Expand Down
Loading