summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHwatwasthat <chris.j.bury@googlemail.com>2023-11-16 23:51:34 +0000
committerGitHub <noreply@github.com>2023-11-16 23:51:34 +0000
commit9415bee16bb44ac716aaf7ff09136645d61ad5e6 (patch)
tree522183d3d325292162ac04bbde98740206f17802
parent59ea5de7817cbd714670f297e2ec6e0272c16542 (diff)
Add `[confirm]` attribute (#1723)
-rw-r--r--.gitignore1
-rw-r--r--README.md24
-rw-r--r--completions/just.bash2
-rw-r--r--completions/just.elvish1
-rw-r--r--completions/just.fish1
-rw-r--r--completions/just.powershell1
-rw-r--r--completions/just.zsh1
-rw-r--r--src/attribute.rs1
-rw-r--r--src/config.rs24
-rw-r--r--src/error.rs12
-rw-r--r--src/justfile.rs6
-rw-r--r--src/recipe.rs14
-rw-r--r--tests/confirm.rs105
-rw-r--r--tests/lib.rs1
14 files changed, 180 insertions, 14 deletions
diff --git a/.gitignore b/.gitignore
index e6c81f9f..ec381e40 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 09368f02..23cee3e4 100644
--- a/README.md
+++ b/README.md
@@ -1251,13 +1251,14 @@ Recipes may be annotated with attributes that change their behavior.
| Name | Description |
| ----------------------------------- | ----------------------------------------------- |
-| `[no-cd]`<sup>1.9.0</sup> | Don't change directory before executing recipe. |
-| `[no-exit-message]`<sup>1.7.0</sup> | Don't print an error message if recipe fails. |
+| `[confirm]`<sup>master</sup> | Require confirmation prior to executing recipe. |
| `[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. |
+| `[no-exit-message]`<sup>1.7.0</sup> | Don't print an error message if recipe fails. |
+| `[private]`<sup>1.10.0</sup> | See [Private Recipes](#private-recipes). |
| `[unix]`<sup>1.8.0</sup> | Enable recipe on Unixes. (Includes MacOS). |
| `[windows]`<sup>1.8.0</sup> | Enable recipe on Windows. |
-| `[private]`<sup>1.10.0</sup> | See [Private Recipes](#private-recipes). |
A recipe can have multiple attributes, either on multiple lines:
@@ -1321,6 +1322,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 Recipes<sup>master</sup>
+
+`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:
diff --git a/completions/just.bash b/completions/just.bash
index 99d7fdb7..4647ceb0 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 <ARGUMENTS>... "
+ 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 <ARGUMENTS>... "
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
diff --git a/completions/just.elvish b/completions/just.elvish
index 1b3be811..51176476 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 <DOTENV-FILENAME> instead of `.env`'
cand --dotenv-path 'Load environment file at <DOTENV-PATH> 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 7aedd4a8..aa126bdd 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 <DOTENV-FILENAME> instead of `.env`'
complete -c just -n "__fish_use_subcommand" -l dotenv-path -d 'Load environment file at <DOTENV-PATH> 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 0842dc8c..d2907e3c 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 <DOTENV-FILENAME> instead of `.env`')
[CompletionResult]::new('--dotenv-path', 'dotenv-path', [CompletionResultType]::ParameterName, 'Load environment file at <DOTENV-PATH> 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 4ddfa867..b2ee3d65 100644
--- a/completions/just.zsh
+++ b/completions/just.zsh
@@ -36,6 +36,7 @@ _just() {
'(--dotenv-path)--dotenv-filename=[Search for environment file named <DOTENV-FILENAME> instead of `.env`]' \
'--dotenv-path=[Load environment file at <DOTENV-PATH> 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 ffd532f4..b4c71665 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/config.rs b/src/config.rs
index 682229bc..9e8b7b92 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 <COMMAND-COLOR>"),
)
+ .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 b14cc5a2..60548697 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 4d59c525..3648c769 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,
diff --git a/src/recipe.rs b/src/recipe.rs
index 407115d7..4ea8bc2b 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 00000000..8b37489c
--- /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 37b1aff7..ea7d63c1 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;