Search code examples
rustclap

How can I make clap ignore flags after a certain subcommand?


Let's say I'm writing a command-line program called foo, using clap as the arguments parser:

#[derive(Debug, Parser)]
struct Cli {
    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Debug, Subcommand)]
enum Commands {
    Run {
        executable: OsString,
        args: Vec<OsString>,
    },
}

One of the things it's supposed to do is run other command-line programs. For example, to tell foo to run the executable bar, the user would run:

foo run bar

If the user tries to pass flags for bar:

foo run bar --baz

...I would like them to be captured as raw arguments so that they can be passed directly to bar like so:

Cli {
    command: Run {
        executable: "bar",
        args: [
            "--baz",
        ],
    },
}

However instead of capturing it as a raw argument, clap parses it as a flag for foo to handle.

Is there any way to tell clap to ignore any flags it sees after a certain point?


I'm aware that the user could tell clap to stop parsing subsequent arguments as flags like so:

foo run -- bar --baz

I'd like to avoid this workaround if possible, since it's more verbose than necessary and it's somewhat counterintuitive from a user perspective. Usually you put in a -- to avoid having args processed as flags, not the other way around.


Solution

  • A few thing will be helpful here:

    • Arg::trailing_var_arg: Tells clap that this is a "VarArg" and everything that follows should be captured by it, as if the user had used a --.

    • Arg::allow_hyphen_values: Tells clap to allow values which start with a leading hyphen (-).

    • Command::disable_help_flag: Tells clap to not look for a --help flag after this command (or subcommand), and to instead treat it as a raw value.

    These options aren't all supported by the clap derive interface, so you will have to implement the Args trait manually using the builder interface. Here's how you could do that for the example in the question:

    #[derive(Debug, Parser)]
    struct Cli {
        #[command(subcommand)]
        pub command: Commands,
    }
    
    #[derive(Debug, Subcommand)]
    enum Commands {
        Run(RunArgs),
    }
    
    #[derive(Debug)]
    struct RunArgs {
        executable: OsString,
        args: Vec<OsString>,
    }
    
    impl FromArgMatches for RunArgs {
        fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
            let executable = matches
                .get_raw("executable")
                .and_then(|mut executable| executable.next())
                .map(|executable| executable.to_owned())
                .ok_or(clap::Error::new(ErrorKind::MissingRequiredArgument))?;
    
            let args = matches
                .get_raw("args")
                .unwrap_or_default()
                .map(|i| i.to_owned())
                .collect();
    
            Ok(Self { executable, args })
        }
    
        fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), clap::Error> {
            *self = Self::from_arg_matches(matches)?;
    
            Ok(())
        }
    }
    
    impl Args for RunArgs {
        fn augment_args(cmd: Command) -> Command {
            cmd.disable_help_flag(true)
                .arg(
                    Arg::new("executable")
                        .required(true)
                        .allow_hyphen_values(true),
                )
                .arg(
                    Arg::new("args")
                        .action(ArgAction::Append)
                        .allow_hyphen_values(true)
                        .trailing_var_arg(true),
                )
        }
    
        fn augment_args_for_update(cmd: Command) -> Command {
            Self::augment_args(cmd)
        }
    }