-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Partial / Pre Parsing a CLI #1880
Comments
This feature would also be immensely useful for implementing #380, localizing a CLI interface.
This is not quite true. You already mentioned some exceptions such as in the case of parse errors, but there is one more road block that makes that technique unworkable. A number of things happen in This makes the technique useless for localization because by the time we get information about a potential language flag the App has already spit out strings that may have needed localization. |
As a minimal viable solution, we could simply allow ignoring unknown or missing positionals and options. // first "probe" app
let preprocess = App::new("app")
.settings(IgnoreErrors) // also disables --help, version, and others
.arg("--color <color> 'coloring policy: never/always'")
.get_matches(); // will succeed no matter what
let mut app = App::new("main_app");
match preprocess.value_of("color") {
Some("always") => app = app.setting(ColorAlways),
Some("never") => app = app.setting(ColorNever),
_ => {}
}
// real parsing starts here
app.get_matches() This kind or partial parsing would allow for most, if not all, cases and would be fairly easy to implement. |
Yes that does indeed sound like a MVS, and something along the lines of what I expected to be able to hack together already and failed. It's not the most elegant solution maybe, but it would get me by for my use case of localization. |
@alerque It sounds like get_matches_safe could be what you're looking for? That would address the immediate problem of clap prematurely exiting with help or an error. |
@wabain But it wouldn't give you the partial mach, see the color example above |
I am building an app which spawns another process, and needs to "sniff" if certain flags were passed to the subprocess. Having an Right now I believe this is impossible with For some more detail, my app is used like this:
The subprocess supports hundreds of arguments, and I don't want to have to write code to match all of them if I'm just interested in 2 or 3 flags. The |
Let me have some examples because my English is poor and I might miss points. The original problem is "lax position of arguments."
I think this is solved by adding option to This approach will be match with the @acheronfail 's case. I guess there're two types of app:
The former is very limited. The later is similar. Parsing will terminate with first "--". |
@kenoss Thanks for looking into this, we appreciate that. I didn't quite get your comment, so let's try to get on the same page first.
Could you please elaborate? I think this is where the misunderstanding comes from. The original problem in my understanding is to "loosely parse flags, options, and subcommands but ignore positional arguments entirely". The
You have introduced "belongs to" relationship between two arguments. I can't wrap my head around it - arguments can't belong to each other. An argument can "belong" to a subcommand or the top-level app. Maybe you meant The complexity of implementation... Guys, I think we've all been missing the elephant in the room. The current parsing algorithm can't just "ignore" arguments, especially positional ones. Well, It can ignore, but it can't recover. I think this issue belongs to "postponed until after I finally rewrite the parser algorithm" category. |
Sorry for late reply.
I read the issue again and I've got the point. Thanks to catch me up the argument.
Sorry. I misunderstood by
Yes. I've found that this is a critical blocker of #1232 and I should parsing implementation. |
Implemented as AppSetting::Ignore errors as suggested by @CreepySkeleton in clap-rs#1880 (comment). This is not a complete implementation but it works already in surprisingly many situations. clap-rs#1880
Implemented as AppSetting::Ignore errors as suggested by @CreepySkeleton in clap-rs#1880 (comment). This is not a complete implementation but it works already in surprisingly many situations. clap-rs#1880
Implemented as AppSetting::Ignore errors as suggested by @CreepySkeleton in clap-rs#1880 (comment). This is not a complete implementation but it works already in surprisingly many situations. clap-rs#1880
Implemented as AppSetting::Ignore errors as suggested by @CreepySkeleton in clap-rs#1880 (comment). This is not a complete implementation but it works already in surprisingly many situations. clap-rs#1880
In the short term, we now have IgnoreErrors. Longer term, I feel like #1404 is a more general solution and has existing precedence (Python's argparse at least) compared to a If there is any concern with this, feel free to comment and we can re-open this. |
I don't see anything in #1404 covering the use cases I commented on above. An option would need to be added to stop the default short-circuit behavior where the app prints something and exits when encountering some flags, e.g. |
So if I understand, you would define one For the partial parse, couldn't you either set |
Honestly, I hadn't considered that — but it seems like quite a song and dance for what I want. First I'd have to set both of those (since they aren't the default), do one parsing pass, then unset both of them again for the second pass. I would suggest that should be an option on the parsing function (or an alternate function) to disable all current & future Should I open a new issue for that? |
You can, it would be a better place to continue to discuss it. With that said, I am concerned about the number of parsing functions we already have today and would be preferring we find ways to reduce the number, rather than increase it. As long as we provide the building blocks, that is my main concern. How nice we make things past that is dependent on how prevalent the workflow is. Already, you either have to be setting |
fn extract(item: (ContextKind, &ContextValue)) -> Option<&ContextValue> {
let (k, v) = item;
if k == ContextKind::InvalidArg {
return Some(v);
}
None
}
fn parse_known_args(cmd: &Command) -> Result<(ArgMatches, Vec<String>), Error> {
let mut rem: Vec<String> = vec![];
let mut args: Vec<String> = env::args().collect();
loop {
match cmd.clone().try_get_matches_from(&args) {
Ok(matches) => {
return Ok((matches, rem));
},
Err(error) => {
match error.kind() {
ErrorKind::UnknownArgument => {
let items = error.context().find_map(extract);
match items {
Some(x) => {
match x {
ContextValue::String(s) => {
rem.push(s.to_owned());
args.retain(|a| a != s);
},
_ => {
return Err(error);
},
}
},
None => {
return Err(error);
},
}
},
_ => {
return Err(error);
},
}
},
}
}
} fn main() {
let cmd = Command::new("cmd")
.allow_hyphen_values(true)
.trailing_var_arg(true)
.disable_help_flag(true)
.disable_version_flag(true)
.arg(
Arg::new("config")
.takes_value(true)
.short('c')
.long("config")
.help("path to config"),
);
let result = parse_known_args(&cmd);
let (matches, rem) = result.unwrap();
println!("matches={:#?}", matches);
println!("rem={:#?}", rem);
let matches = cmd.get_matches_from(&rem);
} For anyone who comes across this and is looking for an equivalent to Python Argparse's For anyone who doesn't understand why you would want this, it is a VERY common pattern for me to supply the config file in the first pass, that will either effect the default values if not alter the program's options entirely. |
Clap bails parsing when an "error" is encountered, e.g. a subcommand is missing, "--help" is passed, or the "help" subcommand is invoked. This means that the current approach of parsing args does not handle flags like `--no-pager` or `--color` when an error is encountered. Fix this by separating early args into their own struct and preprocessing them using `ignore_errors` (per clap-rs/clap#1880). The early args are in a new `EarlyArgs` struct because of a known bug where `ignore_errors` causes default values not to be respected (clap-rs/clap#4391 specifically calls out bool, but strings may also be ignored), so when `ignore_errors` is given, the default values will be missing and parsing will fail unless the right arg types are used (e.g`. Option`). By parsing only early args (using the new struct) we only need to adjust `no_pager`, instead of adjusting all args with a default value.
Clap bails parsing when an "error" is encountered, e.g. a subcommand is missing, "--help" is passed, or the "help" subcommand is invoked. This means that the current approach of parsing args does not handle flags like `--no-pager` or `--color` when an error is encountered. Fix this by separating early args into their own struct and preprocessing them using `ignore_errors` (per clap-rs/clap#1880). The early args are in a new `EarlyArgs` struct because of a known bug where `ignore_errors` causes default values not to be respected (clap-rs/clap#4391 specifically calls out bool, but strings may also be ignored), so when `ignore_errors` is given, the default values will be missing and parsing will fail unless the right arg types are used (e.g`. Option`). By parsing only early args (using the new struct) we only need to adjust `no_pager`, instead of adjusting all args with a default value.
Clap bails parsing when an "error" is encountered, e.g. a subcommand is missing, "--help" is passed, or the "help" subcommand is invoked. This means that the current approach of parsing args does not handle flags like `--no-pager` or `--color` when an error is encountered. Fix this by separating early args into their own struct and preprocessing them using `ignore_errors` (per clap-rs/clap#1880). The early args are in a new `EarlyArgs` struct because of a known bug where `ignore_errors` causes default values not to be respected (clap-rs/clap#4391 specifically calls out bool, but strings may also be ignored), so when `ignore_errors` is given, the default values will be missing and parsing will fail unless the right arg types are used (e.g`. Option`). By parsing only early args (using the new struct) we only need to adjust `no_pager`, instead of adjusting all args with a default value.
Clap bails parsing when an "error" is encountered, e.g. a subcommand is missing, "--help" is passed, or the "help" subcommand is invoked. This means that the current approach of parsing args does not handle flags like `--no-pager` or `--color` when an error is encountered. Fix this by separating early args into their own struct and preprocessing them using `ignore_errors` (per clap-rs/clap#1880). The early args are in a new `EarlyArgs` struct because of a known bug where `ignore_errors` causes default values not to be respected (clap-rs/clap#4391 specifically calls out bool, but strings may also be ignored), so when `ignore_errors` is given, the default values will be missing and parsing will fail unless the right arg types are used (e.g`. Option`). By parsing only early args (using the new struct) we only need to adjust `no_pager`, instead of adjusting all args with a default value.
Clap bails parsing when an "error" is encountered, e.g. a subcommand is missing, "--help" is passed, or the "help" subcommand is invoked. This means that the current approach of parsing args does not handle flags like `--no-pager` or `--color` when an error is encountered. Fix this by separating early args into their own struct and preprocessing them using `ignore_errors` (per clap-rs/clap#1880). The early args are in a new `EarlyArgs` struct because of a known bug where `ignore_errors` causes default values not to be respected (clap-rs/clap#4391 specifically calls out bool, but strings may also be ignored), so when `ignore_errors` is given, the default values will be missing and parsing will fail unless the right arg types are used (e.g`. Option`). By parsing only early args (using the new struct) we only need to adjust `no_pager`, instead of adjusting all args with a default value.
Clap bails parsing when an "error" is encountered, e.g. a subcommand is missing, "--help" is passed, or the "help" subcommand is invoked. This means that the current approach of parsing args does not handle flags like `--no-pager` or `--color` when an error is encountered. Fix this by separating early args into their own struct and preprocessing them using `ignore_errors` (per clap-rs/clap#1880). The early args are in a new `EarlyArgs` struct because of a known bug where `ignore_errors` causes default values not to be respected (clap-rs/clap#4391 specifically calls out bool, but strings may also be ignored), so when `ignore_errors` is given, the default values will be missing and parsing will fail unless the right arg types are used (e.g`. Option`). By parsing only early args (using the new struct) we only need to adjust `no_pager`, instead of adjusting all args with a default value.
Sorry for the long time bump, I liked @scottidler's answer here so I've made it a little more generic. Slap this at the top of your cli code and you're good to go. My use case is for wrapping use anyhow::{anyhow, bail, Result};
use clap::{
error::{ContextKind, ContextValue, ErrorKind},
ArgMatches, Command, CommandFactory, FromArgMatches, Parser,
};
use std::env::args;
#[derive(Debug, Clone)]
struct Known<T>
where
T: FromArgMatches,
{
matches: T,
rest: Vec<String>,
}
impl<T> Known<T>
where
T: FromArgMatches,
{
pub fn new<I, S>(matches: ArgMatches, rest: I) -> Result<Self>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
Ok(Self {
matches: T::from_arg_matches(&matches)?,
rest: rest.into_iter().map(|a| a.as_ref().to_string()).collect(),
})
}
}
trait ParseKnown: FromArgMatches {
fn parse_known() -> Result<Known<Self>>;
}
impl<T> ParseKnown for T
where
T: CommandFactory + FromArgMatches,
{
fn parse_known() -> Result<Known<T>> {
let command = Self::command();
let mut rest = Vec::new();
let mut args = args().collect::<Vec<_>>();
loop {
match command.clone().try_get_matches_from(&args) {
Ok(matches) => {
return Known::new(matches, rest);
}
Err(e) if matches!(e.kind(), ErrorKind::UnknownArgument) => {
if let Some(ContextValue::String(v)) = e
.context()
.find_map(|(k, v)| matches!(k, ContextKind::InvalidArg).then_some(v))
{
let unknown = args
.iter()
.find(|a| a.starts_with(v))
.cloned()
.ok_or_else(|| anyhow!("No argument starts with {}", v))?;
if args.contains(&unknown) {
args.retain(|a| a != &unknown);
rest.push(unknown);
} else {
bail!("Got unknown argument {} but it is not in args at all.", v);
}
} else {
bail!("No string value found for unknown argument: {}", e);
}
}
Err(e) => bail!("Error getting matches from args '{:?}': {}", args, e),
}
}
}
}
#[derive(Parser, Debug, Clone)]
#[clap(
allow_hyphen_values = true,
trailing_var_arg = true,
disable_help_flag = true,
disable_version_flag = true
)]
struct Args {
#[arg(long)]
libafl_no_link: bool,
#[arg(long)]
libafl: bool,
#[arg(long)]
libafl_ignore_configurations: bool,
#[arg(long, value_delimiter = ',')]
libafl_configurations: Vec<String>,
}
fn main() -> Result<()> {
let args = Args::parse_known()?;
println!("{:#?}", args);
Ok(())
} Handles e.g. |
Comment on closing:
Make sure you completed the following tasks
Describe your use case
A way to declare a subset of arguments to be parsed, then return from parsing early. With the expectation that parsing will be called again, and run to full completion.
This most often comes up when you want some argument to handle some aspect of the CLI itself (such as
--color
flags to control error messages during parsing, or in the above--arg-file
which needs to determine a file to load prior to parsing the rest of the CLI).Describe the solution you'd like
From a user stand point, nothing changes. The run the CLI and things just work. From the a clap consumer stand point, it essentially allows you to pull out some values from the CLI (ignoring everything else), change something about the CLI and continue parsing.
Maybe it's biting off more than we want to chew for now, but this issue seems to come up time and time again where we want to parse some particular subset of arguments, and use those values to inform how the CLI itself behaves.
Alternatives, if applicable
You can already reparse your CLI twice.
But this doesn't help if there are errors in the initial parse, or options included included in the initial parse that the initial app isn't aware of. For example, an
iptables
style CLI where-m <foo>
enables many other arguments and options.Additional context
This could potentially be used in #1693 #1089 #1232 #568
The text was updated successfully, but these errors were encountered: