Clap completion question

Let's say I have a program claptest like this:

Test program for clap --ignore flag and shell completion

Usage: claptest [OPTIONS]

Options:
      --ignore <IGNORE>...
          Comma-separated list of things to ignore: blabla, something, other
          
          [possible values: blabla, something, other]

      --completion <SHELL>
          Generate shell completion script and print to stdout.
          
          Examples: claptest --completion fish > ~/.config/fish/completions/claptest.fish claptest --completion bash > ~/.bash_completion.d/claptest claptest --completion zsh  > ~/.zsh/completions/_claptest
          
          [possible values: bash, elvish, fish, powershell, zsh]

  -h, --help
          Print help (see a summary with '-h')

I like to have completion for a call to claptest like this claptest --ignore blabla,something
I get completion only for one of blabla, something or `other'

Here my source

use std::io;

use clap::{CommandFactory, Parser, ValueEnum};
use clap_complete::{Shell, generate};

// โ”€โ”€ Ignore values โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

#[derive(Debug, Clone, ValueEnum, PartialEq)]
enum Ignore {
    Blabla,
    Something,
    Other,
}

// โ”€โ”€ CLI definition โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

#[derive(Parser, Debug)]
#[command(
    name = "claptest",
    about = "Test program for clap --ignore flag and shell completion"
)]
struct Args {
    /// Comma-separated list of things to ignore: blabla, something, other
    #[arg(long, value_delimiter = ',', num_args = 1..)]
    ignore: Vec<Ignore>,

    /// Generate shell completion script and print to stdout.
    ///
    /// Examples:
    ///   claptest --completion fish > ~/.config/fish/completions/claptest.fish
    ///   claptest --completion bash > ~/.bash_completion.d/claptest
    ///   claptest --completion zsh  > ~/.zsh/completions/_claptest
    #[arg(long, value_name = "SHELL")]
    completion: Option<Shell>,
}

// โ”€โ”€ Main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

fn main() {
    let args = Args::parse();

    if let Some(shell) = args.completion {
        let mut cmd = Args::command();
        generate(shell, &mut cmd, "claptest", &mut io::stdout());
        return;
    }

    println!(
        "ignore_dupes:      {}",
        args.ignore.contains(&Ignore::Blabla)
    );
    println!(
        "ignore_same_named: {}",
        args.ignore.contains(&Ignore::Something)
    );
    println!(
        "ignore_tmpfiles:   {}",
        args.ignore.contains(&Ignore::Other)
    );
}

Any help appreciated.

either try space as a separator, or repeated flag calls, or look into Dynamic completion support ยท Issue #1232 ยท clap-rs/clap ยท GitHub . though I'm not sure how stable that is.

Thanks for the pointer. Very old but nevertheless very helpful. :grin:

Trying out ArgValueCompleter and things seem to work ok.

Requires: clap_complete = { version = "4", features = ["unstable-dynamic"] } in Cargo.toml

Finally I have this

use std::ffi::OsStr;

use clap::{CommandFactory, Parser, ValueEnum};
use clap_complete::CompleteEnv;
use clap_complete::engine::{ArgValueCompleter, CompletionCandidate};

// โ”€โ”€ Ignore values โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

const IGNORE_VALUES: &[&str] = &["blabla", "something", "other"];

fn ignore_completer(current: &OsStr) -> Vec<CompletionCandidate> {
    let current = current.to_str().unwrap_or("");

    let (prefix, fragment) = match current.rfind(',') {
        Some(pos) => (&current[..=pos], &current[pos + 1..]),
        None => ("", current),
    };

    let already: Vec<&str> = prefix
        .split(',')
        .map(|s| s.trim_end_matches(','))
        .filter(|s| !s.is_empty())
        .collect();

    IGNORE_VALUES
        .iter()
        .filter(|&&v| v.starts_with(fragment) && !already.contains(&v))
        .map(|&v| CompletionCandidate::new(format!("{prefix}{v}")))
        .collect()
}

// โ”€โ”€ Shell enum โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

#[derive(Debug, Clone, ValueEnum)]
enum Shell {
    Fish,
    Bash,
    Zsh,
}

impl Shell {
    fn setup_line(&self, bin: &str) -> String {
        match self {
            Shell::Fish => format!("COMPLETE=fish {bin} | source"),
            Shell::Bash => format!("source <(COMPLETE=bash {bin})"),
            Shell::Zsh => format!("source <(COMPLETE=zsh {bin})"),
        }
    }

    fn config_file(&self, bin: &str) -> String {
        match self {
            Shell::Fish => format!("~/.config/fish/completions/{bin}.fish"),
            Shell::Bash => "~/.bashrc".to_string(),
            Shell::Zsh => "~/.zshrc".to_string(),
        }
    }
    fn usage(&self, bin: &str) -> String {
        let line = self.setup_line(bin);
        let file = self.config_file(bin);
        format!(
            "Add the following line to {file}:

    {line}"
        )
    }
}

// โ”€โ”€ CLI definition โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

#[derive(Parser, Debug)]
#[command(
    name = "claptest",
    about = "Test program for clap --ignore flag and shell completion"
)]
struct Args {
    /// Comma-separated list of things to ignore: blabla, something, other
    #[arg(long, value_delimiter = ',', num_args = 1..,
          add = ArgValueCompleter::new(ignore_completer))]
    ignore: Vec<String>,

    /// Print the dynamic completion setup line for the given shell.
    ///
    /// Add the output to your shell config file to enable completion.
    /// Example: claptest --completion fish >> ~/.config/fish/config.fish
    #[arg(long, value_name = "SHELL")]
    completion: Option<Shell>,
}

// โ”€โ”€ Main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

fn main() {
    CompleteEnv::with_factory(Args::command).complete();

    let args = Args::parse();

    if let Some(shell) = &args.completion {
        println!("{}", shell.usage("claptest"));
        return;
    }

    let ignore_dupes = args.ignore.iter().any(|v| v == "dupes");
    let ignore_same_named = args.ignore.iter().any(|v| v == "same-named");
    let ignore_tmpfiles = args.ignore.iter().any(|v| v == "tmpfiles");

    println!("ignore_dupes:      {ignore_dupes}");
    println!("ignore_same_named: {ignore_same_named}");
    println!("ignore_tmpfiles:   {ignore_tmpfiles}");
}