Add [confirm] attribute (#1723)

This commit is contained in:
Hwatwasthat 2023-11-16 23:51:34 +00:00 committed by GitHub
parent 59ea5de781
commit 9415bee16b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 180 additions and 14 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
.DS_Store .DS_Store
.idea .idea
/.vagrant /.vagrant
/.vscode
/README.html /README.html
/book/en/build /book/en/build
/book/en/src /book/en/src

View File

@ -1251,13 +1251,14 @@ Recipes may be annotated with attributes that change their behavior.
| Name | Description | | Name | Description |
| ----------------------------------- | ----------------------------------------------- | | ----------------------------------- | ----------------------------------------------- |
| `[no-cd]`<sup>1.9.0</sup> | Don't change directory before executing recipe. | | `[confirm]`<sup>master</sup> | Require confirmation prior to executing recipe. |
| `[no-exit-message]`<sup>1.7.0</sup> | Don't print an error message if recipe fails. |
| `[linux]`<sup>1.8.0</sup> | Enable recipe on Linux. | | `[linux]`<sup>1.8.0</sup> | Enable recipe on Linux. |
| `[macos]`<sup>1.8.0</sup> | Enable recipe on MacOS. | | `[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). | | `[unix]`<sup>1.8.0</sup> | Enable recipe on Unixes. (Includes MacOS). |
| `[windows]`<sup>1.8.0</sup> | Enable recipe on Windows. | | `[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: 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 `[no-cd]` prevents `just` from changing the current directory when executing
`commit`. `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 ### Command Evaluation Using Backticks
Backticks can be used to store the result of commands: Backticks can be used to store the result of commands:

View File

@ -20,7 +20,7 @@ _just() {
case "${cmd}" in case "${cmd}" in
just) 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 if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0

View File

@ -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-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 --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 --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 -n 'Print what just would do without doing it'
cand --dry-run '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' cand --highlight 'Highlight echoed recipe lines in bold'

View File

@ -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-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 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 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" -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 highlight -d 'Highlight echoed recipe lines in bold'
complete -c just -n "__fish_use_subcommand" -l no-dotenv -d 'Don\'t load `.env` file' complete -c just -n "__fish_use_subcommand" -l no-dotenv -d 'Don\'t load `.env` file'

View File

@ -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-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('--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('--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('-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('--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') [CompletionResult]::new('--highlight', 'highlight', [CompletionResultType]::ParameterName, 'Highlight echoed recipe lines in bold')

View File

@ -36,6 +36,7 @@ _just() {
'(--dotenv-path)--dotenv-filename=[Search for environment file named <DOTENV-FILENAME> instead of `.env`]' \ '(--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]' \ '--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.]' \ '--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)-n[Print what just would do without doing it]' \
'(-q --quiet)--dry-run[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]' \ '--highlight[Highlight echoed recipe lines in bold]' \

View File

@ -6,6 +6,7 @@ use super::*;
#[strum(serialize_all = "kebab-case")] #[strum(serialize_all = "kebab-case")]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub(crate) enum Attribute { pub(crate) enum Attribute {
Confirm,
Linux, Linux,
Macos, Macos,
NoCd, NoCd,

View File

@ -33,6 +33,7 @@ pub(crate) struct Config {
pub(crate) unsorted: bool, pub(crate) unsorted: bool,
pub(crate) unstable: bool, pub(crate) unstable: bool,
pub(crate) verbosity: Verbosity, pub(crate) verbosity: Verbosity,
pub(crate) yes: bool,
} }
mod cmd { mod cmd {
@ -106,6 +107,7 @@ mod arg {
pub(crate) const UNSTABLE: &str = "UNSTABLE"; pub(crate) const UNSTABLE: &str = "UNSTABLE";
pub(crate) const VERBOSE: &str = "VERBOSE"; pub(crate) const VERBOSE: &str = "VERBOSE";
pub(crate) const WORKING_DIRECTORY: &str = "WORKING-DIRECTORY"; 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_ALWAYS: &str = "always";
pub(crate) const COLOR_AUTO: &str = "auto"; pub(crate) const COLOR_AUTO: &str = "auto";
@ -168,6 +170,7 @@ impl Config {
.possible_values(arg::COMMAND_COLOR_VALUES) .possible_values(arg::COMMAND_COLOR_VALUES)
.help("Echo recipe lines in <COMMAND-COLOR>"), .help("Echo recipe lines in <COMMAND-COLOR>"),
) )
.arg(Arg::with_name(arg::YES).long("yes").help("Automatically confirm all recipes."))
.arg( .arg(
Arg::with_name(arg::DRY_RUN) Arg::with_name(arg::DRY_RUN)
.short("n") .short("n")
@ -622,14 +625,14 @@ impl Config {
Ok(Self { Ok(Self {
check: matches.is_present(arg::CHECK), 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), dry_run: matches.is_present(arg::DRY_RUN),
dump_format: Self::dump_format_from_matches(matches)?, dump_format: Self::dump_format_from_matches(matches)?,
highlight: !matches.is_present(arg::NO_HIGHLIGHT), highlight: !matches.is_present(arg::NO_HIGHLIGHT),
shell: matches.value_of(arg::SHELL).map(str::to_owned), invocation_directory,
load_dotenv: !matches.is_present(arg::NO_DOTENV),
shell_command: matches.is_present(arg::SHELL_COMMAND),
unsorted: matches.is_present(arg::UNSORTED),
unstable,
list_heading: matches list_heading: matches
.value_of(arg::LIST_HEADING) .value_of(arg::LIST_HEADING)
.unwrap_or("Available recipes:\n") .unwrap_or("Available recipes:\n")
@ -638,15 +641,16 @@ impl Config {
.value_of(arg::LIST_PREFIX) .value_of(arg::LIST_PREFIX)
.unwrap_or(" ") .unwrap_or(" ")
.to_owned(), .to_owned(),
color, load_dotenv: !matches.is_present(arg::NO_DOTENV),
command_color,
invocation_directory,
search_config, search_config,
shell: matches.value_of(arg::SHELL).map(str::to_owned),
shell_args, shell_args,
shell_command: matches.is_present(arg::SHELL_COMMAND),
subcommand, subcommand,
dotenv_filename: matches.value_of(arg::DOTENV_FILENAME).map(str::to_owned), unsorted: matches.is_present(arg::UNSORTED),
dotenv_path: matches.value_of(arg::DOTENV_PATH).map(PathBuf::from), unstable,
verbosity, verbosity,
yes: matches.is_present(arg::YES),
}) })
} }

View File

@ -88,6 +88,9 @@ pub(crate) enum Error<'src> {
function: Name<'src>, function: Name<'src>,
message: String, message: String,
}, },
GetConfirmation {
io_error: io::Error,
},
IncludeMissingPath { IncludeMissingPath {
file: PathBuf, file: PathBuf,
line: usize, line: usize,
@ -111,6 +114,9 @@ pub(crate) enum Error<'src> {
}, },
NoChoosableRecipes, NoChoosableRecipes,
NoRecipes, NoRecipes,
NotConfirmed {
recipe: &'src str,
},
RegexCompile { RegexCompile {
source: regex::Error, source: regex::Error,
}, },
@ -329,6 +335,9 @@ impl<'src> ColorDisplay for Error<'src> {
let function = function.lexeme(); let function = function.lexeme();
write!(f, "Call to function `{function}` failed: {message}")?; 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 } => { IncludeMissingPath { file: justfile, line } => {
let line = line.ordinal(); let line = line.ordinal();
let justfile = justfile.display(); let justfile = justfile.display();
@ -357,6 +366,9 @@ impl<'src> ColorDisplay for Error<'src> {
} }
NoChoosableRecipes => write!(f, "Justfile contains no choosable recipes.")?, NoChoosableRecipes => write!(f, "Justfile contains no choosable recipes.")?,
NoRecipes => write!(f, "Justfile contains no recipes.")?, NoRecipes => write!(f, "Justfile contains no recipes.")?,
NotConfirmed { recipe } => {
write!(f, "Recipe `{recipe}` was not confirmed")?;
}
RegexCompile { source } => write!(f, "{source}")?, RegexCompile { source } => write!(f, "{source}")?,
Search { search_error } => Display::fmt(search_error, f)?, Search { search_error } => Display::fmt(search_error, f)?,
Shebang { recipe, command, argument, io_error} => { Shebang { recipe, command, argument, io_error} => {

View File

@ -290,6 +290,12 @@ impl<'src> Justfile<'src> {
return Ok(()); return Ok(());
} }
if !context.config.yes && !recipe.confirm()? {
return Err(Error::NotConfirmed {
recipe: recipe.name(),
});
}
let (outer, positional) = Evaluator::evaluate_parameters( let (outer, positional) = Evaluator::evaluate_parameters(
context.config, context.config,
dotenv, dotenv,

View File

@ -63,6 +63,20 @@ impl<'src, D> Recipe<'src, D> {
self.name.line 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 { pub(crate) fn public(&self) -> bool {
!self.private && !self.attributes.contains(&Attribute::Private) !self.private && !self.attributes.contains(&Attribute::Private)
} }

105
tests/confirm.rs Normal file
View File

@ -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();
}

View File

@ -42,6 +42,7 @@ mod choose;
mod command; mod command;
mod completions; mod completions;
mod conditional; mod conditional;
mod confirm;
mod delimiters; mod delimiters;
mod dotenv; mod dotenv;
mod edit; mod edit;