Skip to content
This repository was archived by the owner on Feb 11, 2026. It is now read-only.
This repository was archived by the owner on Feb 11, 2026. It is now read-only.

Make Arguments composable and reuseable #21

@tertsdiepraam

Description

@tertsdiepraam

Many of the coreutils share arguments, possibly with some slight variations. Some examples:

  • head & tail
  • cksum, sum, b2sum, sha1sum, etc.
  • base32, base64, basenc
  • mv, cp

We could copy-paste the Arguments enums for each of these, but I think we can do better.

Essentially, it must be possible to compose multiple Arguments enums. Let's first establish what's currently possible. Given two enums Arg1 and Arg2, we could make a new enum Arg for which the Arg::next_arg calls Arg1::next_arg and Arg2::next_arg in order. That works somewhat but has a problem with abbreviations of long options, because Arg1::next_arg and Arg2::next_arg do not know about each other's long arguments. They also don't know about each other's positional indices.

So we have to break up the trait into multiple methods. One for each of the following operations, which can be composed:

  • Parse short option.
  • Get an iterator of long options.
  • Parse long option.
  • Get the maximum number of positional arguments.
  • Parse positional argument.
  • Get the help text per argument (so we can sort it)
  • (And parsing free arguments once those are implemented)

The next_arg method can then be provided based on these, possibly only implemented by ArgumentsIter.

That's all the internal plumbing that needs to change, but what does it look like in the API?

One option is to provide a macro that creates an automatic implementation:

enum Arg1 { ... }
enum Arg2 { ... }

compose_args!(Arg, [Arg1, Arg2])

That works but I think I prefer supporting it via the derive macro instead, which makes it easier how it's structured:

#[derive(Arguments)]
enum Arg {
    #[include] A(Arg1),
    #[include] B(Arg2),

    #[arg("-a")]
    All,
}

This also provides a nice way of grouping options in --help. A variation on this design makes a distinction between an ArgumentGroup and Arguments, where the former can be included into the latter. This makes sense, because Arguments have, for instance, about text and version info, whereas ArgumentGroup doesn't need that. Additionally, we can restrict ArgumentGroup to not include positional arguments, because those get hard to reason about (and hard to implement in proc macros).

Taking this further can lead to some pretty cool stuff, by combining ArgumentGroup with Value. For example, we can do this:

#[derive(ArgumentGroup, Value)]
enum Format {
    #[arg("--long")]
    #[value("long")]
    Long,
    #[value("columns")]
    Columns,
}

#[derive(Arguments)]
enum Arg {
    #[include]
    #[arg("--format=FORMAT")]
    Format(Format),
}

We could also allow the --format argument to be defined like this:

#[derive(ArgumentGroup, Value)]
#[option("--format=FORMAT")]
enum Format { ... }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions