Skip to main content

zerodds_config/
cli.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The command line of a ZeroDDS daemon, read one flag at a time.
4//!
5//! Each bridge daemon spelled out the same loop: split `--flag=VALUE`, take
6//! the value from the next argument when there is no `=`, name the flag a
7//! value is missing for. They differ only in their flags, so the loop is
8//! here and the flags stay with each daemon:
9//!
10//! ```
11//! use zerodds_config::cli::{CliError, Flags};
12//!
13//! let args = ["--domain=7".to_string(), "--topic".to_string(), "T".to_string()];
14//! let (mut domain, mut topics) = (None::<i32>, Vec::new());
15//! let mut flags = Flags::new(&args);
16//! while let Some(flag) = flags.next_flag() {
17//!     match flag.name() {
18//!         "--domain" => domain = Some(flags.parsed(&flag)?),
19//!         "--topic" => topics.push(flags.value(&flag)?),
20//!         other => return Err(CliError::UnknownFlag(other.into())),
21//!     }
22//! }
23//! assert_eq!((domain, topics), (Some(7), vec!["T".to_string()]));
24//! # Ok::<(), CliError>(())
25//! ```
26
27use alloc::string::{String, ToString};
28use core::str::FromStr;
29
30/// What is wrong with a daemon's command line.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum CliError {
33    /// A flag the daemon does not know.
34    UnknownFlag(String),
35    /// A flag that takes a value, at the end of the command line.
36    MissingValue(String),
37    /// A value its flag cannot use.
38    InvalidValue {
39        /// The flag.
40        flag: String,
41        /// The value given.
42        value: String,
43    },
44    /// One flag of a pair without the other.
45    Unpaired {
46        /// The flag given.
47        given: String,
48        /// The flag it needs.
49        needs: String,
50    },
51}
52
53impl core::fmt::Display for CliError {
54    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
55        match self {
56            Self::UnknownFlag(n) => write!(f, "unknown flag: {n}"),
57            Self::MissingValue(n) => write!(f, "flag {n} needs a value"),
58            Self::InvalidValue { flag, value } => write!(f, "bad value for {flag}: {value}"),
59            Self::Unpaired { given, needs } => write!(f, "{given} needs {needs}"),
60        }
61    }
62}
63
64impl std::error::Error for CliError {}
65
66/// One flag as given: its name, and the value after `=` if there was one.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct Flag {
69    name: String,
70    inline: Option<String>,
71}
72
73impl Flag {
74    /// The flag without an `=VALUE`, e.g. `--domain`.
75    #[must_use]
76    pub fn name(&self) -> &str {
77        &self.name
78    }
79}
80
81/// Walks a command line flag by flag.
82#[derive(Debug)]
83pub struct Flags<'a> {
84    args: &'a [String],
85    next: usize,
86}
87
88impl<'a> Flags<'a> {
89    /// A walk over `args`, the program name not among them.
90    #[must_use]
91    pub fn new(args: &'a [String]) -> Self {
92        Self { args, next: 0 }
93    }
94
95    /// The next flag, `None` past the last argument.
96    pub fn next_flag(&mut self) -> Option<Flag> {
97        let raw = self.args.get(self.next)?;
98        self.next += 1;
99        Some(match raw.split_once('=') {
100            Some((name, value)) => Flag {
101                name: name.to_string(),
102                inline: Some(value.to_string()),
103            },
104            None => Flag {
105                name: raw.clone(),
106                inline: None,
107            },
108        })
109    }
110
111    /// The value of `flag`: what followed its `=`, or else the next
112    /// argument, which the walk then passes over.
113    ///
114    /// # Errors
115    /// [`CliError::MissingValue`] when `flag` is the last argument.
116    pub fn value(&mut self, flag: &Flag) -> Result<String, CliError> {
117        if let Some(v) = &flag.inline {
118            return Ok(v.clone());
119        }
120        let v = self
121            .args
122            .get(self.next)
123            .cloned()
124            .ok_or_else(|| CliError::MissingValue(flag.name.clone()))?;
125        self.next += 1;
126        Ok(v)
127    }
128
129    /// The value of `flag`, parsed.
130    ///
131    /// # Errors
132    /// [`CliError::MissingValue`] as [`Self::value`];
133    /// [`CliError::InvalidValue`] when the value does not parse.
134    pub fn parsed<T: FromStr>(&mut self, flag: &Flag) -> Result<T, CliError> {
135        let v = self.value(flag)?;
136        v.parse().map_err(|_| CliError::InvalidValue {
137            flag: flag.name.clone(),
138            value: v,
139        })
140    }
141}
142
143#[cfg(test)]
144#[allow(clippy::unwrap_used)]
145mod tests {
146    use super::*;
147    use alloc::vec;
148    use alloc::vec::Vec;
149
150    fn args(parts: &[&str]) -> Vec<String> {
151        parts.iter().map(|s| (*s).to_string()).collect()
152    }
153
154    #[test]
155    fn a_value_comes_inline_or_from_the_next_argument() {
156        let a = args(&["--a=1", "--b", "2", "--switch"]);
157        let mut flags = Flags::new(&a);
158        let f = flags.next_flag().unwrap();
159        assert_eq!((f.name(), flags.value(&f).unwrap().as_str()), ("--a", "1"));
160        let f = flags.next_flag().unwrap();
161        assert_eq!(flags.parsed::<u8>(&f).unwrap(), 2);
162        assert_eq!(flags.next_flag().unwrap().name(), "--switch");
163        assert!(flags.next_flag().is_none());
164    }
165
166    #[test]
167    fn a_switch_given_a_value_inline_keeps_its_name() {
168        let a = args(&["--help=yes"]);
169        let mut flags = Flags::new(&a);
170        assert_eq!(flags.next_flag().unwrap().name(), "--help");
171        assert!(flags.next_flag().is_none());
172    }
173
174    #[test]
175    fn a_missing_and_an_unparseable_value_name_their_flag() {
176        let a = args(&["--n"]);
177        let mut flags = Flags::new(&a);
178        let f = flags.next_flag().unwrap();
179        assert_eq!(
180            flags.value(&f).unwrap_err(),
181            CliError::MissingValue("--n".into())
182        );
183        let a = args(&["--n", "x"]);
184        let mut flags = Flags::new(&a);
185        let f = flags.next_flag().unwrap();
186        assert_eq!(
187            flags.parsed::<i32>(&f).unwrap_err(),
188            CliError::InvalidValue {
189                flag: "--n".into(),
190                value: "x".into()
191            }
192        );
193    }
194
195    #[test]
196    fn the_messages_are_the_daemons_messages() {
197        let cases = vec![
198            (CliError::UnknownFlag("--x".into()), "unknown flag: --x"),
199            (
200                CliError::MissingValue("--x".into()),
201                "flag --x needs a value",
202            ),
203            (
204                CliError::InvalidValue {
205                    flag: "--x".into(),
206                    value: "v".into(),
207                },
208                "bad value for --x: v",
209            ),
210            (
211                CliError::Unpaired {
212                    given: "--a".into(),
213                    needs: "--b".into(),
214                },
215                "--a needs --b",
216            ),
217        ];
218        for (e, text) in cases {
219            assert_eq!(e.to_string(), text);
220        }
221    }
222}