Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.3.2 (unreleased)

New:

- Add `Parser::set_short_equals()` to optionally disable the nonstandard `-o=value` syntax.

## 0.3.1 (2025-03-31)

New:
Expand Down Expand Up @@ -62,4 +68,5 @@ Bug fixes:
- Include `bin_name` in `Parser`'s `Debug` output.

## 0.1.0 (2021-07-16)

Initial release.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ The following conventions are supported:
- Long options (`--verbose`)
- `--` to mark the end of options
- `=` to separate options from values (`--option=value`, `-o=value`)
- The nonstandard `-o=value` syntax can be [disabled](https://docs.rs/lexopt/latest/lexopt/struct.Parser.html#method.set_short_equals).
- Spaces to separate options from values (`--option value`, `-o value`)
- Unseparated short options (`-ovalue`)
- Combined short options (`-abc` to mean `-a -b -c`)
Expand Down
4 changes: 4 additions & 0 deletions examples/posixly_correct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@
//!
//! Note that most modern software doesn't follow POSIX's rule and allows
//! options anywhere (as long as they come before "--").
//!
//! [`short_equals`][lexopt::Parser::set_short_equals] also diverges
//! from POSIX (but is otherwise unrelated).

fn main() -> Result<(), lexopt::Error> {
use lexopt::prelude::*;

let mut parser = lexopt::Parser::from_env();
parser.set_short_equals(false);
let mut free = Vec::new();
while let Some(arg) = parser.next()? {
match arg {
Expand Down
6 changes: 6 additions & 0 deletions fuzz/fuzz_targets/fuzz_target_1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ fuzz_target!(|data: &[u8]| {
} else {
decisions = 0;
}
let mut set_short_equals = true;
if data.len() >= 1 {
set_short_equals = data[0] % 2 == 0;
data = &data[1..];
}
let data: Vec<_> = data
// Arguments can't contain null bytes (on Unix) so it's a
// reasonable separator
Expand All @@ -24,6 +29,7 @@ fuzz_target!(|data: &[u8]| {
.map(OsString::from_vec)
.collect();
let mut p = lexopt::Parser::from_args(data);
p.set_short_equals(set_short_equals);
loop {
// 0 -> Parser::next()
// 1 -> Parser::value()
Expand Down
214 changes: 209 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ pub struct Parser {
last_option: LastOption,
/// The name of the command (argv\[0\]).
bin_name: Option<String>,
short_equals: bool,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -184,10 +185,11 @@ impl Parser {
}
// If we find "-=[...]" we interpret it as an option.
// If we find "-o=..." then there's an unexpected value.
// ('-=' as an option exists, see https://linux.die.net/man/1/a2ps.)
// ('-=' as an option exists, see https://linux.die.net/man/1/a2ps.
// Though if you have one you should maybe disable short_equals.)
// clap always interprets it as a short flag in this case, but
// that feels sloppy.
Ok(Some('=')) if *pos > 1 => {
Ok(Some('=')) if *pos > 1 && self.short_equals => {
return Err(Error::UnexpectedValue {
option: self.format_last_option().unwrap(),
value: self.optional_value().unwrap(),
Expand Down Expand Up @@ -215,7 +217,7 @@ impl Parser {
Ok(None) => {
self.state = State::None;
}
Ok(Some('=')) if *pos > 1 => {
Ok(Some('=')) if *pos > 1 && self.short_equals => {
return Err(Error::UnexpectedValue {
option: self.format_last_option().unwrap(),
value: self.optional_value().unwrap(),
Expand Down Expand Up @@ -431,6 +433,9 @@ impl Parser {
/// `--opt=b c` will only yield `"b"` while `-a b c`, `-ab c` and `--opt b c` will
/// yield `"b"`, `"c"`.
///
/// (If [`short_equals`](Self::set_short_equals) is disabled then `-a=b c` will yield
/// `"=b"`, `"c"`.)
///
/// # Errors
/// If not at least one value is found then [`Error::MissingValue`] is returned.
///
Expand Down Expand Up @@ -663,7 +668,7 @@ impl Parser {
return None;
}
let mut had_eq_sign = false;
if arg[pos] == b'=' {
if arg[pos] == b'=' && self.short_equals {
// -o=value.
// clap actually strips out all leading '='s, but that seems silly.
// We allow `-xo=value`. Python's argparse doesn't strip the = in that case.
Expand All @@ -688,7 +693,7 @@ impl Parser {
return None;
}
let mut had_eq_sign = false;
if arg[pos] == b'=' as u16 {
if arg[pos] == b'=' as u16 && self.short_equals {
pos += 1;
had_eq_sign = true;
}
Expand All @@ -712,6 +717,7 @@ impl Parser {
Ok(text) => text,
Err(text) => text.to_string_lossy().into_owned(),
}),
short_equals: true,
}
}

Expand Down Expand Up @@ -774,6 +780,54 @@ impl Parser {
_ => unreachable!(),
}
}

/// Configure whether to parse an equals sign (`=`) for short options.
///
/// If this is **true** (the default), `-o=foobar` will be interpreted as
/// the option `-o` with the value `foobar`.
///
/// If this is **false**, `-o=foobar` will be interpreted as
/// the option `-o` with the value `=foobar`.
///
/// Note that even if this is `true` the equals sign is optional. That is,
/// `-ofoobar` and `-o foobar` are always interpreted as `-o` with the value
/// `foobar` regardless of this setting.
///
/// Most other argument parsers treat the equals sign as part of the value,
/// but the syntax is notably accepted by [`clap`](https://docs.rs/clap/latest/clap/)
/// and by Python's [`argparse`](https://docs.python.org/3/library/argparse.html).
///
/// You may want to disable this setting if it's common for an option's value
/// to start with an equals sign. The Unix `cut` command for example is sometimes
/// used with `-d=`, where `"="` is the value belonging to the `-d` option. By default
/// the empty string `""` is parsed instead.
///
/// # Example
///
/// You can configure this right after creating the parser:
/// ```
/// let mut parser = lexopt::Parser::from_env();
/// parser.set_short_equals(false);
/// ```
///
/// You could also do it temporarily, for an individual option:
/// ```
/// # fn main() -> Result<(), lexopt::Error> {
/// # use lexopt::prelude::*;
/// # let mut parser = lexopt::Parser::from_args(&["-d=", "key=value"]);
/// # let mut delimiter = None;
/// # while let Some(arg) = parser.next()? {
/// # match arg {
/// Short('d') | Long("delimiter") => {
/// parser.set_short_equals(false);
/// delimiter = Some(parser.value()?.string()?);
/// parser.set_short_equals(true);
/// }
/// # _ => (), }} Ok(()) }
/// ```
pub fn set_short_equals(&mut self, on: bool) {
self.short_equals = on;
}
}

impl Arg<'_> {
Expand Down Expand Up @@ -1516,6 +1570,156 @@ mod tests {
Ok(())
}

#[test]
fn short_opt_equals_sign_disabled() -> Result<(), Error> {
let mut p = parse("-d= -d=value -dvalue -d");
p.set_short_equals(false);

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(p.value()?, "=");

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(p.value()?, "=value");

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(p.value()?, "value");

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(
p.value().unwrap_err().to_string(),
"missing argument for option '-d'"
);

assert_eq!(p.next()?, None);

let mut p = parse("-d= -d=value -dvalue -d");
p.set_short_equals(false);

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(p.optional_value().unwrap(), "=");

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(p.optional_value().unwrap(), "=value");

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(p.optional_value().unwrap(), "value");

assert_eq!(p.next()?.unwrap(), Short('d'));

assert_eq!(p.optional_value(), None);
assert_eq!(p.next()?, None);

let mut p = parse("-d= -d=v -dv -d -=");
p.set_short_equals(false);

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(p.next()?.unwrap(), Short('='));

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(p.next()?.unwrap(), Short('='));
assert_eq!(p.next()?.unwrap(), Short('v'));

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(p.next()?.unwrap(), Short('v'));

assert_eq!(p.next()?.unwrap(), Short('d'));

assert_eq!(p.next()?.unwrap(), Short('='));

assert_eq!(p.next()?, None);

let mut p = parse("-d 1 2 -d1 2 -d=1 2 -d=");
p.set_short_equals(false);

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(p.values()?.collect::<Vec<_>>(), &["1", "2"]);

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(p.values()?.collect::<Vec<_>>(), &["1", "2"]);

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(p.values()?.collect::<Vec<_>>(), &["=1", "2"]);

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(p.values()?.collect::<Vec<_>>(), &["="]);

assert_eq!(p.next()?, None);

// Windows has a separate codepath for invalid Unicode.
#[cfg(any(unix, windows, all(target_os = "wasi", target_env = "p1")))]
{
let mut p = Parser::from_args(&[
bad_string("-d=@"),
bad_string("-d=@"),
bad_string("-d=@"),
bad_string("-d=@"),
bad_string("@"),
bad_string("-@"),
]);
p.set_short_equals(false);
assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(p.next()?.unwrap(), Short('='));
assert_eq!(p.next()?.unwrap(), Short('�'));

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(p.value()?, bad_output_string("=@"));

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(p.optional_value(), Some(bad_output_string("=@")));

assert_eq!(p.next()?.unwrap(), Short('d'));
assert_eq!(
p.values()?.collect::<Vec<_>>(),
// First one was sliced, so sanitized on WASI (bad_output_string)
// Second one is passed through even on WASI (bad_string)
&[bad_output_string("=@"), bad_string("@")],
);

assert_eq!(p.next()?.unwrap(), Short('�'));

assert_eq!(p.next()?, None);
}

Ok(())
}

/// It's possible to disable this setting for a single method call.
/// This might break if we parse the equals sign any earlier than needed.
#[test]
fn short_opt_equals_sign_temporarily_disabled() -> Result<(), Error> {
let mut p = parse("-o= -o= -o= -o= -o= -o=");
assert_eq!(p.next()?.unwrap(), Short('o'));
p.set_short_equals(false);
assert_eq!(p.next()?.unwrap(), Short('='));
p.set_short_equals(true);

assert_eq!(p.next()?.unwrap(), Short('o'));
assert_eq!(
p.next().unwrap_err().to_string(),
r#"unexpected argument for option '-o': """#,
);

assert_eq!(p.next()?.unwrap(), Short('o'));
p.set_short_equals(false);
assert_eq!(p.value()?, "=");
p.set_short_equals(true);

assert_eq!(p.next()?.unwrap(), Short('o'));
assert_eq!(p.value()?, "");

assert_eq!(p.next()?.unwrap(), Short('o'));
p.set_short_equals(false);
assert_eq!(p.optional_value().unwrap(), "=");
p.set_short_equals(true);

assert_eq!(p.next()?.unwrap(), Short('o'));
assert_eq!(p.value()?, "");

assert_eq!(p.next()?, None);

Ok(())
}

#[test]
fn bin_name() {
assert_eq!(
Expand Down