feat(config): port the configuration system and CLI argument parser
Ports ZIG/src/config (minus C/WASM APIs) and cli/{args,CommaSplitter,
diagnostics}.zig into ghostty-config:
- all 202 options with exact names, types, defaults and verbatim docs,
declared through one config_struct! macro; Zig's comptime reflection is
replaced by the ConfigField/CliTarget traits
- loading: default XDG files, -e, config-file includes (optional '?',
cycle detection), themes incl. light:/dark: pairs, conditional config,
replay, finalize() derived defaults, clone/changed diffing for reload
- Linux/GTK default keybinds built on ghostty_input::binding::Set
- formatter producing +show-config output
Verified against the installed Ghostty 1.3.1: `+show-config --default`
and `--default --docs` match byte for byte (except gtk-opengl-debug,
which differs between debug/release builds), as do two complex loaded
configs.
Open items (docs/rewrite/notes/ghostty-config.md): url regex needs an
Oniguruma-compatible engine (onig) in the renderer; some font/terminal
types are local copies awaiting integration; KeyRemapSet equality is
order sensitive unlike Zig.
Test Plan:
- cargo test -p ghostty-config (255 tests)
- compare `ghostty +show-config --default --docs` with our formatter
This commit is contained in:
1 parent
800fe5be5e
commit
33d15a2dc2
34 files changed
+14894
-1
No files matched your search
@@ -0,0 +1,1433 @@
|
||||
//! Generic CLI / config-line argument parser (port of `cli/args.zig`).
|
||||
//!
|
||||
//! The Zig implementation uses comptime reflection over the destination
|
||||
//! struct. In Rust this is done with two traits:
|
||||
//!
|
||||
//! * [`ConfigField`]: implemented by every type that can be a field value
|
||||
//! (bool, ints, floats, strings, enums, repeatable lists, ...). It knows
|
||||
//! how to parse itself from a `--key=value` value and how to format itself
|
||||
//! in the `+show-config` format.
|
||||
//! * [`CliTarget`]: implemented by the destination struct (usually generated
|
||||
//! with the [`config_struct!`](crate::config_struct) macro). It maps a key
|
||||
//! to a field and optionally provides diagnostics, compatibility handlers,
|
||||
//! a manual-parse hook and a `--help` hook.
|
||||
|
||||
use crate::comma_splitter::CommaSplitter;
|
||||
use crate::diagnostics::{Diagnostic, DiagnosticList, Location};
|
||||
use crate::formatter::EntryFormatter;
|
||||
use std::fmt;
|
||||
use std::io::BufRead;
|
||||
|
||||
/// For trimming
|
||||
pub const WHITESPACE: &str = " \t";
|
||||
|
||||
/// Trim [`WHITESPACE`] characters from both ends.
|
||||
#[must_use]
|
||||
pub fn trim_ws(s: &str) -> &str {
|
||||
s.trim_matches(|c| c == ' ' || c == '\t')
|
||||
}
|
||||
|
||||
/// The base errors for arg parsing. Additional errors can be returned due
|
||||
/// to type-specific parsing but these are always possible.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ArgError {
|
||||
ValueRequired,
|
||||
InvalidField,
|
||||
InvalidValue,
|
||||
/// A type-specific error. The string is the (Zig) error name and is
|
||||
/// shown to the user as `unknown error <name>`.
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl ArgError {
|
||||
#[must_use]
|
||||
pub fn other(name: &str) -> Self {
|
||||
ArgError::Other(name.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ArgError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ArgError::ValueRequired => f.write_str("ValueRequired"),
|
||||
ArgError::InvalidField => f.write_str("InvalidField"),
|
||||
ArgError::InvalidValue => f.write_str("InvalidValue"),
|
||||
ArgError::Other(s) => f.write_str(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ArgError {}
|
||||
|
||||
impl From<crate::comma_splitter::Error> for ArgError {
|
||||
fn from(e: crate::comma_splitter::Error) -> Self {
|
||||
ArgError::Other(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that can be the value of a configuration / CLI field.
|
||||
pub trait ConfigField: Clone + PartialEq + Default + fmt::Debug {
|
||||
/// For enum types, the list of valid values (used in error messages).
|
||||
const VALID_VALUES: Option<&'static [&'static str]> = None;
|
||||
|
||||
/// Parse `value` (the text after `=`, `None` if there was no `=`) into
|
||||
/// `self`. Repeatable types accumulate; scalar types overwrite.
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError>;
|
||||
|
||||
/// Called when the value was set to the empty string: reset to the
|
||||
/// field's default value (Zig: the `init` function or the declared
|
||||
/// default).
|
||||
fn reset(&mut self, default: Self) {
|
||||
*self = default;
|
||||
}
|
||||
|
||||
/// Write this value in the `+show-config` format.
|
||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result;
|
||||
}
|
||||
|
||||
/// The destination of CLI parsing.
|
||||
pub trait CliTarget {
|
||||
/// Parse a single key/value pair into the destination
|
||||
/// (Zig: `parseIntoField`). Returns `ArgError::InvalidField` if the key
|
||||
/// is unknown.
|
||||
fn parse_into_field(&mut self, key: &str, value: Option<&str>) -> Result<(), ArgError>;
|
||||
|
||||
/// For enum-typed fields, the valid values (used for diagnostics).
|
||||
fn valid_values(&self, _key: &str) -> Option<&'static [&'static str]> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The diagnostic list, if supported (Zig: `_diagnostics` field).
|
||||
fn diagnostics_mut(&mut self) -> Option<&mut DiagnosticList> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Manual parse hook (Zig: `parseManuallyHook`). Return `Ok(false)` to
|
||||
/// stop parsing.
|
||||
fn parse_manually_hook(
|
||||
&mut self,
|
||||
_arg: &str,
|
||||
_iter: &mut dyn ArgIter,
|
||||
) -> Result<bool, ArgError> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Whether `--help`/`-h` is supported (Zig: `help` decl). If so,
|
||||
/// [`CliTarget::help`] is called when seen.
|
||||
fn supports_help(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn help(&mut self) -> Result<(), ArgError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compatibility handler (Zig: `compatibility` decl). Called when
|
||||
/// parsing a field fails. Return true if handled.
|
||||
fn compatibility(&mut self, _key: &str, _value: Option<&str>) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over arguments (Zig: any iterator with `next`).
|
||||
pub trait ArgIter {
|
||||
fn next_arg(&mut self) -> Option<String>;
|
||||
|
||||
/// Location for diagnostics.
|
||||
fn location(&self) -> Option<Location> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the command line arguments from iter into dst.
|
||||
///
|
||||
/// If the destination reports diagnostics (see
|
||||
/// [`CliTarget::diagnostics_mut`]) then errors for individual fields are
|
||||
/// added to the list rather than returned.
|
||||
pub fn parse<T: CliTarget + ?Sized>(dst: &mut T, iter: &mut dyn ArgIter) -> Result<(), ArgError> {
|
||||
while let Some(arg) = iter.next_arg() {
|
||||
// Do manual parsing if we have a hook for it.
|
||||
if !dst.parse_manually_hook(&arg, iter)? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If the destination supports help then we check for it, call
|
||||
// the help function and return.
|
||||
if dst.supports_help() && (arg == "--help" || arg == "-h") {
|
||||
dst.help()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If this doesn't start with "--" then it isn't a config
|
||||
// flag. We don't support positional arguments or configuration
|
||||
// values set with spaces so this is an error.
|
||||
let Some(rest) = arg.strip_prefix("--") else {
|
||||
let loc = iter.location().unwrap_or_default();
|
||||
let Some(diags) = dst.diagnostics_mut() else {
|
||||
return Err(ArgError::InvalidField);
|
||||
};
|
||||
diags.append(Diagnostic {
|
||||
key: arg.clone(),
|
||||
message: "invalid field".to_string(),
|
||||
location: loc,
|
||||
});
|
||||
continue;
|
||||
};
|
||||
|
||||
let (key, value): (&str, Option<&str>) = match rest.find('=') {
|
||||
Some(idx) => (&rest[..idx], Some(&rest[idx + 1..])),
|
||||
None => (rest, None),
|
||||
};
|
||||
|
||||
if let Err(err) = dst.parse_into_field(key, value) {
|
||||
// Fall back to compatibility handlers if able.
|
||||
if dst.compatibility(key, value) {
|
||||
log::info!(
|
||||
"compatibility handler for {key} handled error, you may be using a deprecated field: {err}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let loc = iter.location().unwrap_or_default();
|
||||
let message = match &err {
|
||||
ArgError::InvalidField => "unknown field".to_string(),
|
||||
ArgError::ValueRequired => {
|
||||
let mut m = String::from("value required");
|
||||
format_values(dst.valid_values(key), &mut m);
|
||||
m
|
||||
}
|
||||
ArgError::InvalidValue => {
|
||||
let mut m = match value {
|
||||
Some(v) => format!("invalid value \"{v}\""),
|
||||
None => "invalid value \"null\"".to_string(),
|
||||
};
|
||||
format_values(dst.valid_values(key), &mut m);
|
||||
m
|
||||
}
|
||||
ArgError::Other(name) => format!("unknown error error.{name}"),
|
||||
};
|
||||
|
||||
let Some(diags) = dst.diagnostics_mut() else {
|
||||
return Err(err);
|
||||
};
|
||||
diags.append(Diagnostic {
|
||||
key: key.to_string(),
|
||||
message,
|
||||
location: loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_values(values: Option<&[&str]>, out: &mut String) {
|
||||
if let Some(values) = values {
|
||||
out.push_str(", valid values are: ");
|
||||
out.push_str(&values.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience for a compatibility handler that renames a field: parses
|
||||
/// `value` into the field `to` of `dst`. Returns true if it succeeded.
|
||||
pub fn compatibility_renamed<T: CliTarget + ?Sized>(
|
||||
dst: &mut T,
|
||||
to: &str,
|
||||
value: Option<&str>,
|
||||
) -> bool {
|
||||
match dst.parse_into_field(to, value) {
|
||||
Ok(()) => true,
|
||||
Err(err) => {
|
||||
log::warn!("error parsing renamed field {to}: {err}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Split a tagged-union value `tag:value` (Zig: `parseTaggedUnion`). The tag
|
||||
/// is trimmed of whitespace. If there is no colon the value is empty.
|
||||
#[must_use]
|
||||
pub fn split_tagged_union(v: &str) -> (&str, &str) {
|
||||
match v.find(':') {
|
||||
Some(idx) => (trim_ws(&v[..idx]), &v[idx + 1..]),
|
||||
None => (trim_ws(v), ""),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse an "auto struct" value like `a:hello,b:42` (Zig: `parseAutoStruct`).
|
||||
///
|
||||
/// Entries are split by commas (with quoting / escaping, see
|
||||
/// [`CommaSplitter`]). Each entry must be `key:value`; the value is trimmed
|
||||
/// and, if double-quoted, decoded as a Zig string literal. For every entry
|
||||
/// `set(key, value)` is called; it must return `Ok(true)` if the key matched
|
||||
/// a field, `Ok(false)` if the key is unknown (which yields `InvalidValue`).
|
||||
/// The caller is responsible for tracking which fields were set and for
|
||||
/// applying defaults / required-field checks afterwards.
|
||||
pub fn parse_auto_struct_with(
|
||||
v: &str,
|
||||
mut set: impl FnMut(&str, &str) -> Result<bool, ArgError>,
|
||||
) -> Result<(), ArgError> {
|
||||
let mut iter = CommaSplitter::new(v);
|
||||
while let Some(entry) = iter.next_field()? {
|
||||
let idx = entry.find(':').ok_or(ArgError::InvalidValue)?;
|
||||
let key = trim_ws(&entry[..idx]);
|
||||
let raw = trim_ws(&entry[idx + 1..]);
|
||||
|
||||
let decoded;
|
||||
let value = if raw.len() >= 2 && raw.starts_with('"') && raw.ends_with('"') {
|
||||
decoded = parse_zig_string_literal(raw).ok_or(ArgError::InvalidValue)?;
|
||||
decoded.as_str()
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
|
||||
if !set(key, value)? {
|
||||
return Err(ArgError::InvalidValue);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode a double-quoted Zig string literal (including the quotes).
|
||||
/// Returns `None` on failure. Bytes produced by `\x` escapes that do not form
|
||||
/// valid UTF-8 are converted lossily.
|
||||
#[must_use]
|
||||
pub fn parse_zig_string_literal(s: &str) -> Option<String> {
|
||||
let b = s.as_bytes();
|
||||
if b.len() < 2 || b[0] != b'"' || b[b.len() - 1] != b'"' {
|
||||
return None;
|
||||
}
|
||||
let inner = &b[1..b.len() - 1];
|
||||
let mut out: Vec<u8> = Vec::with_capacity(inner.len());
|
||||
let mut i = 0;
|
||||
while i < inner.len() {
|
||||
let c = inner[i];
|
||||
match c {
|
||||
b'\\' => {
|
||||
i += 1;
|
||||
let e = *inner.get(i)?;
|
||||
match e {
|
||||
b'n' => out.push(b'\n'),
|
||||
b'r' => out.push(b'\r'),
|
||||
b't' => out.push(b'\t'),
|
||||
b'\\' => out.push(b'\\'),
|
||||
b'\'' => out.push(b'\''),
|
||||
b'"' => out.push(b'"'),
|
||||
b'x' => {
|
||||
let h = inner.get(i + 1..i + 3)?;
|
||||
let h = std::str::from_utf8(h).ok()?;
|
||||
out.push(u8::from_str_radix(h, 16).ok()?);
|
||||
i += 2;
|
||||
}
|
||||
b'u' => {
|
||||
if *inner.get(i + 1)? != b'{' {
|
||||
return None;
|
||||
}
|
||||
let close = inner[i + 2..].iter().position(|&x| x == b'}')? + i + 2;
|
||||
let h = std::str::from_utf8(&inner[i + 2..close]).ok()?;
|
||||
if h.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let cp = u32::from_str_radix(h, 16).ok()?;
|
||||
let ch = char::from_u32(cp)?;
|
||||
let mut buf = [0u8; 4];
|
||||
out.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
|
||||
i = close;
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
b'"' | b'\n' => return None,
|
||||
_ => out.push(c),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
Some(String::from_utf8_lossy(&out).into_owned())
|
||||
}
|
||||
|
||||
/// Parse a "packed struct" value (comma separated names, optionally
|
||||
/// prefixed with `no-`, or a standalone boolean). `names` are the field
|
||||
/// names; `set(index, value)` is called for each setting.
|
||||
pub fn parse_packed_struct_with(
|
||||
v: &str,
|
||||
names: &[&str],
|
||||
mut set: impl FnMut(usize, bool),
|
||||
) -> Result<(), ArgError> {
|
||||
// Allow standalone boolean values like "true" and "false" to
|
||||
// turn on or off all of the struct's fields.
|
||||
if let Ok(b) = parse_bool(v) {
|
||||
for i in 0..names.len() {
|
||||
set(i, b);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for part_raw in v.split(',') {
|
||||
let trimmed = trim_ws(part_raw);
|
||||
let (part, value) = match trimmed.strip_prefix("no-") {
|
||||
Some(rest) => (rest, false),
|
||||
None => (trimmed, true),
|
||||
};
|
||||
match names.iter().position(|n| *n == part) {
|
||||
Some(i) => set(i, value),
|
||||
None => return Err(ArgError::InvalidValue),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_bool(v: &str) -> Result<bool, ArgError> {
|
||||
match v {
|
||||
"1" | "t" | "T" | "true" => Ok(true),
|
||||
"0" | "f" | "F" | "false" => Ok(false),
|
||||
_ => Err(ArgError::InvalidValue),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse an integer like Zig's `std.fmt.parseInt(T, s, 0)`: optional sign,
|
||||
/// optional `0x`/`0o`/`0b` prefix, underscores between digits allowed.
|
||||
pub fn parse_int_base0(s: &str) -> Option<i128> {
|
||||
let (neg, rest) = match s.as_bytes().first()? {
|
||||
b'-' => (true, &s[1..]),
|
||||
b'+' => (false, &s[1..]),
|
||||
_ => (false, s),
|
||||
};
|
||||
let (base, digits) = if rest.len() >= 2 && rest.as_bytes()[0] == b'0' {
|
||||
match rest.as_bytes()[1] {
|
||||
b'x' => (16, &rest[2..]),
|
||||
b'o' => (8, &rest[2..]),
|
||||
b'b' => (2, &rest[2..]),
|
||||
_ => (10, rest),
|
||||
}
|
||||
} else {
|
||||
(10, rest)
|
||||
};
|
||||
if digits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let db = digits.as_bytes();
|
||||
if db[0] == b'_' || db[db.len() - 1] == b'_' || digits.contains("__") {
|
||||
return None;
|
||||
}
|
||||
let mut acc: i128 = 0;
|
||||
for &c in db {
|
||||
if c == b'_' {
|
||||
continue;
|
||||
}
|
||||
let d = i128::from((c as char).to_digit(base)?);
|
||||
acc = acc.checked_mul(i128::from(base))?.checked_add(d)?;
|
||||
}
|
||||
Some(if neg { -acc } else { acc })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// ConfigField impls for primitives
|
||||
|
||||
impl ConfigField for bool {
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
*self = parse_bool(value.unwrap_or("t"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
||||
writeln!(f.writer, "{} = {}", f.name, self)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_int_field {
|
||||
($($t:ty),*) => {$(
|
||||
impl ConfigField for $t {
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||
let n = parse_int_base0(v).ok_or(ArgError::InvalidValue)?;
|
||||
*self = <$t>::try_from(n).map_err(|_| ArgError::InvalidValue)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
||||
writeln!(f.writer, "{} = {}", f.name, self)
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
impl_int_field!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize);
|
||||
|
||||
macro_rules! impl_float_field {
|
||||
($($t:ty),*) => {$(
|
||||
impl ConfigField for $t {
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||
*self = v.parse::<$t>().map_err(|_| ArgError::InvalidValue)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
||||
writeln!(f.writer, "{} = {}", f.name, self)
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
impl_float_field!(f32, f64);
|
||||
|
||||
impl ConfigField for String {
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
*self = value.ok_or(ArgError::ValueRequired)?.to_string();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
||||
writeln!(f.writer, "{} = {}", f.name, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ConfigField> ConfigField for Option<T> {
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
// For optional fields, we just treat it as the child type.
|
||||
if let Some(v) = self {
|
||||
v.parse_cli(value)
|
||||
} else {
|
||||
let mut tmp = T::default();
|
||||
tmp.parse_cli(value)?;
|
||||
*self = Some(tmp);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Some(v) => v.format_entry(f),
|
||||
None => writeln!(f.writer, "{} = ", f.name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Iterators
|
||||
|
||||
/// An iterator that considers its location to be CLI args. It
|
||||
/// iterates through an underlying iterator and increments a counter
|
||||
/// to track the current CLI arg index.
|
||||
///
|
||||
/// This also ignores any argument that starts with `+`. It assumes that
|
||||
/// actions were parsed out before this iterator was created.
|
||||
pub struct ArgsIterator<I: Iterator<Item = String>> {
|
||||
/// The underlying args iterator.
|
||||
pub iterator: I,
|
||||
/// Our current index into the iterator. This is 1-indexed because
|
||||
/// the 0 value is used to indicate that we haven't read any values yet.
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl<I: Iterator<Item = String>> ArgsIterator<I> {
|
||||
pub fn new(iterator: I) -> Self {
|
||||
Self { iterator, index: 0 }
|
||||
}
|
||||
|
||||
pub fn next(&mut self) -> Option<String> {
|
||||
loop {
|
||||
let value = self.iterator.next()?;
|
||||
self.index += 1;
|
||||
// We ignore any argument that starts with "+". This is used
|
||||
// to indicate actions and are expected to be parsed out before
|
||||
// this iterator is created.
|
||||
if value.starts_with('+') {
|
||||
continue;
|
||||
}
|
||||
return Some(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Iterator<Item = String>> ArgIter for ArgsIterator<I> {
|
||||
fn next_arg(&mut self) -> Option<String> {
|
||||
self.next()
|
||||
}
|
||||
|
||||
fn location(&self) -> Option<Location> {
|
||||
Some(Location::Cli(self.index))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an args iterator for the process args. This will skip argv0.
|
||||
#[must_use]
|
||||
pub fn args_iterator() -> ArgsIterator<std::iter::Skip<std::env::Args>> {
|
||||
ArgsIterator::new(std::env::args().skip(1))
|
||||
}
|
||||
|
||||
/// An iterator valid for arg parsing from a slice.
|
||||
pub struct SliceIterator<'a> {
|
||||
pub slice: &'a [String],
|
||||
pub idx: usize,
|
||||
}
|
||||
|
||||
impl ArgIter for SliceIterator<'_> {
|
||||
fn next_arg(&mut self) -> Option<String> {
|
||||
let v = self.slice.get(self.idx)?;
|
||||
self.idx += 1;
|
||||
Some(v.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a [`SliceIterator`] from a slice.
|
||||
#[must_use]
|
||||
pub fn slice_iterator(slice: &[String]) -> SliceIterator<'_> {
|
||||
SliceIterator { slice, idx: 0 }
|
||||
}
|
||||
|
||||
/// A simple whitespace-splitting iterator used in tests and for
|
||||
/// string-based argument parsing.
|
||||
pub struct VecIterator {
|
||||
items: std::vec::IntoIter<String>,
|
||||
}
|
||||
|
||||
impl VecIterator {
|
||||
#[must_use]
|
||||
pub fn new(items: Vec<String>) -> Self {
|
||||
Self {
|
||||
items: items.into_iter(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Split on spaces (no quoting except double quotes around a value).
|
||||
#[must_use]
|
||||
pub fn from_str_args(s: &str) -> Self {
|
||||
Self::new(split_args(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl ArgIter for VecIterator {
|
||||
fn next_arg(&mut self) -> Option<String> {
|
||||
self.items.next()
|
||||
}
|
||||
}
|
||||
|
||||
/// Split a string into args like `std.process.ArgIteratorGeneral`: whitespace
|
||||
/// separated, double quotes group (and are removed).
|
||||
#[must_use]
|
||||
pub fn split_args(s: &str) -> Vec<String> {
|
||||
let mut out = vec![];
|
||||
let mut cur = String::new();
|
||||
let mut in_arg = false;
|
||||
let mut quoted = false;
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'"' => {
|
||||
quoted = !quoted;
|
||||
in_arg = true;
|
||||
}
|
||||
c if c.is_whitespace() && !quoted => {
|
||||
if in_arg {
|
||||
out.push(std::mem::take(&mut cur));
|
||||
in_arg = false;
|
||||
}
|
||||
}
|
||||
c => {
|
||||
cur.push(c);
|
||||
in_arg = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if in_arg {
|
||||
out.push(cur);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Reads CLI args by line. Each CLI arg is expected to be a single line.
|
||||
/// This is used to implement configuration files.
|
||||
pub struct LineIterator<R: BufRead> {
|
||||
/// Our stateful reader.
|
||||
pub r: R,
|
||||
/// Filepath that is used for diagnostics. It is only used for
|
||||
/// diagnostic messages so it can be formatted however you want.
|
||||
pub filepath: String,
|
||||
/// The current line that we're on. 1-indexed; zero if we haven't read
|
||||
/// any lines yet.
|
||||
pub line: usize,
|
||||
}
|
||||
|
||||
impl<R: BufRead> LineIterator<R> {
|
||||
/// The maximum size a single line can be. We don't expect any
|
||||
/// CLI arg to exceed this size.
|
||||
pub const MAX_LINE_SIZE: usize = 4096;
|
||||
|
||||
pub fn new(r: R) -> Self {
|
||||
Self {
|
||||
r,
|
||||
filepath: String::new(),
|
||||
line: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_path(r: R, filepath: impl Into<String>) -> Self {
|
||||
Self {
|
||||
r,
|
||||
filepath: filepath.into(),
|
||||
line: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) -> Option<String> {
|
||||
let mut buf = Vec::new();
|
||||
let entry: String = loop {
|
||||
buf.clear();
|
||||
match self.r.read_until(b'\n', &mut buf) {
|
||||
Ok(0) => return None,
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::warn!("cannot read from \"{}\": {e}", self.filepath);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
if buf.last() == Some(&b'\n') {
|
||||
buf.pop();
|
||||
}
|
||||
if buf.len() > Self::MAX_LINE_SIZE - 2 {
|
||||
log::warn!("cannot read from \"{}\": StreamTooLong", self.filepath);
|
||||
return None;
|
||||
}
|
||||
|
||||
self.line += 1;
|
||||
|
||||
let line = String::from_utf8_lossy(&buf);
|
||||
// Trim any whitespace (including CR) around it
|
||||
let trim = line.trim_matches(|c| c == ' ' || c == '\t' || c == '\r');
|
||||
|
||||
// Ignore blank lines and comments
|
||||
if trim.is_empty() || trim.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
break trim.to_string();
|
||||
};
|
||||
|
||||
let mut result = String::from("--");
|
||||
if let Some(idx) = entry.find('=') {
|
||||
let key = trim_ws(&entry[..idx]);
|
||||
let mut value = trim_ws(&entry[idx + 1..]);
|
||||
|
||||
// Detect a quoted string.
|
||||
if value.len() >= 2 && value.starts_with('"') && value.ends_with('"') {
|
||||
// Trim quotes since our CLI args processor expects
|
||||
// quotes to already be gone.
|
||||
value = &value[1..value.len() - 1];
|
||||
}
|
||||
|
||||
result.push_str(key);
|
||||
result.push('=');
|
||||
result.push_str(value);
|
||||
} else {
|
||||
result.push_str(&entry);
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: BufRead> ArgIter for LineIterator<R> {
|
||||
fn next_arg(&mut self) -> Option<String> {
|
||||
self.next()
|
||||
}
|
||||
|
||||
fn location(&self) -> Option<Location> {
|
||||
// If we have no filepath then we have no location.
|
||||
if self.filepath.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(Location::File {
|
||||
path: self.filepath.clone(),
|
||||
line: self.line,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{config_enum, config_packed, config_struct};
|
||||
use std::io::Cursor;
|
||||
|
||||
fn parse_str<T: CliTarget>(dst: &mut T, s: &str) -> Result<(), ArgError> {
|
||||
let mut it = VecIterator::from_str_args(s);
|
||||
parse(dst, &mut it)
|
||||
}
|
||||
|
||||
// ---- test structs -------------------------------------------------
|
||||
|
||||
config_struct! {
|
||||
pub struct Simple {
|
||||
"a" => a: String = String::new(),
|
||||
"b" => b: bool = false,
|
||||
"b-f" => b_f: bool = true,
|
||||
}
|
||||
extra {
|
||||
diags: DiagnosticList = DiagnosticList::default(),
|
||||
}
|
||||
}
|
||||
impl Default for Simple {
|
||||
fn default() -> Self {
|
||||
Self::raw_default()
|
||||
}
|
||||
}
|
||||
impl CliTarget for Simple {
|
||||
fn parse_into_field(&mut self, key: &str, value: Option<&str>) -> Result<(), ArgError> {
|
||||
self.parse_field(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
config_struct! {
|
||||
pub struct Quoted {
|
||||
"a" => a: u8 = 0,
|
||||
"b" => b: String = String::new(),
|
||||
}
|
||||
}
|
||||
impl CliTarget for Quoted {
|
||||
fn parse_into_field(&mut self, key: &str, value: Option<&str>) -> Result<(), ArgError> {
|
||||
self.parse_field(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
config_struct! {
|
||||
pub struct Reset {
|
||||
"a" => a: u8 = 42,
|
||||
"b" => b: bool = false,
|
||||
}
|
||||
}
|
||||
impl CliTarget for Reset {
|
||||
fn parse_into_field(&mut self, key: &str, value: Option<&str>) -> Result<(), ArgError> {
|
||||
self.parse_field(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
config_enum! {
|
||||
pub enum OneTwo { One => "one", Two => "two" }
|
||||
}
|
||||
|
||||
config_struct! {
|
||||
pub struct Diag {
|
||||
"a" => a: String = String::new(),
|
||||
"b" => b: OneTwo = OneTwo::One,
|
||||
}
|
||||
extra {
|
||||
diags: DiagnosticList = DiagnosticList::default(),
|
||||
}
|
||||
}
|
||||
impl CliTarget for Diag {
|
||||
fn parse_into_field(&mut self, key: &str, value: Option<&str>) -> Result<(), ArgError> {
|
||||
self.parse_field(key, value)
|
||||
}
|
||||
fn valid_values(&self, key: &str) -> Option<&'static [&'static str]> {
|
||||
Self::valid_values_for(key)
|
||||
}
|
||||
fn diagnostics_mut(&mut self) -> Option<&mut DiagnosticList> {
|
||||
Some(&mut self.diags)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_simple() {
|
||||
let mut data = Simple::raw_default();
|
||||
parse_str(&mut data, "--a=42 --b --b-f=false").unwrap();
|
||||
assert_eq!(data.a, "42");
|
||||
assert!(data.b);
|
||||
assert!(!data.b_f);
|
||||
|
||||
// Reparsing works
|
||||
parse_str(&mut data, "--a=84").unwrap();
|
||||
assert_eq!(data.a, "84");
|
||||
assert!(data.b);
|
||||
assert!(!data.b_f);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_quoted_value() {
|
||||
let mut data = Quoted::raw_default();
|
||||
parse_str(&mut data, "--a=\"42\" --b=\"hello!\"").unwrap();
|
||||
assert_eq!(data.a, 42);
|
||||
assert_eq!(data.b, "hello!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_value_resets_to_default() {
|
||||
let mut data = Reset::raw_default();
|
||||
parse_str(&mut data, "--a= --b=").unwrap();
|
||||
assert_eq!(data.a, 42);
|
||||
assert!(!data.b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_positional_arguments_are_invalid() {
|
||||
let mut data = Reset::raw_default();
|
||||
data.a = 0;
|
||||
let r = parse_str(&mut data, "--a=84 what");
|
||||
assert_eq!(r, Err(ArgError::InvalidField));
|
||||
assert_eq!(data.a, 84);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_diagnostic_tracking() {
|
||||
let mut data = Diag::raw_default();
|
||||
parse_str(&mut data, "--what --a=42").unwrap();
|
||||
assert_eq!(data.a, "42");
|
||||
assert_eq!(data.diags.items().len(), 1);
|
||||
let diag = &data.diags.items()[0];
|
||||
assert_eq!(diag.location, Location::None);
|
||||
assert_eq!(diag.key, "what");
|
||||
assert_eq!(diag.message, "unknown field");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_diagnostic_location() {
|
||||
let mut data = Diag::raw_default();
|
||||
let mut iter = LineIterator::with_path(Cursor::new("a=42\nwhat\nb=two"), "test");
|
||||
parse(&mut data, &mut iter).unwrap();
|
||||
assert_eq!(data.a, "42");
|
||||
assert_eq!(data.b, OneTwo::Two);
|
||||
assert_eq!(data.diags.items().len(), 1);
|
||||
let diag = &data.diags.items()[0];
|
||||
assert_eq!(diag.key, "what");
|
||||
assert_eq!(diag.message, "unknown field");
|
||||
assert_eq!(
|
||||
diag.location,
|
||||
Location::File {
|
||||
path: "test".into(),
|
||||
line: 2
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_diagnostic_invalid_enum_value_lists_valid_values() {
|
||||
let mut data = Diag::raw_default();
|
||||
parse_str(&mut data, "--b=three").unwrap();
|
||||
assert_eq!(
|
||||
data.diags.items()[0].message,
|
||||
"invalid value \"three\", valid values are: one, two"
|
||||
);
|
||||
}
|
||||
|
||||
pub struct Compat {
|
||||
pub a: bool,
|
||||
}
|
||||
impl CliTarget for Compat {
|
||||
fn parse_into_field(&mut self, key: &str, value: Option<&str>) -> Result<(), ArgError> {
|
||||
if key == "a" {
|
||||
// Fails for anything but a valid bool.
|
||||
return self.a.parse_cli(value);
|
||||
}
|
||||
Err(ArgError::InvalidField)
|
||||
}
|
||||
fn compatibility(&mut self, key: &str, value: Option<&str>) -> bool {
|
||||
if key == "a" && value == Some("yuh") {
|
||||
self.a = true;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_compatibility_handler() {
|
||||
let mut data = Compat { a: false };
|
||||
parse_str(&mut data, "--a=yuh").unwrap();
|
||||
assert!(data.a);
|
||||
}
|
||||
|
||||
pub struct Renamed {
|
||||
pub a: bool,
|
||||
pub b: bool,
|
||||
}
|
||||
impl CliTarget for Renamed {
|
||||
fn parse_into_field(&mut self, key: &str, value: Option<&str>) -> Result<(), ArgError> {
|
||||
match key {
|
||||
"a" => self.a.parse_cli(value),
|
||||
"b" => self.b.parse_cli(value),
|
||||
_ => Err(ArgError::InvalidField),
|
||||
}
|
||||
}
|
||||
fn compatibility(&mut self, key: &str, value: Option<&str>) -> bool {
|
||||
if key == "old" {
|
||||
return compatibility_renamed(self, "a", value);
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_compatibility_renamed() {
|
||||
let mut data = Renamed { a: false, b: false };
|
||||
parse_str(&mut data, "--old=true --b=true").unwrap();
|
||||
assert!(data.a);
|
||||
assert!(data.b);
|
||||
}
|
||||
|
||||
config_struct! {
|
||||
pub struct Underscore {
|
||||
"a" => a: String = "12".to_string(),
|
||||
}
|
||||
}
|
||||
impl CliTarget for Underscore {
|
||||
fn parse_into_field(&mut self, key: &str, value: Option<&str>) -> Result<(), ArgError> {
|
||||
// Underscore-prefixed fields are never parseable.
|
||||
if key.starts_with('_') {
|
||||
return Err(ArgError::InvalidField);
|
||||
}
|
||||
self.parse_field(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_ignore_underscore_prefixed_fields() {
|
||||
let mut data = Underscore::raw_default();
|
||||
assert_eq!(
|
||||
data.parse_into_field("_a", Some("42")),
|
||||
Err(ArgError::InvalidField)
|
||||
);
|
||||
assert_eq!(data.a, "12");
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct WithInit {
|
||||
pub v: String,
|
||||
}
|
||||
impl Default for WithInit {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
v: "default".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ConfigField for WithInit {
|
||||
fn parse_cli(&mut self, _value: Option<&str>) -> Result<(), ArgError> {
|
||||
Ok(())
|
||||
}
|
||||
fn reset(&mut self, _default: Self) {
|
||||
// `init`
|
||||
self.v = "HELLO!".into();
|
||||
}
|
||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
||||
writeln!(f.writer, "{} = {}", f.name, self.v)
|
||||
}
|
||||
}
|
||||
|
||||
config_struct! {
|
||||
pub struct HasInit {
|
||||
"a" => a: WithInit = WithInit::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_struct_with_init_func() {
|
||||
let mut data = HasInit::raw_default();
|
||||
data.parse_field("a", Some("")).unwrap();
|
||||
assert_eq!(data.a.v, "HELLO!");
|
||||
}
|
||||
|
||||
macro_rules! one_field {
|
||||
($name:ident, $ty:ty, $def:expr) => {
|
||||
config_struct! {
|
||||
pub struct $name {
|
||||
"v" => v: $ty = $def,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_string() {
|
||||
one_field!(S, String, String::new());
|
||||
let mut d = S::raw_default();
|
||||
d.parse_field("v", Some("42")).unwrap();
|
||||
assert_eq!(d.v, "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_bool() {
|
||||
one_field!(S, bool, false);
|
||||
let mut d = S::raw_default();
|
||||
for t in ["1", "t", "T", "true"] {
|
||||
d.parse_field("v", Some(t)).unwrap();
|
||||
assert!(d.v);
|
||||
d.v = false;
|
||||
}
|
||||
for f in ["0", "f", "F", "false"] {
|
||||
d.v = true;
|
||||
d.parse_field("v", Some(f)).unwrap();
|
||||
assert!(!d.v);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_unsigned_numbers() {
|
||||
one_field!(S, u8, 0);
|
||||
let mut d = S::raw_default();
|
||||
d.parse_field("v", Some("1")).unwrap();
|
||||
assert_eq!(d.v, 1);
|
||||
d.parse_field("v", Some("0x10")).unwrap();
|
||||
assert_eq!(d.v, 16);
|
||||
assert_eq!(d.parse_field("v", Some("256")), Err(ArgError::InvalidValue));
|
||||
assert_eq!(d.parse_field("v", Some("-1")), Err(ArgError::InvalidValue));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_floats() {
|
||||
one_field!(S, f64, 0.0);
|
||||
let mut d = S::raw_default();
|
||||
d.parse_field("v", Some("1")).unwrap();
|
||||
assert!((d.v - 1.0).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
config_enum! {
|
||||
pub enum Three { One => "one", Two => "two", Three => "three" }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_enums() {
|
||||
one_field!(S, Three, Three::One);
|
||||
let mut d = S::raw_default();
|
||||
d.parse_field("v", Some("two")).unwrap();
|
||||
assert_eq!(d.v, Three::Two);
|
||||
}
|
||||
|
||||
config_packed! {
|
||||
pub struct Pk {
|
||||
"a" => a: false,
|
||||
"b" => b: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_packed_struct() {
|
||||
one_field!(S, Pk, Pk::default());
|
||||
let mut d = S::raw_default();
|
||||
d.parse_field("v", Some("b")).unwrap();
|
||||
assert!(!d.v.a);
|
||||
assert!(d.v.b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_packed_struct_negation() {
|
||||
one_field!(S, Pk, Pk::default());
|
||||
let mut d = S::raw_default();
|
||||
d.parse_field("v", Some("a,no-b")).unwrap();
|
||||
assert!(d.v.a);
|
||||
assert!(!d.v.b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_packed_struct_true_false() {
|
||||
one_field!(S, Pk, Pk::default());
|
||||
let mut d = S::raw_default();
|
||||
d.parse_field("v", Some("true")).unwrap();
|
||||
assert!(d.v.a && d.v.b);
|
||||
d.parse_field("v", Some("false")).unwrap();
|
||||
assert!(!d.v.a && !d.v.b);
|
||||
assert_eq!(
|
||||
d.parse_field("v", Some("true,a")),
|
||||
Err(ArgError::InvalidValue)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_packed_struct_whitespace() {
|
||||
one_field!(S, Pk, Pk::default());
|
||||
let mut d = S::raw_default();
|
||||
d.parse_field("v", Some(" a, no-b ")).unwrap();
|
||||
assert!(d.v.a);
|
||||
assert!(!d.v.b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_optional_field() {
|
||||
one_field!(S, Option<bool>, None);
|
||||
let mut d = S::raw_default();
|
||||
d.parse_field("v", Some("1")).unwrap();
|
||||
assert_eq!(d.v, Some(true));
|
||||
// Unset
|
||||
d.parse_field("v", Some("")).unwrap();
|
||||
assert_eq!(d.v, None);
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Default)]
|
||||
pub struct WithParse {
|
||||
pub v: String,
|
||||
}
|
||||
impl ConfigField for WithParse {
|
||||
fn parse_cli(&mut self, _value: Option<&str>) -> Result<(), ArgError> {
|
||||
self.v = "HELLO!".into();
|
||||
Ok(())
|
||||
}
|
||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
||||
writeln!(f.writer, "{} = {}", f.name, self.v)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_struct_with_parse_func() {
|
||||
one_field!(S, WithParse, WithParse::default());
|
||||
let mut d = S::raw_default();
|
||||
d.parse_field("v", Some("42")).unwrap();
|
||||
assert_eq!(d.v.v, "HELLO!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_optional_struct_with_parse_func() {
|
||||
one_field!(S, Option<WithParse>, None);
|
||||
let mut d = S::raw_default();
|
||||
d.parse_field("v", Some("42")).unwrap();
|
||||
assert_eq!(d.v.unwrap().v, "HELLO!");
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct Basic {
|
||||
pub a: String,
|
||||
pub b: u32,
|
||||
pub c: u8,
|
||||
}
|
||||
impl Default for Basic {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
a: String::new(),
|
||||
b: 0,
|
||||
c: 12,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ConfigField for Basic {
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||
let mut r = Basic::default();
|
||||
let mut set = [false; 3];
|
||||
parse_auto_struct_with(v, |k, val| {
|
||||
match k {
|
||||
"a" => {
|
||||
r.a.parse_cli(Some(val))?;
|
||||
set[0] = true;
|
||||
}
|
||||
"b" => {
|
||||
r.b.parse_cli(Some(val))?;
|
||||
set[1] = true;
|
||||
}
|
||||
"c" => {
|
||||
r.c.parse_cli(Some(val))?;
|
||||
set[2] = true;
|
||||
}
|
||||
_ => return Ok(false),
|
||||
}
|
||||
Ok(true)
|
||||
})?;
|
||||
// a and b are required
|
||||
if !set[0] || !set[1] {
|
||||
return Err(ArgError::InvalidValue);
|
||||
}
|
||||
*self = r;
|
||||
Ok(())
|
||||
}
|
||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
||||
writeln!(
|
||||
f.writer,
|
||||
"{} = a:{},b:{},c:{}",
|
||||
f.name, self.a, self.b, self.c
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_struct_with_basic_fields() {
|
||||
one_field!(S, Basic, Basic::default());
|
||||
let mut d = S::raw_default();
|
||||
d.parse_field("v", Some("a:hello,b:42")).unwrap();
|
||||
assert_eq!(d.v.a, "hello");
|
||||
assert_eq!(d.v.b, 42);
|
||||
assert_eq!(d.v.c, 12);
|
||||
|
||||
d.parse_field("v", Some("a:world,b:84,c:24")).unwrap();
|
||||
assert_eq!(d.v.a, "world");
|
||||
assert_eq!(d.v.b, 84);
|
||||
assert_eq!(d.v.c, 24);
|
||||
|
||||
assert_eq!(
|
||||
d.parse_field("v", Some("a:hello")),
|
||||
Err(ArgError::InvalidValue)
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Default)]
|
||||
pub enum Tu {
|
||||
#[default]
|
||||
None,
|
||||
A(u8),
|
||||
B(u8),
|
||||
C,
|
||||
D(String),
|
||||
}
|
||||
impl ConfigField for Tu {
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||
let (tag, rest) = split_tagged_union(v);
|
||||
*self = match tag {
|
||||
"a" => {
|
||||
let mut x = 0u8;
|
||||
x.parse_cli(Some(rest))?;
|
||||
Tu::A(x)
|
||||
}
|
||||
"b" => {
|
||||
let mut x = 0u8;
|
||||
x.parse_cli(Some(rest))?;
|
||||
Tu::B(x)
|
||||
}
|
||||
"c" => {
|
||||
if !rest.is_empty() {
|
||||
return Err(ArgError::InvalidValue);
|
||||
}
|
||||
Tu::C
|
||||
}
|
||||
"d" => Tu::D(rest.to_string()),
|
||||
_ => return Err(ArgError::InvalidValue),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
fn format_entry(&self, _f: &mut EntryFormatter<'_>) -> fmt::Result {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_tagged_union() {
|
||||
one_field!(S, Tu, Tu::None);
|
||||
let mut d = S::raw_default();
|
||||
d.parse_field("v", Some("a:1")).unwrap();
|
||||
assert_eq!(d.v, Tu::A(1));
|
||||
d.parse_field("v", Some("b:2")).unwrap();
|
||||
assert_eq!(d.v, Tu::B(2));
|
||||
d.parse_field("v", Some("c")).unwrap();
|
||||
assert_eq!(d.v, Tu::C);
|
||||
d.parse_field("v", Some("d:hello")).unwrap();
|
||||
assert_eq!(d.v, Tu::D("hello".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_tagged_union_unknown_field() {
|
||||
one_field!(S, Tu, Tu::None);
|
||||
let mut d = S::raw_default();
|
||||
assert_eq!(d.parse_field("v", Some("x:1")), Err(ArgError::InvalidValue));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_tagged_union_invalid_field_value() {
|
||||
one_field!(S, Tu, Tu::None);
|
||||
let mut d = S::raw_default();
|
||||
assert_eq!(
|
||||
d.parse_field("v", Some("a:hello")),
|
||||
Err(ArgError::InvalidValue)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_into_field_tagged_union_missing_tag() {
|
||||
one_field!(S, Tu, Tu::None);
|
||||
let mut d = S::raw_default();
|
||||
assert_eq!(d.parse_field("v", Some("a")), Err(ArgError::InvalidValue));
|
||||
assert_eq!(d.parse_field("v", Some(":a")), Err(ArgError::InvalidValue));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn args_iterator() {
|
||||
let child = split_args("--what +list-things --a=42").into_iter();
|
||||
let mut iter = ArgsIterator::new(child);
|
||||
assert_eq!(iter.next().unwrap(), "--what");
|
||||
assert_eq!(iter.next().unwrap(), "--a=42");
|
||||
assert_eq!(iter.next(), None);
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
fn line_iter(s: &str) -> LineIterator<Cursor<Vec<u8>>> {
|
||||
LineIterator::new(Cursor::new(s.as_bytes().to_vec()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_iterator() {
|
||||
let mut iter = line_iter(
|
||||
"A\nB=42\nC\n\n# A comment\nD\n\n # An indented comment\n E\n\n# A quoted string with whitespace\nF= \"value \"",
|
||||
);
|
||||
assert_eq!(iter.next().unwrap(), "--A");
|
||||
assert_eq!(iter.next().unwrap(), "--B=42");
|
||||
assert_eq!(iter.next().unwrap(), "--C");
|
||||
assert_eq!(iter.next().unwrap(), "--D");
|
||||
assert_eq!(iter.next().unwrap(), "--E");
|
||||
assert_eq!(iter.next().unwrap(), "--F=value ");
|
||||
assert_eq!(iter.next(), None);
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_iterator_end_in_newline() {
|
||||
let mut iter = line_iter("A\n\n");
|
||||
assert_eq!(iter.next().unwrap(), "--A");
|
||||
assert_eq!(iter.next(), None);
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_iterator_spaces_around_equals() {
|
||||
let mut iter = line_iter("A = B\n\n");
|
||||
assert_eq!(iter.next().unwrap(), "--A=B");
|
||||
assert_eq!(iter.next(), None);
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_iterator_no_value() {
|
||||
let mut iter = line_iter("A = \n\n");
|
||||
assert_eq!(iter.next().unwrap(), "--A=");
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_iterator_with_crlf_line_endings() {
|
||||
let mut iter = line_iter("A\r\nB = C\r\n");
|
||||
assert_eq!(iter.next().unwrap(), "--A");
|
||||
assert_eq!(iter.next().unwrap(), "--B=C");
|
||||
assert_eq!(iter.next(), None);
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_iterator_with_buffered_reader() {
|
||||
let r = std::io::BufReader::with_capacity(2, Cursor::new(b"A\nB = C\n".to_vec()));
|
||||
let mut iter = LineIterator::new(r);
|
||||
assert_eq!(iter.next().unwrap(), "--A");
|
||||
assert_eq!(iter.next().unwrap(), "--B=C");
|
||||
assert_eq!(iter.next(), None);
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_iterator_with_buffered_and_primed_reader() {
|
||||
use std::io::BufRead;
|
||||
let mut r = std::io::BufReader::with_capacity(2, Cursor::new(b"A\nB = C\n".to_vec()));
|
||||
r.fill_buf().unwrap();
|
||||
let mut iter = LineIterator::new(r);
|
||||
assert_eq!(iter.next().unwrap(), "--A");
|
||||
assert_eq!(iter.next().unwrap(), "--B=C");
|
||||
assert_eq!(iter.next(), None);
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_int_base0_forms() {
|
||||
assert_eq!(parse_int_base0("0x_1"), None);
|
||||
assert_eq!(parse_int_base0("1_000"), Some(1000));
|
||||
assert_eq!(parse_int_base0("-0b11"), Some(-3));
|
||||
assert_eq!(parse_int_base0("0o17"), Some(15));
|
||||
assert_eq!(parse_int_base0(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zig_string_literal_decode() {
|
||||
assert_eq!(
|
||||
parse_zig_string_literal("\"a\\n\\x41\\u{1F47B}\"").unwrap(),
|
||||
"a\nA\u{1F47B}"
|
||||
);
|
||||
assert_eq!(parse_zig_string_literal("\"\\q\""), None);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user