query/
lib.rs

1//! This library gives various functions that are used to query a user. Each function performs the
2//! following:
3//!
4//!  - Read the next command line argument, and try to parse it as an answer. If the parsing fails,
5//!    panic.  Otherwise, return the argument
6//!
7//!  - If there are no command line arguments left, query the user for an input, and parse it as an
8//!    answer. If the parsing fails, query the user again.
9//!
10//! The "normal" usage mode is to not supply any command line arguments and just use the second
11//! functionality. However, the first is useful for testing and batch processing.
12
13#![deny(clippy::use_self, unsafe_op_in_unsafe_fn)]
14
15use std::{
16    cell::RefCell,
17    env::Args,
18    fmt::Display,
19    io::{Write, stderr, stdin},
20};
21
22thread_local! {
23    static ARGV: RefCell<Args> = {
24        let mut args = std::env::args();
25        args.next();
26        RefCell::new(args)
27    }
28}
29
30pub fn optional<S, E: Display>(
31    prompt: &str,
32    mut parser: impl for<'a> FnMut(&'a str) -> Result<S, E>,
33) -> Option<S> {
34    raw(&format!("{prompt} (optional)"), |x| {
35        if x.is_empty() {
36            Ok(None)
37        } else {
38            parser(x).map(Some)
39        }
40    })
41}
42
43pub fn with_default<S, E: Display>(
44    prompt: &str,
45    default: &str,
46    mut parser: impl for<'a> FnMut(&'a str) -> Result<S, E>,
47) -> S {
48    raw(&format!("{prompt} (default: {default})"), |x| {
49        if x.is_empty() {
50            parser(default)
51        } else {
52            parser(x)
53        }
54    })
55}
56
57pub fn yes_no(prompt: &str) -> bool {
58    with_default(prompt, "y", |response| {
59        if response.starts_with('y') || response.starts_with('n') {
60            Ok(response.starts_with('y'))
61        } else {
62            Err(format!(
63                "unrecognized response '{response}'. Should be '(y)es' or '(n)o'"
64            ))
65        }
66    })
67}
68
69pub fn raw<S, E: Display>(
70    prompt: &str,
71    mut parser: impl for<'a> FnMut(&'a str) -> Result<S, E>,
72) -> S {
73    let cli: Option<(String, Result<S, E>)> = ARGV.with(|argv| {
74        let arg = argv.borrow_mut().next()?;
75        let result = parser(&arg);
76        Some((arg, result))
77    });
78
79    match cli {
80        Some((arg, Ok(res))) => {
81            eprintln!("{prompt}: {arg}");
82            return res;
83        }
84        Some((arg, Err(e))) => {
85            eprintln!("{prompt}: {arg}");
86            eprintln!("{e:#}");
87            std::process::exit(1);
88        }
89        None => (),
90    }
91
92    loop {
93        eprint!("{prompt}: ");
94        stderr().flush().unwrap();
95        let mut input = String::new();
96        stdin()
97            .read_line(&mut input)
98            .unwrap_or_else(|_| panic!("Error reading for prompt: {prompt}"));
99        let trimmed = input.trim();
100        match parser(trimmed) {
101            Ok(res) => {
102                return res;
103            }
104            Err(e) => {
105                eprintln!("{e:#}\n\nTry again");
106            }
107        }
108    }
109}
110
111pub fn vector(prompt: &str, len: usize) -> Vec<u32> {
112    raw(prompt, |s| {
113        let v = s[1..s.len() - 1]
114            .split(',')
115            .map(|x| x.trim().parse::<u32>().map_err(|e| e.to_string()))
116            .collect::<Result<Vec<_>, String>>()?;
117        if v.len() != len {
118            return Err(format!(
119                "Target has dimension {} but {} coordinates supplied",
120                len,
121                v.len()
122            ));
123        }
124        Ok(v)
125    })
126}