Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Option Value Binding or ArgMatches Traversal #5727

Open
2 tasks done
JS-Zheng opened this issue Sep 11, 2024 · 1 comment
Open
2 tasks done

Support for Option Value Binding or ArgMatches Traversal #5727

JS-Zheng opened this issue Sep 11, 2024 · 1 comment
Labels
C-enhancement Category: Raise on the bar on expectations

Comments

@JS-Zheng
Copy link

JS-Zheng commented Sep 11, 2024

Please complete the following tasks

Clap Version

4.5.11

Describe your use case

I am developing a GNU rm-like tool and need to support options like:

  • -i: prompt always
  • -I: prompt once
  • -f: force
  • --interactive[=WHEN]: prompt according to WHEN: never, once (-I), or always (-i); without WHEN, prompt always

I am following the GNU "last one wins" precedence rule. When two mutually exclusive options are provided (e.g., -i and -f), the last one specified on the command line should override the previous ones.

Additionally, I want to extend this behavior to support levels for interactive and force options. For example, -i -i would set the interactive level = 2, and -fff would set the force level = 3.

Clap handles the "last one wins" behavior using overrides_with_all, and I used ArgAction::Count for level counting.

.arg(
    Arg::new(ARG_FORCE)
        .short('f')
        .long(ARG_FORCE)
        .help("Ignore nonexistent files and arguments, never prompt")
        .overrides_with_all(&[ARG_PROMPT_ALWAYS, ARG_PROMPT_ONCE, ARG_INTERACTIVE])
        .action(ArgAction::Count),
)
.arg(
    Arg::new(ARG_PROMPT_ALWAYS)
        .short('i')
        .help("Prompt before every removal")
        .overrides_with_all(&[ARG_PROMPT_ONCE, ARG_FORCE])
        .action(ArgAction::Count),
)
.arg(
    Arg::new(ARG_PROMPT_ONCE)
        .short('I')
        .help("Prompt once before removing more than three files, or when removing recursively")
        .overrides_with_all(&[ARG_PROMPT_ALWAYS, ARG_FORCE])
        .action(ArgAction::Count),
)

However, implementing the long option --interactive=WHEN alongside short options like -i and -I poses challenges. Specifically:

  • -i cannot be aliased to --interactive=always
  • -I cannot be aliased to --interactive=once
  • Currently, Clap does not allow for traversing ArgMatches in a way that can respect option order, which is crucial for determining precedence in cases like rm -I --interactive=always -I, where -I should win, but it's difficult to ensure this given the current API.
Arg::new("interactive")
    .long("interactive")
    .help("Prompt according to WHEN: never, once (-I), or always (-i). Without WHEN, prompts always")
    .value_name("WHEN")
    .value_parser(arg_interactive_parser)
    .num_args(0..=1)
    .require_equals(true)
    .default_missing_value("always")
    .overrides_with_all(&[ARG_FORCE])
    .action(ArgAction::Append),

Describe the solution you'd like

I propose two features:

  1. Support for ArgMatches Traversal
    Traversing ArgMatches would allow me to check the order in which options were provided, making it possible to implement the "last one wins" precedence rule accurately.

  2. Option Value Binding
    This would allow short options to be bound to specific values of long options. For example, -i would be treated as an alias for --interactive=always, and -I would map to --interactive=once. This would provide an elegant solution to handle the complexity of combining long and short options with the same meaning.

Example (pseudo-code):

Arg::new("interactive")
    .long("interactive")
    .value_name("WHEN")
    .value_parser(["always", "once", "never"])
    .bind_to("-i", "always") // Bind short option -i to --interactive=always
    .bind_to("-I", "once")   // Bind short option -I to --interactive=once
    .bind(Fn)                // Bind by Closure

In addition to allowing option precedence, this feature could benefit other tools that rely on complex option parsing and precedence rules. It would also make it easier to support aliasing of options and reduce manual argument handling for developers.

Alternatives, if applicable

An alternative would be to manually parse the arguments in sequence and manage conflicts ourselves, but this would result in more complex code, reduce the benefits of using Clap, and potentially introduce bugs. Supporting these features natively in Clap would be much cleaner and more reliable.

Additional Context

Related issue: #1206

@JS-Zheng JS-Zheng added the C-enhancement Category: Raise on the bar on expectations label Sep 11, 2024
@epage
Copy link
Member

epage commented Sep 17, 2024

I propose two features:

Please keep issues focused on a single feature. It helps keep the conversation focused and makes the state clear as we have a single status (close, rejected) for an issue.

Support for ArgMatches Traversal
Traversing ArgMatches would allow me to check the order in which options were provided, making it possible to implement the "last one wins" precedence rule accurately.

We support position-sensitive arguments, though it might not be the most intuitive, see https://docs.rs/clap/latest/clap/_derive/_cookbook/find/index.html

fn position_sensitive_flag(arg: Arg) -> Arg {
    // Flags don't track the position of each occurrence, so we need to emulate flags with
    // value-less options to get the same result

Huh, that shouldn't be the case anymore and we should be able to drop that part from the example

Option Value Binding
This would allow short options to be bound to specific values of long options. For example, -i would be treated as an alias for --interactive=always, and -I would map to --interactive=once. This would provide an elegant solution to handle the complexity of combining long and short options with the same meaning.

We previously had Arg::replace which ran into issues, see #2836. We'll need this more fleshed out to evaluate how to move forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-enhancement Category: Raise on the bar on expectations
Projects
None yet
Development

No branches or pull requests

2 participants