1use std::io::{BufRead, IsTerminal, Read, Write};
10
11use anyhow::{Result, bail};
12
13pub fn confirm(prompt: &str, assume_yes: bool) -> Result<()> {
31 let stdin = std::io::stdin();
32 confirm_with(prompt, assume_yes, stdin.is_terminal(), &mut stdin.lock())
33}
34
35fn confirm_with<R: BufRead>(
42 prompt: &str,
43 assume_yes: bool,
44 interactive: bool,
45 reader: &mut R,
46) -> Result<()> {
47 if assume_yes {
48 return Ok(());
49 }
50
51 if !interactive {
52 bail!("refusing to prompt on a non-interactive stdin; pass --yes to confirm");
53 }
54
55 let mut stderr = std::io::stderr();
56 write!(stderr, "{prompt} [y/N] ")?;
57 stderr.flush()?;
58
59 let mut answer = String::new();
60 reader.read_line(&mut answer)?;
61
62 match answer.trim().to_ascii_lowercase().as_str() {
63 "y" | "yes" => Ok(()),
64 _ => bail!("aborted"),
65 }
66}
67
68pub fn resolve_secret_value(argument: Option<&str>, label: &str) -> Result<String> {
92 match argument {
93 Some(value) => reject_empty(value.to_string(), label),
94 None => {
95 let mut buffer = String::new();
96 std::io::stdin().read_to_string(&mut buffer)?;
97 reject_empty(strip_trailing_newline(&buffer).to_string(), label)
98 }
99 }
100}
101
102fn strip_trailing_newline(raw: &str) -> &str {
104 raw.strip_suffix('\n')
105 .map_or(raw, |s| s.strip_suffix('\r').unwrap_or(s))
106}
107
108fn reject_empty(value: String, label: &str) -> Result<String> {
110 if value.is_empty() {
111 bail!("{label} must not be empty");
112 }
113 Ok(value)
114}
115
116#[cfg(test)]
117mod tests {
118 use std::io::Cursor;
119
120 use super::*;
121
122 fn answer(input: &str) -> Result<()> {
123 let mut reader = Cursor::new(input.as_bytes().to_vec());
124 confirm_with("Delete?", false, true, &mut reader)
125 }
126
127 #[test]
128 fn assume_yes_skips_the_prompt() {
129 let mut reader = Cursor::new(Vec::new());
130 assert!(confirm_with("Delete?", true, false, &mut reader).is_ok());
131 }
132
133 #[test]
134 fn non_interactive_stdin_is_refused() {
135 let mut reader = Cursor::new(b"y\n".to_vec());
136 let err = confirm_with("Delete?", false, false, &mut reader).unwrap_err();
137 assert!(err.to_string().contains("--yes"), "{err}");
138 }
139
140 #[test]
141 fn affirmative_answers_are_accepted() {
142 assert!(answer("y\n").is_ok());
143 assert!(answer("Y\n").is_ok());
144 assert!(answer("yes\n").is_ok());
145 assert!(answer(" YES \n").is_ok());
146 }
147
148 #[test]
149 fn other_answers_abort() {
150 for input in ["n\n", "no\n", "\n", "", "maybe\n", "yep\n"] {
151 let err = answer(input).unwrap_err();
152 assert_eq!(err.to_string(), "aborted", "input {input:?}");
153 }
154 }
155
156 #[test]
157 fn argument_value_is_used_verbatim() {
158 assert_eq!(
159 resolve_secret_value(Some(" spaced "), "value").unwrap(),
160 " spaced "
161 );
162 }
163
164 #[test]
165 fn empty_argument_is_rejected() {
166 let err = resolve_secret_value(Some(""), "value").unwrap_err();
167 assert!(err.to_string().contains("must not be empty"), "{err}");
168 }
169
170 #[test]
171 fn only_the_trailing_newline_is_stripped() {
172 assert_eq!(strip_trailing_newline("secret\n"), "secret");
173 assert_eq!(strip_trailing_newline("secret\r\n"), "secret");
174 assert_eq!(strip_trailing_newline("secret"), "secret");
175 assert_eq!(strip_trailing_newline("line1\nline2\n"), "line1\nline2");
176 assert_eq!(strip_trailing_newline("secret\n\n"), "secret\n");
177 assert_eq!(strip_trailing_newline(" padded "), " padded ");
178 }
179}