From: Gabriel Goller <g.goller@proxmox.com>
To: pbs-devel@lists.proxmox.com
Cc: Thomas Lamprecht <t.lamprecht@proxmox.com>
Subject: [pbs-devel] [PATCH proxmox v7 1/2] router: cli: add confirmation helper
Date: Mon, 3 Jun 2024 10:43:10 +0200 [thread overview]
Message-ID: <20240603084316.64361-2-g.goller@proxmox.com> (raw)
In-Reply-To: <20240603084316.64361-1-g.goller@proxmox.com>
Add confirmation helper that outputs a prompt and lets the user
confirm or deny it.
Implemented to close #4763.
Co-authored-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-router/Cargo.toml | 1 +
proxmox-router/src/cli/mod.rs | 114 +++++++++++++++++++++++++++++++++-
2 files changed, 114 insertions(+), 1 deletion(-)
diff --git a/proxmox-router/Cargo.toml b/proxmox-router/Cargo.toml
index dcd71a40..0b9d361f 100644
--- a/proxmox-router/Cargo.toml
+++ b/proxmox-router/Cargo.toml
@@ -19,6 +19,7 @@ percent-encoding.workspace = true
serde_json.workspace = true
serde.workspace = true
unicode-width ="0.1.8"
+regex.workspace = true
# cli:
tokio = { workspace = true, features = [], optional = true }
diff --git a/proxmox-router/src/cli/mod.rs b/proxmox-router/src/cli/mod.rs
index 7df94ad9..78f08920 100644
--- a/proxmox-router/src/cli/mod.rs
+++ b/proxmox-router/src/cli/mod.rs
@@ -12,7 +12,12 @@
//! - Ability to create interactive commands (using ``rustyline``)
//! - Supports complex/nested commands
-use std::collections::HashMap;
+use std::{
+ collections::HashMap,
+ io::{self, Write},
+};
+
+use anyhow::{bail, Error};
use crate::ApiMethod;
@@ -61,6 +66,113 @@ pub fn init_cli_logger(env_var_name: &str, default_log_level: &str) {
.init();
}
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+/// Use for simple yes or no questions, where booleans can be confusing, especially if there's a
+/// default response to consider. The implementation provides query helper for the CLI.
+pub enum Confirmation {
+ Yes,
+ No,
+}
+
+impl Confirmation {
+ /// Get the formatted choice for the query prompt, with self being the highlighted (default)
+ /// one displayed as upper case.
+ pub fn default_choice_str(self) -> &'static str {
+ match self {
+ Self::Yes => "Y/n",
+ Self::No => "y/N",
+ }
+ }
+
+ /// Returns true if the answer is Yes
+ pub fn is_yes(self) -> bool {
+ self == Self::Yes
+ }
+
+ /// Returns true if the answer is No
+ pub fn is_no(self) -> bool {
+ self == Self::No
+ }
+
+ /// Parse an input string reference as yes or no confirmation.
+ ///
+ /// The input string is checked verbatim if it is exactly one of the single chars 'y', 'Y',
+ /// 'n', or 'N'. You must trim the string before calling, if needed, or use one of the query
+ /// helper functions.
+ ///
+ /// ```
+ /// use proxmox_router::cli::Confirmation;
+ ///
+ /// let answer = Confirmation::from_str("y");
+ /// assert!(answer.expect("valid").is_yes());
+ ///
+ /// let answer = Confirmation::from_str("N");
+ /// assert!(answer.expect("valid").is_no());
+ ///
+ /// let answer = Confirmation::from_str("bogus");
+ /// assert!(answer.is_err());
+ /// ```
+ pub fn from_str(input: &str) -> Result<Self, Error> {
+ match input.trim() {
+ "y" | "Y" => Ok(Self::Yes),
+ "n" | "N" => Ok(Self::No),
+ _ => bail!("unexpected choice '{input}'! Use 'y' or 'n'"),
+ }
+ }
+
+ /// Parse a input string reference as yes or no confirmation, allowing a fallback default
+ /// answer if the user enters an empty choice.
+ ///
+ /// The input string is checked verbatim if it is exactly one of the single chars 'y', 'Y',
+ /// 'n', or 'N'. The empty string maps to the default. You must trim the string before calling,
+ /// if needed, or use one of the query helper functions.
+ ///
+ /// ```
+ /// use proxmox_router::cli::Confirmation;
+ ///
+ /// let answer = Confirmation::from_str_with_default("", Confirmation::No);
+ /// assert!(answer.expect("valid").is_no());
+ ///
+ /// let answer = Confirmation::from_str_with_default("n", Confirmation::Yes);
+ /// assert!(answer.expect("valid").is_no());
+ ///
+ /// let answer = Confirmation::from_str_with_default("yes", Confirmation::Yes);
+ /// assert!(answer.is_err()); // full-word answer not allowed for now.
+ /// ```
+ pub fn from_str_with_default(input: &str, default: Self) -> Result<Self, Error> {
+ match input.trim() {
+ "y" | "Y" => Ok(Self::Yes),
+ "n" | "N" => Ok(Self::No),
+ "" => Ok(default),
+ _ => bail!("unexpected choice '{input}'! Use enter for default or use 'y' or 'n'"),
+ }
+ }
+
+ /// Print a query prompt with available yes no choices and returns the String the user enters.
+ fn read_line(query: &str, choices: &str) -> Result<String, io::Error> {
+ print!("{query} [{choices}]: ");
+
+ io::stdout().flush()?;
+ let stdin = io::stdin();
+ let mut line = String::new();
+ stdin.read_line(&mut line)?;
+ Ok(line)
+ }
+
+ /// Print a query prompt and parse the white-space trimmed answer using `from_str`.
+ pub fn query(query: &str) -> Result<Self, Error> {
+ let line = Self::read_line(query, "y/n")?;
+ Confirmation::from_str(line.trim())
+ }
+
+ /// Print a query prompt and parse the answer using `from_str_with_default`, falling back to the
+ /// default_answer if the user provided an empty string.
+ pub fn query_with_default(query: &str, default_answer: Self) -> Result<Self, Error> {
+ let line = Self::read_line(query, default_answer.default_choice_str())?;
+ Confirmation::from_str_with_default(line.trim(), default_answer)
+ }
+}
+
/// Define a simple CLI command.
pub struct CliCommand {
/// The Schema definition.
--
2.43.0
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
next prev parent reply other threads:[~2024-06-03 8:43 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-06-03 8:43 [pbs-devel] [PATCH proxmox{, -backup} v7 0/2] close #4763: client: add command to forget backup group Gabriel Goller
2024-06-03 8:43 ` Gabriel Goller [this message]
2024-06-03 8:43 ` [pbs-devel] [PATCH proxmox-backup v7 2/2] " Gabriel Goller
2024-06-19 9:37 ` [pbs-devel] applied-series: [PATCH proxmox{, -backup} v7 0/2] " Wolfgang Bumiller
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20240603084316.64361-2-g.goller@proxmox.com \
--to=g.goller@proxmox.com \
--cc=pbs-devel@lists.proxmox.com \
--cc=t.lamprecht@proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.