Skip to content

clap: Handling of ambiguous long options #11380

@GunterSchmidt

Description

@GunterSchmidt

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, "")
        }
    })
}

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