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;