mirror of
https://github.com/casey/just.git
synced 2024-11-26 11:43:43 +03:00
Allow setting custom confirm prompt (#1834)
This commit is contained in:
parent
5bbc89b718
commit
8bd411de45
@ -104,9 +104,11 @@ string : STRING
|
||||
sequence : expression ',' sequence
|
||||
| expression ','?
|
||||
|
||||
recipe : attribute? '@'? NAME parameter* variadic? ':' dependency* body?
|
||||
recipe : attributes* '@'? NAME parameter* variadic? ':' dependency* body?
|
||||
|
||||
attribute : '[' NAME ']' eol
|
||||
attributes : '[' attribute* ']' eol
|
||||
|
||||
attribute : NAME ( '(' string ')' )?
|
||||
|
||||
parameter : '$'? NAME
|
||||
| '$'? NAME '=' value
|
||||
|
11
README.md
11
README.md
@ -1456,6 +1456,7 @@ Recipes may be annotated with attributes that change their behavior.
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| `[confirm]`<sup>1.17.0</sup> | Require confirmation prior to executing recipe. |
|
||||
| `[confirm("prompt")]`<sup>master</sup> | Require confirmation prior to executing recipe with a custom prompt. |
|
||||
| `[linux]`<sup>1.8.0</sup> | Enable recipe on Linux. |
|
||||
| `[macos]`<sup>1.8.0</sup> | Enable recipe on MacOS. |
|
||||
| `[no-cd]`<sup>1.9.0</sup> | Don't change directory before executing recipe. |
|
||||
@ -1544,6 +1545,16 @@ delete all:
|
||||
rm -rf *
|
||||
```
|
||||
|
||||
#### Custom Confirmation Prompt<sup>master</sup>
|
||||
|
||||
The default confirmation prompt can be overridden with `[confirm(PROMPT)]`:
|
||||
|
||||
```just
|
||||
[confirm("Are you sure you want to delete everything?")]
|
||||
delete-everything:
|
||||
rm -rf *
|
||||
```
|
||||
|
||||
### Command Evaluation Using Backticks
|
||||
|
||||
Backticks can be used to store the result of commands:
|
||||
|
@ -3,7 +3,7 @@ use super::*;
|
||||
/// An alias, e.g. `name := target`
|
||||
#[derive(Debug, PartialEq, Clone, Serialize)]
|
||||
pub(crate) struct Alias<'src, T = Rc<Recipe<'src>>> {
|
||||
pub(crate) attributes: BTreeSet<Attribute>,
|
||||
pub(crate) attributes: BTreeSet<Attribute<'src>>,
|
||||
pub(crate) name: Name<'src>,
|
||||
#[serde(
|
||||
bound(serialize = "T: Keyed<'src>"),
|
||||
|
@ -211,11 +211,11 @@ impl<'src> Analyzer<'src> {
|
||||
fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src> {
|
||||
let name = alias.name.lexeme();
|
||||
|
||||
for attr in &alias.attributes {
|
||||
if *attr != Attribute::Private {
|
||||
for attribute in &alias.attributes {
|
||||
if *attribute != Attribute::Private {
|
||||
return Err(alias.name.token.error(AliasInvalidAttribute {
|
||||
alias: name,
|
||||
attr: *attr,
|
||||
attribute: attribute.clone(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(
|
||||
EnumString, PartialEq, Debug, Copy, Clone, Serialize, Ord, PartialOrd, Eq, IntoStaticStr,
|
||||
)]
|
||||
#[derive(EnumString, PartialEq, Debug, Clone, Serialize, Ord, PartialOrd, Eq, IntoStaticStr)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub(crate) enum Attribute {
|
||||
Confirm,
|
||||
pub(crate) enum Attribute<'src> {
|
||||
Confirm(Option<StringLiteral<'src>>),
|
||||
Linux,
|
||||
Macos,
|
||||
NoCd,
|
||||
@ -17,14 +15,45 @@ pub(crate) enum Attribute {
|
||||
Windows,
|
||||
}
|
||||
|
||||
impl Attribute {
|
||||
pub(crate) fn from_name(name: Name) -> Option<Attribute> {
|
||||
impl<'src> Attribute<'src> {
|
||||
pub(crate) fn from_name(name: Name) -> Option<Self> {
|
||||
name.lexeme().parse().ok()
|
||||
}
|
||||
|
||||
pub(crate) fn to_str(self) -> &'static str {
|
||||
pub(crate) fn name(&self) -> &'static str {
|
||||
self.into()
|
||||
}
|
||||
|
||||
pub(crate) fn with_argument(
|
||||
self,
|
||||
name: Name<'src>,
|
||||
argument: StringLiteral<'src>,
|
||||
) -> CompileResult<'src, Self> {
|
||||
match self {
|
||||
Self::Confirm(_) => Ok(Self::Confirm(Some(argument))),
|
||||
_ => Err(name.error(CompileErrorKind::UnexpectedAttributeArgument { attribute: self })),
|
||||
}
|
||||
}
|
||||
|
||||
fn argument(&self) -> Option<&StringLiteral> {
|
||||
if let Self::Confirm(prompt) = self {
|
||||
prompt.as_ref()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> Display for Attribute<'src> {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
write!(f, "{}", self.name())?;
|
||||
|
||||
if let Some(argument) = self.argument() {
|
||||
write!(f, "({argument})")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -32,7 +61,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(Attribute::NoExitMessage.to_str(), "no-exit-message");
|
||||
fn name() {
|
||||
assert_eq!(Attribute::NoExitMessage.name(), "no-exit-message");
|
||||
}
|
||||
}
|
||||
|
@ -32,11 +32,13 @@ impl Display for CompileError<'_> {
|
||||
use CompileErrorKind::*;
|
||||
|
||||
match &*self.kind {
|
||||
AliasInvalidAttribute { alias, attr } => write!(
|
||||
f,
|
||||
"Alias {alias} has an invalid attribute `{}`",
|
||||
attr.to_str(),
|
||||
),
|
||||
AliasInvalidAttribute { alias, attribute } => {
|
||||
write!(
|
||||
f,
|
||||
"Alias `{alias}` has invalid attribute `{}`",
|
||||
attribute.name(),
|
||||
)
|
||||
}
|
||||
AliasShadowsRecipe { alias, recipe_line } => write!(
|
||||
f,
|
||||
"Alias `{alias}` defined on line {} shadows recipe `{alias}` defined on line {}",
|
||||
@ -209,6 +211,13 @@ impl Display for CompileError<'_> {
|
||||
"Non-default parameter `{parameter}` follows default parameter"
|
||||
),
|
||||
UndefinedVariable { variable } => write!(f, "Variable `{variable}` not defined"),
|
||||
UnexpectedAttributeArgument { attribute } => {
|
||||
write!(
|
||||
f,
|
||||
"Attribute `{}` specified with argument but takes no arguments",
|
||||
attribute.name(),
|
||||
)
|
||||
}
|
||||
UnexpectedCharacter { expected } => write!(f, "Expected character `{expected}`"),
|
||||
UnexpectedClosingDelimiter { close } => {
|
||||
write!(f, "Unexpected closing delimiter `{}`", close.close())
|
||||
|
@ -4,7 +4,7 @@ use super::*;
|
||||
pub(crate) enum CompileErrorKind<'src> {
|
||||
AliasInvalidAttribute {
|
||||
alias: &'src str,
|
||||
attr: Attribute,
|
||||
attribute: Attribute<'src>,
|
||||
},
|
||||
AliasShadowsRecipe {
|
||||
alias: &'src str,
|
||||
@ -85,6 +85,9 @@ pub(crate) enum CompileErrorKind<'src> {
|
||||
UndefinedVariable {
|
||||
variable: &'src str,
|
||||
},
|
||||
UnexpectedAttributeArgument {
|
||||
attribute: Attribute<'src>,
|
||||
},
|
||||
UnexpectedCharacter {
|
||||
expected: char,
|
||||
},
|
||||
|
@ -435,7 +435,7 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
/// Parse an alias, e.g `alias name := target`
|
||||
fn parse_alias(
|
||||
&mut self,
|
||||
attributes: BTreeSet<Attribute>,
|
||||
attributes: BTreeSet<Attribute<'src>>,
|
||||
) -> CompileResult<'src, Alias<'src, Name<'src>>> {
|
||||
self.presume_keyword(Keyword::Alias)?;
|
||||
let name = self.parse_name()?;
|
||||
@ -672,7 +672,7 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
&mut self,
|
||||
doc: Option<&'src str>,
|
||||
quiet: bool,
|
||||
attributes: BTreeSet<Attribute>,
|
||||
attributes: BTreeSet<Attribute<'src>>,
|
||||
) -> CompileResult<'src, UnresolvedRecipe<'src>> {
|
||||
let name = self.parse_name()?;
|
||||
|
||||
@ -905,7 +905,7 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
}
|
||||
|
||||
/// Parse recipe attributes
|
||||
fn parse_attributes(&mut self) -> CompileResult<'src, Option<BTreeSet<Attribute>>> {
|
||||
fn parse_attributes(&mut self) -> CompileResult<'src, Option<BTreeSet<Attribute<'src>>>> {
|
||||
let mut attributes = BTreeMap::new();
|
||||
|
||||
while self.accepted(BracketL)? {
|
||||
@ -922,6 +922,15 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
first: *line,
|
||||
}));
|
||||
}
|
||||
|
||||
let attribute = if self.accepted(ParenL)? {
|
||||
let argument = self.parse_string_literal()?;
|
||||
self.expect(ParenR)?;
|
||||
attribute.with_argument(name, argument)?
|
||||
} else {
|
||||
attribute
|
||||
};
|
||||
|
||||
attributes.insert(attribute, name.line);
|
||||
|
||||
if !self.accepted(Comma)? {
|
||||
|
@ -13,11 +13,13 @@ pub(crate) struct DisplayRange<T>(T);
|
||||
impl Display for DisplayRange<&Range<usize>> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
if self.0.start == self.0.end {
|
||||
write!(f, "0")?;
|
||||
} else if self.0.start == self.0.end - 1 {
|
||||
write!(f, "{}", self.0.start)?;
|
||||
} else if self.0.end == usize::MAX {
|
||||
write!(f, "{} or more", self.0.start)?;
|
||||
} else {
|
||||
write!(f, "{} to {}", self.0.start, self.0.end)?;
|
||||
write!(f, "{} to {}", self.0.start, self.0.end - 1)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -47,15 +49,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn exclusive() {
|
||||
assert!(!(0..0).range_contains(&0));
|
||||
assert!(!(0..0).range_contains(&0));
|
||||
assert!(!(1..10).range_contains(&0));
|
||||
assert!(!(1..10).range_contains(&10));
|
||||
assert!(!(1..10).range_contains(&0));
|
||||
assert!(!(1..10).range_contains(&10));
|
||||
assert!((0..1).range_contains(&0));
|
||||
assert!((0..1).range_contains(&0));
|
||||
assert!((10..20).range_contains(&15));
|
||||
assert!((10..20).range_contains(&15));
|
||||
}
|
||||
|
||||
@ -75,8 +73,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
assert_eq!((1..1).display().to_string(), "1");
|
||||
assert_eq!((1..2).display().to_string(), "1 to 2");
|
||||
assert!(!(1..1).contains(&1));
|
||||
assert!((1..1).is_empty());
|
||||
assert!((5..5).is_empty());
|
||||
assert_eq!((1..1).display().to_string(), "0");
|
||||
assert_eq!((1..2).display().to_string(), "1");
|
||||
assert_eq!((5..6).display().to_string(), "5");
|
||||
assert_eq!((5..10).display().to_string(), "5 to 9");
|
||||
assert_eq!((1..usize::MAX).display().to_string(), "1 or more");
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ fn error_from_signal(recipe: &str, line_number: Option<usize>, exit_status: Exit
|
||||
/// A recipe, e.g. `foo: bar baz`
|
||||
#[derive(PartialEq, Debug, Clone, Serialize)]
|
||||
pub(crate) struct Recipe<'src, D = Dependency<'src>> {
|
||||
pub(crate) attributes: BTreeSet<Attribute>,
|
||||
pub(crate) attributes: BTreeSet<Attribute<'src>>,
|
||||
pub(crate) body: Vec<Line<'src>>,
|
||||
pub(crate) dependencies: Vec<D>,
|
||||
#[serde(skip)]
|
||||
@ -71,17 +71,22 @@ impl<'src, D> Recipe<'src, D> {
|
||||
}
|
||||
|
||||
pub(crate) fn confirm(&self) -> RunResult<'src, bool> {
|
||||
if self.attributes.contains(&Attribute::Confirm) {
|
||||
eprint!("Run recipe `{}`? ", self.name);
|
||||
let mut line = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut line)
|
||||
.map_err(|io_error| Error::GetConfirmation { io_error })?;
|
||||
let line = line.trim().to_lowercase();
|
||||
Ok(line == "y" || line == "yes")
|
||||
} else {
|
||||
Ok(true)
|
||||
for attribute in &self.attributes {
|
||||
if let Attribute::Confirm(prompt) = attribute {
|
||||
if let Some(prompt) = prompt {
|
||||
eprint!("{} ", prompt.cooked);
|
||||
} else {
|
||||
eprint!("Run recipe `{}`? ", self.name);
|
||||
}
|
||||
let mut line = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut line)
|
||||
.map_err(|io_error| Error::GetConfirmation { io_error })?;
|
||||
let line = line.trim().to_lowercase();
|
||||
return Ok(line == "y" || line == "yes");
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn check_can_be_default_recipe(&self) -> RunResult<'src, ()> {
|
||||
@ -423,7 +428,7 @@ impl<'src, D: Display> ColorDisplay for Recipe<'src, D> {
|
||||
}
|
||||
|
||||
for attribute in &self.attributes {
|
||||
writeln!(f, "[{}]", attribute.to_str())?;
|
||||
writeln!(f, "[{attribute}]")?;
|
||||
}
|
||||
|
||||
if self.quiet {
|
||||
|
@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
#[derive(PartialEq, Debug, Clone, Ord, Eq, PartialOrd)]
|
||||
pub(crate) struct StringLiteral<'src> {
|
||||
pub(crate) kind: StringKind,
|
||||
pub(crate) raw: &'src str,
|
||||
|
@ -72,7 +72,7 @@ fn multiple_attributes_one_line_error_message() {
|
||||
)
|
||||
.stderr(
|
||||
"
|
||||
error: Expected ']' or ',', but found identifier
|
||||
error: Expected ']', ',', or '(', but found identifier
|
||||
——▶ justfile:1:17
|
||||
│
|
||||
1 │ [macos, windows linux]
|
||||
@ -106,3 +106,26 @@ fn multiple_attributes_one_line_duplicate_check() {
|
||||
.status(1)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unexpected_attribute_argument() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[private('foo')]
|
||||
foo:
|
||||
exit 1
|
||||
",
|
||||
)
|
||||
.stderr(
|
||||
"
|
||||
error: Attribute `private` specified with argument but takes no arguments
|
||||
——▶ justfile:1:2
|
||||
│
|
||||
1 │ [private('foo')]
|
||||
│ ^^^^^^^
|
||||
",
|
||||
)
|
||||
.status(1)
|
||||
.run();
|
||||
}
|
||||
|
@ -103,3 +103,49 @@ fn do_not_confirm_recipe_with_confirm_recipe_dependency() {
|
||||
.status(1)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_recipe_with_prompt() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[confirm(\"This is dangerous - are you sure you want to run it?\")]
|
||||
requires_confirmation:
|
||||
echo confirmed
|
||||
",
|
||||
)
|
||||
.stderr("This is dangerous - are you sure you want to run it? echo confirmed\n")
|
||||
.stdout("confirmed\n")
|
||||
.stdin("y")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_recipe_with_prompt_too_many_args() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[confirm(\"This is dangerous - are you sure you want to run it?\",\"this second argument is not supported\")]
|
||||
requires_confirmation:
|
||||
echo confirmed
|
||||
",
|
||||
)
|
||||
.stderr("error: Expected ')', but found ','\n ——▶ justfile:1:64\n │\n1 │ [confirm(\"This is dangerous - are you sure you want to run it?\",\"this second argument is not supported\")]\n │ ^\n")
|
||||
.stdout("")
|
||||
.status(1)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_attribute_is_formatted_correctly() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[confirm('prompt')]
|
||||
foo:
|
||||
",
|
||||
)
|
||||
.arg("--dump")
|
||||
.stdout("[confirm('prompt')]\nfoo:\n")
|
||||
.run();
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ test! {
|
||||
name: invalid_alias_attribute,
|
||||
justfile: "[private]\n[linux]\nalias t := test\n\ntest:\n",
|
||||
stderr: "
|
||||
error: Alias t has an invalid attribute `linux`
|
||||
error: Alias `t` has invalid attribute `linux`
|
||||
——▶ justfile:3:7
|
||||
│
|
||||
3 │ alias t := test
|
||||
|
Loading…
Reference in New Issue
Block a user