lexopt

repository·master·Indexed 19 days ago

https://github.com/blyxxyz/lexopt

A minimalist, imperative command line argument parser for Rust (v0.3.2). It prioritizes correctness, a small footprint, and the handling of OsString over automatic help generation or macro-based declarations. Lexopt uses a loop-based approach to parse short options, long options, and positional arguments, providing tools for robust Unicode support and raw argument access.

Tokens
4.3K
Snippets
14
Records
18
Agent score
63%

What's inside lexopt

  1. How Lexopt handles Unicode and OsString

    master

    Lexopt is designed to be pedantic and correct regarding character encoding:

    • OsString: Arguments are returned as OsString to allow handling of non-Unicode filenames or arguments safely.
    • Conversion: Use .string() to decode an OsString into a String (which may fail if not valid UTF-8) or use the lexopt::prelude to access .parse() for types that implement FromStr.
    • Unicode Support: The library supports Unicode. Short options must be a single Unicode codepoint (char).
    • Robustness: Options can be combined with non-Unicode arguments without error. If an option name itself is not valid Unicode, it is patched using String::from_utf8_lossy and will likely result in an unrecognized argument error.
  2. Parse command line arguments with Lexopt

    master

    Lexopt uses an imperative, loop-based approach to parsing. Instead of declaring a schema upfront, you iterate through arguments using Parser::from_env() and parser.next().

    Key patterns:

    • Options: Match on Short('c') or Long("name").
    • Option Values: Call parser.value() immediately after matching an option to consume its associated value. This tells the parser that the preceding option requires an argument.
    • Positional Arguments: Match on Value(val). A common pattern for single positional arguments is to check if your target variable is still None.
    • Error Handling: Use arg.unexpected() to return an error for unrecognized arguments. You can also promote strings to errors for custom messages.
    • Help Generation: Lexopt does not generate help text automatically; you must implement the logic to print usage instructions and exit manually.
    use lexopt::prelude::*;
    
    fn parse_args() -> Result<Args, lexopt::Error> {
        let mut thing = None;
        let mut number = 1;
        let mut shout = false;
        let mut parser = lexopt::Parser::from_env();
    
        while let Some(arg) = parser.next()? {
            match arg {
                Short('n') | Long("number") => {
                    number = parser.value()?.parse()?;
                }
                Long("shout") => {
                    shout = true;
                }
                Value(val) if thing.is_none() => {
                    thing = Some(val.string()?);
                }
                Long("help") => {
                    println!("Usage: hello [-n|--number=NUM] [--shout] THING");
                    std::process::exit(0);
                }
                _ => return Err(arg.unexpected()),
            }
        }
    
        Ok(Args {
            thing: thing.ok_or("missing argument THING")?,
            number,
            shout,
        })
    }
  3. Use `lexopt::prelude` for cleaner argument processing

    master

    The lexopt::prelude module exports Arg variants (Short, Long, Value) and the ValueExt trait. Using this prelude allows you to write more concise code without prefixing every argument type with Arg::.

    It is recommended to import the prelude inside the function where you are processing arguments rather than at the module level to avoid namespace pollution.

    fn parse_args() -> Result<(), lexopt::Error> {
        use lexopt::prelude::*;
        // Now you can use Short, Long, Value, and .parse() directly
        // ...
        Ok(())
    }
  4. Parse command line arguments with `Parser`

    master

    Lexopt provides a Parser that yields a stream of arguments (Arg) which you can match against in a loop. Unlike declarative parsers, you manually handle the logic for each option and value.

    Core Workflow

    1. Initialize a parser using Parser::from_env() (for standard CLI apps) or Parser::from_iter() (for testing).
    2. Loop using parser.next() to receive Arg variants: Short(char), Long(&str), or Value(OsString).
    3. For options that require values, call parser.value() immediately after matching the option.
    4. Use arg.unexpected() to convert unhandled arguments into errors.
    use lexopt::prelude::*;
    
    fn parse_args() -> Result<Args, lexopt::Error> {
        let mut thing = None;
        let mut number = 1;
        let mut shout = false;
        let mut parser = lexopt::Parser::from_env();
    
        while let Some(arg) = parser.next()? {
            match arg {
                Short('n') | Long("number") => {
                    number = parser.value()?.parse()?;
                }
                Long("shout") => {
                    shout = true;
                }
                Value(val) if thing.is_none() => {
                    thing = Some(val.string()?);
                }
                Long("help") => {
                    println!("Usage: hello [-n|--number=NUM] [--shout] THING");
                    std::process::exit(0);
                }
                _ => return Err(arg.unexpected()),
            }
        }
    
        Ok(Args {
            thing: thing.ok_or("missing argument THING")?,
            number,
            shout,
        })
    }
  5. Access raw command line arguments

    master

    If you need to implement custom syntax that deviates from standard conventions (for example, treating -123 as a number instead of a list of short options), you can bypass the standard parser using:

    • Parser::raw_args()
    • Parser::try_raw_args()

    These methods provide an escape hatch to consume the original command line arguments directly.

  6. Configure short option equality with set_short_equals

    master

    The Parser can be configured using set_short_equals. This is a niche setting that affects how short options are handled. Because this method takes &mut self, it is context-sensitive; if you change the setting for a specific option, you should revert it once that option is parsed to avoid affecting subsequent parsing.

    // Example of context-sensitive configuration
    parser.set_short_equals(true);
    // ... parse specific option ...
    parser.set_short_equals(false); // Revert to default
  7. Access raw arguments via RawArgs::as_slice

    master
    If you need to observe the underlying arguments being processed (for example, to implement custom logic or lookahead), use the as_slice() method on RawArgs. This provides access to the internal slice of arguments stored by the parser.
  8. Parse values using ValueExt::parse

    master

    To parse an argument value into a specific type while maintaining helpful error messages, use the ValueExt::parse method. This method wraps errors into a uniform lexopt::Error type and includes the original string in the error message to provide better context when parsing fails.

    // Example pattern for parsing a value
    let val: i32 = parser.value_ext().parse()?;
  9. Use ValueExt::string for cleaner string parsing

    master

    As of version 0.3.0, ValueExt provides a string() method. This is an alternative to into_string() that returns a cleaner type, making it easier to chain with .parse() for types like String or other primitives.

    // Preferred workflow for parsing strings or types from strings
    let val: String = parser.value_ext().string()?.parse()?;
  10. Supported command line syntax conventions

    master

    Lexopt supports the following standard argument conventions:

    • Short options: -q
    • Long options: --verbose
    • End of options: -- (marks the end of option parsing)
    • Option-value separation:
      • --option=value or -o=value (Note: -o=value can be disabled via Parser::set_short_equals)
      • --option value or -o value (space-separated)
    • Short option variations:
      • Unseparated: -ovalue
      • Combined: -abc (equivalent to -a -b -c)
    • Advanced parsing:
      • Optional arguments: Use Parser::optional_value() (e.g., for -i or -isuffix).
      • Multiple arguments: Use Parser::values() to consume multiple values for a single option.

    Not supported:

    • Single-dash long options (e.g., -name)
    • Abbreviated long options (e.g., --num for --number)
  11. Handle unexpected arguments with `Arg::unexpected()`

    master

    When matching against Arg variants in your loop, any argument that doesn't match your expected options or positional arguments should be converted into a formal error using arg.unexpected(). This ensures your parser provides consistent error messages for unknown flags or arguments.

    match arg {
        Short('v') => { /* handle verbose */ }
        _ => return Err(arg.unexpected()), // Converts unknown arg to Error
    }
  12. Access raw arguments with `raw_args()` and `try_raw_args()`

    master

    Lexopt allows you to bypass the option/value logic and access the remaining arguments as a raw stream of OsStrings.

    • raw_args(): Returns a RawArgs iterator. If the parser is currently in the middle of processing an option (e.g., a pending value from --opt=val), this returns Err(Error::UnexpectedValue). Use this when you have reached a positional argument and want to treat everything else as a command.
    • try_raw_args(): A safer version that returns None if there is a pending value, making it safe to call at any time.

    RawArgs provides .peek(), .next_if(), and .as_slice() to inspect arguments without consuming them.

    // Example: treating a positional argument as a command
    match arg {
        Value(prog) => {
            let args = parser.raw_args()?.collect::<Vec<_>>();
            let cmd = std::process::Command::new(prog).args(args);
        }
        _ => {}
    }