bpaf

repository·master·Indexed 19 days ago

https://github.com/pacak/bpaf

A lightweight, flexible command-line argument parser for Rust (version 0.9.27) that supports both a derive-based macro approach and a functional combinatoric API. It features dynamic shell completion for Bash, Zsh, Fish, and Elvish, as well as tools for transforming, validating, and reusing parsers.

Tokens
28.3K
Snippets
97
Records
149
Agent score
60%

What's inside bpaf

  1. Overview of bpaf APIs

    master

    bpaf is a lightweight and flexible command line argument parser for Rust that provides two distinct styles of API:

    1. Derive API: Uses procedural macros to generate parsers from structs. It is generally more convenient and requires less typing.
    2. Combinatoric API: Uses functional combinators to build parsers. It offers maximum flexibility and can reduce boilerplate by avoiding the need for intermediate structs.

    You can mix and match both APIs within the same project. Both APIs share the same underlying features, keywords, and structure.

  2. Use option arguments with values

    master

    Option arguments are similar to standard options but require an additional value. These values can be provided in several formats:

    • Separated by a space: --option value
    • Using an equals sign: --option=value
    • Directly adjacent to a short name: -oValue

    In the generated help messages, these arguments are typically represented by a placeholder metavariable (an all-caps word describing the value, such as NAME, AGE, or SPEC). The relative position of these arguments in the command line usually does not matter.

    $ cargo build --package bpaf
    $ cargo test -j2
    $ cargo check --bin=megapotato
  3. Understand switches and flags in bpaf

    master

    In bpaf, switches (also known as flags) are arguments that do not carry additional data; their presence or absence is the only information they provide. They typically follow standard CLI conventions:

    • Short options: Single dash (e.g., -v). Multiple short options can often be squashed together (e.g., -vvv is equivalent to -v -v -v).
    • Long options: Double dash (e.g., --alpha).
    • Ordering: The relative position of switches usually does not matter (e.g., --alpha --beta is equivalent to --beta --alpha).

    To implement these in your parser, use the NamedArg::switch or NamedArg::flag methods.

    $ cargo --help
    $ ls -la
    $ ls --time --reverse
  4. Define groups of options that can be specified multiple times

    master

    In bpaf, you can define groups of options that are treated as a single unit and can be repeated multiple times in a command line invocation. When a group is repeated, bpaf ensures that each instance is kept without overwriting the previous ones. This is useful for configuring multiple entities of the same type, such as multiple sensors, network interfaces, or database connections, where each entity requires a specific set of related flags.

     $ prometheus_sensors_exporter \
         --sensor \
             --sensor-device=tmp102 \
             --sensor-name="temperature_tmp102_outdoor" \
             --sensor-i2c-bus=0 \
             --sensor-i2c-address=0x48 \
         --sensor \
             --sensor-device=tmp102 \
             --sensor-name="temperature_tmp102_indoor" \
             --sensor-i2c-bus=1 \
             --sensor-i2c-address=0x49 \
  5. Understanding Functors in the context of bpaf

    master

    A Functor is a design pattern that allows you to apply a function to a value inside a context (like Option<T> or Result<T, E>) without changing the structure of that context. In bpaf, this concept is used to transform the values produced by parsers.

    For example, if a parser produces a u32, you can use functor-like mapping to transform that value into a different type or modify it, while the Parser still maintains its identity as a parser.

  6. How commands and subcommands work in bpaf

    master

    Commands (or subcommands) allow a single application to perform multiple different functions by starting a new parser when a specific keyword is encountered. Unlike positional arguments which represent a single item, a command triggers a new parser context that includes its own help text and arguments. Once a command is matched, the parser handles all command-line options and arguments located to the right of the command name.

    Common patterns for commands include tools like cargo, where build, clippy, or asm act as subcommands that change the parser's behavior and available flags.

    # Example pattern of command usage in CLI
    $ cargo build --release
    $ cargo clippy
    $ cargo asm --intel --everything
  7. Transform and validate parsers

    master

    You can transform, validate, and reuse parsers using a functional approach. This allows you to build complex logic from simple, reusable primitives.

    • Reuse: Export functions that return impl Parser<T> to share logic across subcommands.
    • Repetition: Use .many() to collect multiple occurrences of a flag into a Vec<T>.
    • Fallback: Use .fallback(value) to provide a default if the parser is not present.
    • Validation: Use .guard(|value| condition, "error message") to enforce constraints.
    • Transformation: Use .map(|value| new_type) to convert the parsed value into a different type.
    fn speed() -> impl Parser<f64> {
        long("speed")
            .help("Speed in KPH")
            .argument::<f64>("SPEED")
    }
    
    // Collects multiple `--speed` flags into a vector
    fn multiple_args() -> impl Parser<Vec<f64>> {
        speed().many()
    }
    
    // Uses 42.0 if `--speed` is not present
    fn with_fallback() -> impl Parser<f64> {
        speed().fallback(42.0)
    }
    
    #[derive(Clone, Debug)]
    struct Speed(f64);
    
    fn speed_with_validation() -> impl Parser<Speed> {
        long("speed")
            .help("Speed in KPH")
            .argument::<f64>("SPEED")
            .guard(|&speed| speed >= 0.0, "You need to buy a DLC to move backwards")
            .guard(|&speed| speed <= 100.0, "You need to buy a DLC to break the speed limits")
            .map(|speed| Speed(speed))
    }
  8. Understanding Alternative Functors in the context of bpaf

    master

    The Alternative abstraction extends Applicative by providing a way to combine two values in a context into one, typically representing a choice.

    In Rust terms, this is analogous to Option::or. In bpaf, this allows you to define parsers that can succeed if either one of several options is provided, which is essential for handling mutually exclusive command-line arguments or providing fallback options.

  9. Choose between Combinatoric and Derive APIs in bpaf

    master

    bpaf provides two distinct styles for building command line parsers. You can use them independently or mix and match both within a single parser.

    • Combinatoric API: Involves more manual typing but has no dependency on procedural macros and provides better IDE support (autocompletion, type checking).
    • Derive API: Uses procedural macros (bpaf_derive) to reduce boilerplate and typing, though IDE support for the generated code may be limited.
  10. How bpaf ensures correctness through representation

    master

    bpaf ensures correctness by only accepting items that can be represented in the output type. If a parser cannot represent a specific combination of inputs in its target type, it will reject them.

    Example Behavior: If your parser accepts both --intel and --att flags but encodes the result into an enum Style { Intel, Att }:

    • Using both --intel and --att at once will be rejected because the enum can only hold one variant.
    • If the parser is configured to collect multiple styles into a Vec<Style>, then any combination of those flags will be accepted.
  11. Understanding Applicative Functors in the context of bpaf

    master

    While a Functor allows you to transform a single value in a context, an Applicative Functor allows you to combine multiple values in a context.

    In bpaf, this is the core mechanism used to build complex structures from individual parsers. For example, if you have three separate parsers for three different fields, Applicative logic allows you to combine them into a single struct. This is the power behind the construct! macro, which composes several values according to Applicative laws.