clap: Handling of ambiguous long options

Open
#11,380 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
35/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Stale
Tech stack
rust
Domain
cli

Research direction

Start at handle_clap_result_with_exit_code and inspect how clap reports UnknownArgument and InvalidSubcommand errors. Review the existing ErrorFormatter and UError handling before deciding how ambiguous long-option matches should be represented. Done means ambiguous abbreviations list all matching options while help/version and ordinary errors retain their current behavior.

Written by the indexing model from the issue text.

Description

I am sure, this must have come up before, but could not find a discussion on it.

When using abbreviated long options, sometimes the option is not unique, e.g.

$ ls --h
ls: option '--h' is ambiguous; possibilities: '--human-readable' '--hide-control-chars' '--hide' '--hyperlink' '--help'

But clap returns just an error with a single suggestion:

error: unexpected argument '--h' found

tip: a similar argument exists: '--hide'

Usage: ls [OPTION]... [FILE]...

This is less information to the user, who has now to call --help first, if he does not remember the exact option.

Use case: User wants to hide control characters, but only remembers "something with --hide",
so he types "ls --hid" and should get
ls: option '--hi' is ambiguous; possibilities: '--hide-control-chars' '--hide'.
He then can just simply repeat the command (cursor up) and add a few letters: "ls --hide-c".

With uutils this is not possible.

clap should return a list of possible options instead of the first suggested one, but does not support that functionality.

Below is a code change that would output something like:

Error: Argument '--h' is ambiguous.
Did you mean one of these?
  --help
  --hide
  --hide-control-chars
  --human-readable
  --hyperlink 

What is your take on that?

pub fn handle_clap_result_with_exit_code<I, T>(
    mut cmd: Command,
    itr: I,
    exit_code: i32,
) -> UResult<ArgMatches>
where
    I: IntoIterator<Item = T>,
    T: Into<OsString> + Clone,
{
    // cloning args for double use in error case
    let args = itr.into_iter().collect::<Vec<T>>();
    let itr = args.clone();
    // using mut to avoid cloning cmd
    cmd.try_get_matches_from_mut(itr).map_err(|e| {
        if e.exit_code() == 0 {
            e.into() // Preserve help/version
        } else {
            // find ambiguous options
            if e.kind() == ErrorKind::UnknownArgument || e.kind() == ErrorKind::InvalidSubcommand {
                // Find the string the user actually typed (e.g., "--de")
                // for arg in &itr {}
                let args_str: Vec<String> = args
                    .into_iter()
                    .map(|t| {
                        let o: OsString = t.into();
                        o.to_string_lossy().to_string()
                    })
                    .collect();
                if let Some(provided) = args_str.iter().find(|a| a.starts_with("--")) {
                    let search_term = provided.trim_start_matches("--");

                    // Manually filter all defined long arguments
                    let mut matches: Vec<_> = cmd
                        .get_arguments()
                        .filter_map(|arg| arg.get_long())
                        .filter(|l| l.starts_with(search_term))
                        .collect();

                    if matches.len() > 1 {
                        // TODO error handling to UError
                        eprintln!("Error: Argument '{}' is ambiguous.", provided);
                        eprintln!("Did you mean one of these?");
                        matches.sort();
                        for m in matches {
                            eprintln!("  --{}", m);
                        }
                        std::process::exit(1);
                    }
                }
            }

            let formatter = ErrorFormatter::new(crate::util_name());
            let code = formatter.print_error(&e, exit_code);
            USimpleError::new(code, "")
        }
    })
}

Dominant language
Rust
Stars
24.1k
Forks
2k
Avg merge
1d 5h
Merged PRs (30d)
365

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from uutils/coreutils

All issues in uutils/coreutils

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.