use std::io::{BufRead as _, IsTerminal as _, Write as _};
use rc_core::{Error, Result};
use crate::output::Formatter;
pub(crate) struct Confirmation<'a> {
pub(crate) prompt: &'a str,
pub(crate) requires_yes: &'a str,
pub(crate) declined: &'a str,
}
pub(crate) fn confirm(request: &Confirmation<'_>, yes: bool, formatter: &Formatter) -> Result<()> {
if yes {
return Ok(());
}
if formatter.is_json() || !std::io::stdin().is_terminal() {
return Err(Error::InvalidPath(request.requires_yes.to_string()));
}
let mut stderr = std::io::stderr().lock();
write!(stderr, "{} ", request.prompt).map_err(Error::Io)?;
stderr.flush().map_err(Error::Io)?;
let mut answer = String::new();
std::io::stdin()
.lock()
.read_line(&mut answer)
.map_err(Error::Io)?;
if matches!(answer.trim().to_ascii_lowercase().as_str(), "y" | "yes") {
Ok(())
} else {
Err(Error::Interrupted(request.declined.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::OutputConfig;
fn request() -> Confirmation<'static> {
Confirmation {
prompt: "Delete everything? [y/N]",
requires_yes: "Deleting everything requires --yes in non-interactive or JSON mode",
declined: "Deleting everything was declined",
}
}
fn formatter(json: bool) -> Formatter {
Formatter::new(OutputConfig {
json,
no_color: true,
..Default::default()
})
}
#[test]
fn yes_skips_the_question_entirely() {
confirm(&request(), true, &formatter(true)).expect("--yes must be honoured");
}
#[test]
fn json_mode_refuses_instead_of_assuming_consent() {
let error = confirm(&request(), false, &formatter(true)).expect_err("must refuse");
assert!(matches!(error, Error::InvalidPath(_)), "{error:?}");
assert_eq!(error.exit_code(), 2, "a missing --yes is a usage error");
assert!(error.to_string().contains("--yes"), "{error}");
}
#[test]
fn a_declined_answer_is_reported_as_an_interruption() {
let error = Error::Interrupted(request().declined.to_string());
assert_eq!(error.exit_code(), 130);
}
}