use std::io::{BufRead, IsTerminal, Read, Write};
use anyhow::{Result, bail};
pub fn confirm(prompt: &str, assume_yes: bool) -> Result<()> {
let stdin = std::io::stdin();
confirm_with(prompt, assume_yes, stdin.is_terminal(), &mut stdin.lock())
}
fn confirm_with<R: BufRead>(
prompt: &str,
assume_yes: bool,
interactive: bool,
reader: &mut R,
) -> Result<()> {
if assume_yes {
return Ok(());
}
if !interactive {
bail!("refusing to prompt on a non-interactive stdin; pass --yes to confirm");
}
let mut stderr = std::io::stderr();
write!(stderr, "{prompt} [y/N] ")?;
stderr.flush()?;
let mut answer = String::new();
reader.read_line(&mut answer)?;
match answer.trim().to_ascii_lowercase().as_str() {
"y" | "yes" => Ok(()),
_ => bail!("aborted"),
}
}
pub fn resolve_secret_value(argument: Option<&str>, label: &str) -> Result<String> {
match argument {
Some(value) => reject_empty(value.to_string(), label),
None => {
let mut buffer = String::new();
std::io::stdin().read_to_string(&mut buffer)?;
reject_empty(strip_trailing_newline(&buffer).to_string(), label)
}
}
}
fn strip_trailing_newline(raw: &str) -> &str {
raw.strip_suffix('\n')
.map_or(raw, |s| s.strip_suffix('\r').unwrap_or(s))
}
fn reject_empty(value: String, label: &str) -> Result<String> {
if value.is_empty() {
bail!("{label} must not be empty");
}
Ok(value)
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use super::*;
fn answer(input: &str) -> Result<()> {
let mut reader = Cursor::new(input.as_bytes().to_vec());
confirm_with("Delete?", false, true, &mut reader)
}
#[test]
fn assume_yes_skips_the_prompt() {
let mut reader = Cursor::new(Vec::new());
assert!(confirm_with("Delete?", true, false, &mut reader).is_ok());
}
#[test]
fn non_interactive_stdin_is_refused() {
let mut reader = Cursor::new(b"y\n".to_vec());
let err = confirm_with("Delete?", false, false, &mut reader).unwrap_err();
assert!(err.to_string().contains("--yes"), "{err}");
}
#[test]
fn affirmative_answers_are_accepted() {
assert!(answer("y\n").is_ok());
assert!(answer("Y\n").is_ok());
assert!(answer("yes\n").is_ok());
assert!(answer(" YES \n").is_ok());
}
#[test]
fn other_answers_abort() {
for input in ["n\n", "no\n", "\n", "", "maybe\n", "yep\n"] {
let err = answer(input).unwrap_err();
assert_eq!(err.to_string(), "aborted", "input {input:?}");
}
}
#[test]
fn argument_value_is_used_verbatim() {
assert_eq!(
resolve_secret_value(Some(" spaced "), "value").unwrap(),
" spaced "
);
}
#[test]
fn empty_argument_is_rejected() {
let err = resolve_secret_value(Some(""), "value").unwrap_err();
assert!(err.to_string().contains("must not be empty"), "{err}");
}
#[test]
fn only_the_trailing_newline_is_stripped() {
assert_eq!(strip_trailing_newline("secret\n"), "secret");
assert_eq!(strip_trailing_newline("secret\r\n"), "secret");
assert_eq!(strip_trailing_newline("secret"), "secret");
assert_eq!(strip_trailing_newline("line1\nline2\n"), "line1\nline2");
assert_eq!(strip_trailing_newline("secret\n\n"), "secret\n");
assert_eq!(strip_trailing_newline(" padded "), " padded ");
}
}