diff --git a/.gitignore b/.gitignore
index e6c81f9f4f..ec381e403c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
.DS_Store
.idea
/.vagrant
+/.vscode
/README.html
/book/en/build
/book/en/src
diff --git a/README.md b/README.md
index 3a66bad23f..6738f102de 100644
--- a/README.md
+++ b/README.md
@@ -1250,13 +1250,14 @@ Recipes may be annotated with attributes that change their behavior.
| Name | Description |
| ----------------------------------- | ----------------------------------------------- |
-| `[no-cd]`1.9.0 | Don't change directory before executing recipe. |
-| `[no-exit-message]`1.7.0 | Don't print an error message if recipe fails. |
+| `[confirm]`master | Require confirmation prior to executing recipe. |
| `[linux]`1.8.0 | Enable recipe on Linux. |
| `[macos]`1.8.0 | Enable recipe on MacOS. |
+| `[no-cd]`1.9.0 | Don't change directory before executing recipe. |
+| `[no-exit-message]`1.7.0 | Don't print an error message if recipe fails. |
+| `[private]`1.10.0 | See [Private Recipes](#private-recipes). |
| `[unix]`1.8.0 | Enable recipe on Unixes. (Includes MacOS). |
| `[windows]`1.8.0 | Enable recipe on Windows. |
-| `[private]`1.10.0 | See [Private Recipes](#private-recipes). |
A recipe can have multiple attributes, either on multiple lines:
@@ -1320,6 +1321,23 @@ Can be used with paths that are relative to the current directory, because
`[no-cd]` prevents `just` from changing the current directory when executing
`commit`.
+### Requiring Confirmation for Recipesmaster
+
+`just` normally executes all recipes unless there is an error. The `[confirm]`
+attribute allows recipes require confirmation in the terminal prior to running.
+This can be overridden by passing `--yes` to `just`, which will automatically
+confirm any recipes marked by this attribute.
+
+Recipes dependent on a recipe that requires confirmation will not be run if the
+relied upon recipe is not confirmed, as well as recipes passed after any recipe
+that requires confirmation.
+
+```just
+[confirm]
+delete all:
+ rm -rf *
+```
+
### Command Evaluation Using Backticks
Backticks can be used to store the result of commands:
@@ -1810,7 +1828,9 @@ split shebang lines differently.
Windows does not support shebang lines. On Windows, `just` splits the shebang
line into a command and arguments, saves the recipe body to a file, and invokes
the split command and arguments, adding the path to the saved recipe body as
-the final argument.
+the final argument. For example, on Windows, if a recipe starts with `#! py`,
+the final command the OS runs will be something like `py
+C:\Temp\PATH_TO_SAVED_RECIPE_BODY`.
### Safer Bash Shebang Recipes
diff --git a/completions/just.bash b/completions/just.bash
index 99d7fdb716..4647ceb05e 100644
--- a/completions/just.bash
+++ b/completions/just.bash
@@ -20,7 +20,7 @@ _just() {
case "${cmd}" in
just)
- opts=" -n -q -u -v -e -l -h -V -f -d -c -s --check --dry-run --highlight --no-dotenv --no-highlight --quiet --shell-command --clear-shell-args --unsorted --unstable --verbose --changelog --choose --dump --edit --evaluate --fmt --init --list --summary --variables --help --version --chooser --color --command-color --dump-format --list-heading --list-prefix --justfile --set --shell --shell-arg --working-directory --command --completions --show --dotenv-filename --dotenv-path ... "
+ opts=" -n -q -u -v -e -l -h -V -f -d -c -s --check --yes --dry-run --highlight --no-dotenv --no-highlight --quiet --shell-command --clear-shell-args --unsorted --unstable --verbose --changelog --choose --dump --edit --evaluate --fmt --init --list --summary --variables --help --version --chooser --color --command-color --dump-format --list-heading --list-prefix --justfile --set --shell --shell-arg --working-directory --command --completions --show --dotenv-filename --dotenv-path ... "
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
diff --git a/completions/just.elvish b/completions/just.elvish
index 1b3be811f1..51176476da 100644
--- a/completions/just.elvish
+++ b/completions/just.elvish
@@ -35,6 +35,7 @@ edit:completion:arg-completer[just] = [@words]{
cand --dotenv-filename 'Search for environment file named instead of `.env`'
cand --dotenv-path 'Load environment file at instead of searching for one'
cand --check 'Run `--fmt` in ''check'' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.'
+ cand --yes 'Automatically confirm all recipes.'
cand -n 'Print what just would do without doing it'
cand --dry-run 'Print what just would do without doing it'
cand --highlight 'Highlight echoed recipe lines in bold'
diff --git a/completions/just.fish b/completions/just.fish
index 7aedd4a81d..aa126bdd7c 100644
--- a/completions/just.fish
+++ b/completions/just.fish
@@ -52,6 +52,7 @@ complete -c just -n "__fish_use_subcommand" -s s -l show -d 'Show information ab
complete -c just -n "__fish_use_subcommand" -l dotenv-filename -d 'Search for environment file named instead of `.env`'
complete -c just -n "__fish_use_subcommand" -l dotenv-path -d 'Load environment file at instead of searching for one'
complete -c just -n "__fish_use_subcommand" -l check -d 'Run `--fmt` in \'check\' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.'
+complete -c just -n "__fish_use_subcommand" -l yes -d 'Automatically confirm all recipes.'
complete -c just -n "__fish_use_subcommand" -s n -l dry-run -d 'Print what just would do without doing it'
complete -c just -n "__fish_use_subcommand" -l highlight -d 'Highlight echoed recipe lines in bold'
complete -c just -n "__fish_use_subcommand" -l no-dotenv -d 'Don\'t load `.env` file'
diff --git a/completions/just.powershell b/completions/just.powershell
index 0842dc8c78..d2907e3c98 100644
--- a/completions/just.powershell
+++ b/completions/just.powershell
@@ -40,6 +40,7 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock {
[CompletionResult]::new('--dotenv-filename', 'dotenv-filename', [CompletionResultType]::ParameterName, 'Search for environment file named instead of `.env`')
[CompletionResult]::new('--dotenv-path', 'dotenv-path', [CompletionResultType]::ParameterName, 'Load environment file at instead of searching for one')
[CompletionResult]::new('--check', 'check', [CompletionResultType]::ParameterName, 'Run `--fmt` in ''check'' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.')
+ [CompletionResult]::new('--yes', 'yes', [CompletionResultType]::ParameterName, 'Automatically confirm all recipes.')
[CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Print what just would do without doing it')
[CompletionResult]::new('--dry-run', 'dry-run', [CompletionResultType]::ParameterName, 'Print what just would do without doing it')
[CompletionResult]::new('--highlight', 'highlight', [CompletionResultType]::ParameterName, 'Highlight echoed recipe lines in bold')
diff --git a/completions/just.zsh b/completions/just.zsh
index 4ddfa867ca..b2ee3d6591 100644
--- a/completions/just.zsh
+++ b/completions/just.zsh
@@ -36,6 +36,7 @@ _just() {
'(--dotenv-path)--dotenv-filename=[Search for environment file named instead of `.env`]' \
'--dotenv-path=[Load environment file at instead of searching for one]' \
'--check[Run `--fmt` in '\''check'\'' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.]' \
+'--yes[Automatically confirm all recipes.]' \
'(-q --quiet)-n[Print what just would do without doing it]' \
'(-q --quiet)--dry-run[Print what just would do without doing it]' \
'--highlight[Highlight echoed recipe lines in bold]' \
diff --git a/src/attribute.rs b/src/attribute.rs
index ffd532f480..b4c71665e7 100644
--- a/src/attribute.rs
+++ b/src/attribute.rs
@@ -6,6 +6,7 @@ use super::*;
#[strum(serialize_all = "kebab-case")]
#[serde(rename_all = "kebab-case")]
pub(crate) enum Attribute {
+ Confirm,
Linux,
Macos,
NoCd,
diff --git a/src/completions.rs b/src/completions.rs
index 36999ee88e..37382884cf 100644
--- a/src/completions.rs
+++ b/src/completions.rs
@@ -40,7 +40,7 @@ complete -c just -a '(__fish_just_complete_recipes)'
pub(crate) const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
(
r#" _arguments "${_arguments_options[@]}" \"#,
- r#" local common=("#,
+ r" local common=(",
),
(
r"'*--set=[Override with ]' \",
@@ -206,5 +206,5 @@ pub(crate) const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
fi
fi"#,
),
- (r#" just)"#, r#" "$1")"#),
+ (r" just)", r#" "$1")"#),
];
diff --git a/src/config.rs b/src/config.rs
index 682229bc12..9e8b7b9256 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -33,6 +33,7 @@ pub(crate) struct Config {
pub(crate) unsorted: bool,
pub(crate) unstable: bool,
pub(crate) verbosity: Verbosity,
+ pub(crate) yes: bool,
}
mod cmd {
@@ -106,6 +107,7 @@ mod arg {
pub(crate) const UNSTABLE: &str = "UNSTABLE";
pub(crate) const VERBOSE: &str = "VERBOSE";
pub(crate) const WORKING_DIRECTORY: &str = "WORKING-DIRECTORY";
+ pub(crate) const YES: &str = "YES";
pub(crate) const COLOR_ALWAYS: &str = "always";
pub(crate) const COLOR_AUTO: &str = "auto";
@@ -168,6 +170,7 @@ impl Config {
.possible_values(arg::COMMAND_COLOR_VALUES)
.help("Echo recipe lines in "),
)
+ .arg(Arg::with_name(arg::YES).long("yes").help("Automatically confirm all recipes."))
.arg(
Arg::with_name(arg::DRY_RUN)
.short("n")
@@ -622,14 +625,14 @@ impl Config {
Ok(Self {
check: matches.is_present(arg::CHECK),
+ color,
+ command_color,
+ dotenv_filename: matches.value_of(arg::DOTENV_FILENAME).map(str::to_owned),
+ dotenv_path: matches.value_of(arg::DOTENV_PATH).map(PathBuf::from),
dry_run: matches.is_present(arg::DRY_RUN),
dump_format: Self::dump_format_from_matches(matches)?,
highlight: !matches.is_present(arg::NO_HIGHLIGHT),
- shell: matches.value_of(arg::SHELL).map(str::to_owned),
- load_dotenv: !matches.is_present(arg::NO_DOTENV),
- shell_command: matches.is_present(arg::SHELL_COMMAND),
- unsorted: matches.is_present(arg::UNSORTED),
- unstable,
+ invocation_directory,
list_heading: matches
.value_of(arg::LIST_HEADING)
.unwrap_or("Available recipes:\n")
@@ -638,15 +641,16 @@ impl Config {
.value_of(arg::LIST_PREFIX)
.unwrap_or(" ")
.to_owned(),
- color,
- command_color,
- invocation_directory,
+ load_dotenv: !matches.is_present(arg::NO_DOTENV),
search_config,
+ shell: matches.value_of(arg::SHELL).map(str::to_owned),
shell_args,
+ shell_command: matches.is_present(arg::SHELL_COMMAND),
subcommand,
- dotenv_filename: matches.value_of(arg::DOTENV_FILENAME).map(str::to_owned),
- dotenv_path: matches.value_of(arg::DOTENV_PATH).map(PathBuf::from),
+ unsorted: matches.is_present(arg::UNSORTED),
+ unstable,
verbosity,
+ yes: matches.is_present(arg::YES),
})
}
diff --git a/src/error.rs b/src/error.rs
index b14cc5a207..605486972f 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -88,6 +88,9 @@ pub(crate) enum Error<'src> {
function: Name<'src>,
message: String,
},
+ GetConfirmation {
+ io_error: io::Error,
+ },
IncludeMissingPath {
file: PathBuf,
line: usize,
@@ -111,6 +114,9 @@ pub(crate) enum Error<'src> {
},
NoChoosableRecipes,
NoRecipes,
+ NotConfirmed {
+ recipe: &'src str,
+ },
RegexCompile {
source: regex::Error,
},
@@ -329,6 +335,9 @@ impl<'src> ColorDisplay for Error<'src> {
let function = function.lexeme();
write!(f, "Call to function `{function}` failed: {message}")?;
}
+ GetConfirmation { io_error } => {
+ write!(f, "Failed to read confirmation from stdin: {io_error}")?;
+ }
IncludeMissingPath { file: justfile, line } => {
let line = line.ordinal();
let justfile = justfile.display();
@@ -357,6 +366,9 @@ impl<'src> ColorDisplay for Error<'src> {
}
NoChoosableRecipes => write!(f, "Justfile contains no choosable recipes.")?,
NoRecipes => write!(f, "Justfile contains no recipes.")?,
+ NotConfirmed { recipe } => {
+ write!(f, "Recipe `{recipe}` was not confirmed")?;
+ }
RegexCompile { source } => write!(f, "{source}")?,
Search { search_error } => Display::fmt(search_error, f)?,
Shebang { recipe, command, argument, io_error} => {
diff --git a/src/justfile.rs b/src/justfile.rs
index ee46e335d5..3648c769e5 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -290,6 +290,12 @@ impl<'src> Justfile<'src> {
return Ok(());
}
+ if !context.config.yes && !recipe.confirm()? {
+ return Err(Error::NotConfirmed {
+ recipe: recipe.name(),
+ });
+ }
+
let (outer, positional) = Evaluator::evaluate_parameters(
context.config,
dotenv,
@@ -669,16 +675,7 @@ mod tests {
}
}
- macro_rules! test {
- ($name:ident, $input:expr, $expected:expr $(,)*) => {
- #[test]
- fn $name() {
- test($input, $expected);
- }
- };
- }
-
- fn test(input: &str, expected: &str) {
+ fn case(input: &str, expected: &str) {
let justfile = compile(input);
let actual = format!("{}", justfile.color_display(Color::never()));
assert_eq!(actual, expected);
@@ -688,123 +685,143 @@ mod tests {
assert_eq!(redumped, actual);
}
- test! {
- parse_empty,
- "
+ #[test]
+ fn parse_empty() {
+ case(
+ "
# hello
",
- "",
+ "",
+ );
}
- test! {
- parse_string_default,
- r#"
+ #[test]
+ fn parse_string_default() {
+ case(
+ r#"
foo a="b\t":
"#,
- r#"foo a="b\t":"#,
+ r#"foo a="b\t":"#,
+ );
}
- test! {
- parse_multiple,
- r#"
+ #[test]
+ fn parse_multiple() {
+ case(
+ r"
a:
b:
-"#,
- r#"a:
+", r"a:
-b:"#,
+b:",
+ );
}
- test! {
- parse_variadic,
- r#"
+ #[test]
+ fn parse_variadic() {
+ case(
+ r"
foo +a:
- "#,
- r#"foo +a:"#,
+ ",
+ r"foo +a:",
+ );
}
- test! {
- parse_variadic_string_default,
- r#"
+ #[test]
+ fn parse_variadic_string_default() {
+ case(
+ r#"
foo +a="Hello":
"#,
- r#"foo +a="Hello":"#,
+ r#"foo +a="Hello":"#,
+ );
}
- test! {
- parse_raw_string_default,
- r"
+ #[test]
+ fn parse_raw_string_default() {
+ case(
+ r"
foo a='b\t':
",
- r"foo a='b\t':",
+ r"foo a='b\t':",
+ );
}
- test! {
- parse_export,
- r#"
+ #[test]
+ fn parse_export() {
+ case(
+ r#"
export a := "hello"
"#,
- r#"export a := "hello""#,
+ r#"export a := "hello""#,
+ );
}
- test! {
- parse_alias_after_target,
- r#"
+ #[test]
+ fn parse_alias_after_target() {
+ case(
+ r"
foo:
echo a
alias f := foo
-"#,
-r#"alias f := foo
+",
+ r"alias f := foo
foo:
- echo a"#
+ echo a",
+ );
}
- test! {
- parse_alias_before_target,
- r#"
+ #[test]
+ fn parse_alias_before_target() {
+ case(
+ r"
alias f := foo
foo:
echo a
-"#,
-r#"alias f := foo
+",
+ r"alias f := foo
foo:
- echo a"#
+ echo a",
+ );
}
- test! {
- parse_alias_with_comment,
- r#"
+ #[test]
+ fn parse_alias_with_comment() {
+ case(
+ r"
alias f := foo #comment
foo:
echo a
-"#,
-r#"alias f := foo
+",
+ r"alias f := foo
foo:
- echo a"#
+ echo a",
+ );
}
- test! {
- parse_complex,
- "
+ #[test]
+ fn parse_complex() {
+ case(
+ "
x:
y:
z:
@@ -819,7 +836,7 @@ hello a b c : x y z #hello
2
3
",
- "bar := foo
+ "bar := foo
foo := \"xx\"
@@ -837,12 +854,14 @@ x:
y:
-z:"
+z:",
+ );
}
- test! {
- parse_shebang,
- "
+ #[test]
+ fn parse_shebang() {
+ case(
+ "
practicum := 'hello'
install:
\t#!/bin/sh
@@ -850,176 +869,195 @@ install:
\t\treturn
\tfi
",
- "practicum := 'hello'
+ "practicum := 'hello'
install:
#!/bin/sh
if [[ -f {{ practicum }} ]]; then
\treturn
fi",
+ );
}
- test! {
- parse_simple_shebang,
- "a:\n #!\n print(1)",
- "a:\n #!\n print(1)",
+ #[test]
+ fn parse_simple_shebang() {
+ case("a:\n #!\n print(1)", "a:\n #!\n print(1)");
}
- test! {
- parse_assignments,
- r#"a := "0"
+ #[test]
+ fn parse_assignments() {
+ case(
+ r#"a := "0"
c := a + b + a + b
b := "1"
"#,
- r#"a := "0"
+ r#"a := "0"
b := "1"
c := a + b + a + b"#,
+ );
}
- test! {
- parse_assignment_backticks,
- "a := `echo hello`
+ #[test]
+ fn parse_assignment_backticks() {
+ case(
+ "a := `echo hello`
c := a + b + a + b
b := `echo goodbye`",
- "a := `echo hello`
+ "a := `echo hello`
b := `echo goodbye`
c := a + b + a + b",
+ );
}
- test! {
- parse_interpolation_backticks,
- r#"a:
+ #[test]
+ fn parse_interpolation_backticks() {
+ case(
+ r#"a:
echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#,
- r#"a:
+ r#"a:
echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#,
+ );
}
- test! {
- eof_test,
- "x:\ny:\nz:\na b c: x y z",
- "a b c: x y z\n\nx:\n\ny:\n\nz:",
+ #[test]
+ fn eof_test() {
+ case("x:\ny:\nz:\na b c: x y z", "a b c: x y z\n\nx:\n\ny:\n\nz:");
}
- test! {
- string_quote_escape,
- r#"a := "hello\"""#,
- r#"a := "hello\"""#,
+ #[test]
+ fn string_quote_escape() {
+ case(r#"a := "hello\"""#, r#"a := "hello\"""#);
}
- test! {
- string_escapes,
- r#"a := "\n\t\r\"\\""#,
- r#"a := "\n\t\r\"\\""#,
+ #[test]
+ fn string_escapes() {
+ case(r#"a := "\n\t\r\"\\""#, r#"a := "\n\t\r\"\\""#);
}
- test! {
- parameters,
- "a b c:
+ #[test]
+ fn parameters() {
+ case(
+ "a b c:
{{b}} {{c}}",
- "a b c:
+ "a b c:
{{ b }} {{ c }}",
+ );
}
- test! {
- unary_functions,
- "
+ #[test]
+ fn unary_functions() {
+ case(
+ "
x := arch()
a:
{{os()}} {{os_family()}} {{num_cpus()}}",
- "x := arch()
+ "x := arch()
a:
{{ os() }} {{ os_family() }} {{ num_cpus() }}",
+ );
}
- test! {
- env_functions,
- r#"
+ #[test]
+ fn env_functions() {
+ case(
+ r#"
x := env_var('foo',)
a:
{{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var("baz"))}}"#,
- r#"x := env_var('foo')
+ r#"x := env_var('foo')
a:
{{ env_var_or_default('foo' + 'bar', 'baz') }} {{ env_var(env_var("baz")) }}"#,
+ );
}
- test! {
- parameter_default_string,
- r#"
+ #[test]
+ fn parameter_default_string() {
+ case(
+ r#"
f x="abc":
"#,
- r#"f x="abc":"#,
+ r#"f x="abc":"#,
+ );
}
- test! {
- parameter_default_raw_string,
- r#"
+ #[test]
+ fn parameter_default_raw_string() {
+ case(
+ r"
f x='abc':
-"#,
- r#"f x='abc':"#,
+",
+ r"f x='abc':",
+ );
}
- test! {
- parameter_default_backtick,
- r#"
+ #[test]
+ fn parameter_default_backtick() {
+ case(
+ r"
f x=`echo hello`:
-"#,
- r#"f x=`echo hello`:"#,
+",
+ r"f x=`echo hello`:",
+ );
}
- test! {
- parameter_default_concatenation_string,
- r#"
+ #[test]
+ fn parameter_default_concatenation_string() {
+ case(
+ r#"
f x=(`echo hello` + "foo"):
"#,
- r#"f x=(`echo hello` + "foo"):"#,
+ r#"f x=(`echo hello` + "foo"):"#,
+ );
}
- test! {
- parameter_default_concatenation_variable,
- r#"
+ #[test]
+ fn parameter_default_concatenation_variable() {
+ case(
+ r#"
x := "10"
f y=(`echo hello` + x) +z="foo":
"#,
- r#"x := "10"
+ r#"x := "10"
f y=(`echo hello` + x) +z="foo":"#,
+ );
}
- test! {
- parameter_default_multiple,
- r#"
+ #[test]
+ fn parameter_default_multiple() {
+ case(
+ r#"
x := "10"
f y=(`echo hello` + x) +z=("foo" + "bar"):
"#,
- r#"x := "10"
+ r#"x := "10"
f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
+ );
}
- test! {
- concatenation_in_group,
- "x := ('0' + '1')",
- "x := ('0' + '1')",
+ #[test]
+ fn concatenation_in_group() {
+ case("x := ('0' + '1')", "x := ('0' + '1')");
}
- test! {
- string_in_group,
- "x := ('0' )",
- "x := ('0')",
+ #[test]
+ fn string_in_group() {
+ case("x := ('0' )", "x := ('0')");
}
#[rustfmt::skip]
- test! {
- escaped_dos_newlines,
- "@spam:\r
+ #[test]
+ fn escaped_dos_newlines() {
+ case("@spam:\r
\t{ \\\r
\t\tfiglet test; \\\r
\t\tcargo build --color always 2>&1; \\\r
@@ -1031,6 +1069,6 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
\tfiglet test; \\
\tcargo build --color always 2>&1; \\
\tcargo test --color always -- --color always 2>&1; \\
- } | less",
+ } | less");
}
}
diff --git a/src/parser.rs b/src/parser.rs
index 81ae3c19df..15590c08cd 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -1020,11 +1020,11 @@ mod tests {
test! {
name: recipe_named_alias,
- text: r#"
+ text: r"
[private]
alias:
echo 'echoing alias'
- "#,
+ ",
tree: (justfile
(recipe alias (body ("echo 'echoing alias'")))
),
@@ -1154,13 +1154,13 @@ mod tests {
test! {
name: recipe_plus_variadic,
- text: r#"foo +bar:"#,
+ text: r"foo +bar:",
tree: (justfile (recipe foo (params +(bar)))),
}
test! {
name: recipe_star_variadic,
- text: r#"foo *bar:"#,
+ text: r"foo *bar:",
tree: (justfile (recipe foo (params *(bar)))),
}
@@ -1172,13 +1172,13 @@ mod tests {
test! {
name: recipe_variadic_variable_default,
- text: r#"foo +bar=baz:"#,
+ text: r"foo +bar=baz:",
tree: (justfile (recipe foo (params +(bar baz)))),
}
test! {
name: recipe_variadic_addition_group_default,
- text: r#"foo +bar=(baz + bob):"#,
+ text: r"foo +bar=(baz + bob):",
tree: (justfile (recipe foo (params +(bar ((+ baz bob)))))),
}
@@ -1440,9 +1440,9 @@ mod tests {
test! {
name: recipe_variadic_with_default_after_default,
- text: r#"
+ text: r"
f a=b +c=d:
- "#,
+ ",
tree: (justfile (recipe f (params (a b) +(c d)))),
}
diff --git a/src/recipe.rs b/src/recipe.rs
index 407115d735..4ea8bc2b62 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -63,6 +63,20 @@ impl<'src, D> Recipe<'src, D> {
self.name.line
}
+ 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)
+ }
+ }
+
pub(crate) fn public(&self) -> bool {
!self.private && !self.attributes.contains(&Attribute::Private)
}
diff --git a/tests/confirm.rs b/tests/confirm.rs
new file mode 100644
index 0000000000..8b37489c41
--- /dev/null
+++ b/tests/confirm.rs
@@ -0,0 +1,105 @@
+use super::*;
+
+#[test]
+fn confirm_recipe_arg() {
+ Test::new()
+ .arg("--yes")
+ .justfile(
+ "
+ [confirm]
+ requires_confirmation:
+ echo confirmed
+ ",
+ )
+ .stderr("echo confirmed\n")
+ .stdout("confirmed\n")
+ .run();
+}
+
+#[test]
+fn recipe_with_confirm_recipe_dependency_arg() {
+ Test::new()
+ .arg("--yes")
+ .justfile(
+ "
+ dep_confirmation: requires_confirmation
+ echo confirmed2
+
+ [confirm]
+ requires_confirmation:
+ echo confirmed
+ ",
+ )
+ .stderr("echo confirmed\necho confirmed2\n")
+ .stdout("confirmed\nconfirmed2\n")
+ .run();
+}
+
+#[test]
+fn confirm_recipe() {
+ Test::new()
+ .justfile(
+ "
+ [confirm]
+ requires_confirmation:
+ echo confirmed
+ ",
+ )
+ .stderr("Run recipe `requires_confirmation`? echo confirmed\n")
+ .stdout("confirmed\n")
+ .stdin("y")
+ .run();
+}
+
+#[test]
+fn recipe_with_confirm_recipe_dependency() {
+ Test::new()
+ .justfile(
+ "
+ dep_confirmation: requires_confirmation
+ echo confirmed2
+
+ [confirm]
+ requires_confirmation:
+ echo confirmed
+ ",
+ )
+ .stderr("Run recipe `requires_confirmation`? echo confirmed\necho confirmed2\n")
+ .stdout("confirmed\nconfirmed2\n")
+ .stdin("y")
+ .run();
+}
+
+#[test]
+fn do_not_confirm_recipe() {
+ Test::new()
+ .justfile(
+ "
+ [confirm]
+ requires_confirmation:
+ echo confirmed
+ ",
+ )
+ .stderr("Run recipe `requires_confirmation`? error: Recipe `requires_confirmation` was not confirmed\n")
+ .stdout("")
+ .status(1)
+ .run();
+}
+
+#[test]
+fn do_not_confirm_recipe_with_confirm_recipe_dependency() {
+ Test::new()
+ .justfile(
+ "
+ dep_confirmation: requires_confirmation
+ echo mistake
+
+ [confirm]
+ requires_confirmation:
+ echo confirmed
+ ",
+ )
+ .stderr("Run recipe `requires_confirmation`? error: Recipe `requires_confirmation` was not confirmed\n")
+ .status(1)
+ .run();
+}
diff --git a/tests/lib.rs b/tests/lib.rs
index 37b1aff7cb..ea7d63c1b2 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -42,6 +42,7 @@ mod choose;
mod command;
mod completions;
mod conditional;
+mod confirm;
mod delimiters;
mod dotenv;
mod edit;