use std::io::{self, Write};
use anyhow::{Context, Result};
pub fn resolve_omnidiff_path() -> String {
std::env::current_exe()
.ok()
.and_then(|path| path.to_str().map(str::to_string))
.unwrap_or_else(|| "omnidiff".to_string())
}
pub fn parse_yes_no(input: &str, default: bool) -> Option<bool> {
match input.trim().to_lowercase().as_str() {
"" => Some(default),
"y" | "yes" => Some(true),
"n" | "no" => Some(false),
_ => None,
}
}
pub fn ask_yes_no(prompt: &str, default: bool) -> Result<bool> {
loop {
let input = read_line(prompt)?;
match parse_yes_no(&input, default) {
Some(answer) => return Ok(answer),
None => println!("'{input}' - please answer 'y' or 'n'."),
}
}
}
pub fn read_line(prompt: &str) -> Result<String> {
print!("{prompt}");
io::stdout().flush().context("failed to write prompt")?;
let mut input = String::new();
let bytes = io::stdin()
.read_line(&mut input)
.context("failed to read from stdin")?;
if bytes == 0 {
println!();
anyhow::bail!("input ended before the last question - nothing was changed");
}
Ok(input.trim().to_string())
}
pub fn shell_quote(value: &str) -> String {
let is_safe = |c: char| c.is_ascii_alphanumeric() || "-_./+:@%=".contains(c);
if !value.is_empty() && value.chars().all(is_safe) {
return value.to_string();
}
format!("'{}'", value.replace('\'', r"'\''"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shell_quote_leaves_a_plain_path_alone() {
assert_eq!(
shell_quote("/usr/local/bin/omnidiff"),
"/usr/local/bin/omnidiff"
);
assert_eq!(shell_quote("omnidiff"), "omnidiff");
}
#[test]
fn shell_quote_single_quotes_a_path_with_spaces_or_shell_syntax() {
assert_eq!(
shell_quote("/Applications/My Tools/omnidiff"),
"'/Applications/My Tools/omnidiff'"
);
assert_eq!(
shell_quote(r"C:\Program Files\omnidiff\omnidiff.exe"),
r"'C:\Program Files\omnidiff\omnidiff.exe'"
);
assert_eq!(shell_quote("$HOME/bin/omnidiff"), "'$HOME/bin/omnidiff'");
assert_eq!(shell_quote(""), "''");
}
#[test]
fn shell_quote_escapes_an_embedded_single_quote() {
assert_eq!(
shell_quote("/home/o'brien/omnidiff"),
r"'/home/o'\''brien/omnidiff'"
);
}
#[test]
fn resolve_omnidiff_path_never_returns_an_empty_string() {
assert!(!resolve_omnidiff_path().is_empty());
}
#[test]
fn parse_yes_no_accepts_y_n_and_their_full_spellings_case_insensitively() {
assert_eq!(parse_yes_no("y", false), Some(true));
assert_eq!(parse_yes_no("Y", false), Some(true));
assert_eq!(parse_yes_no("yes", false), Some(true));
assert_eq!(parse_yes_no("n", true), Some(false));
assert_eq!(parse_yes_no("no", true), Some(false));
}
#[test]
fn parse_yes_no_falls_back_to_the_default_on_an_empty_line() {
assert_eq!(parse_yes_no("", true), Some(true));
assert_eq!(parse_yes_no(" ", false), Some(false));
}
#[test]
fn parse_yes_no_rejects_anything_else() {
assert_eq!(parse_yes_no("maybe", true), None);
assert_eq!(parse_yes_no("ye", true), None);
}
}