Add [confirm]
attribute (#1723)
This commit is contained in:
parent
59ea5de781
commit
9415bee16b
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
/.vagrant
|
||||
/.vscode
|
||||
/README.html
|
||||
/book/en/build
|
||||
/book/en/src
|
||||
|
24
README.md
24
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:
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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')
|
||||
|
@ -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]' \
|
||||
|
@ -6,6 +6,7 @@ use super::*;
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub(crate) enum Attribute {
|
||||
Confirm,
|
||||
Linux,
|
||||
Macos,
|
||||
NoCd,
|
||||
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
|
12
src/error.rs
12
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} => {
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
105
tests/confirm.rs
Normal file
105
tests/confirm.rs
Normal 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();
|
||||
}
|
@ -42,6 +42,7 @@ mod choose;
|
||||
mod command;
|
||||
mod completions;
|
||||
mod conditional;
|
||||
mod confirm;
|
||||
mod delimiters;
|
||||
mod dotenv;
|
||||
mod edit;
|
||||
|
Loading…
Reference in New Issue
Block a user