Skip to content
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

Aliases the other way around (POSIX style) #970

Closed
ericbn opened this issue May 27, 2017 · 7 comments
Closed

Aliases the other way around (POSIX style) #970

ericbn opened this issue May 27, 2017 · 7 comments
Labels
C-enhancement Category: Raise on the bar on expectations

Comments

@ericbn
Copy link

ericbn commented May 27, 2017

Clap 2.24.1 and rust 1.12 are currently being used.

Suppose I have defined these:

.arg(flag("foo"))
.arg(flag("no-foo").overrides_with("foo"))
.arg(flag("bar").overrides_with("bar").takes_value(true).possible_values(&["0", "1"]))

And I want to have two other options that work as aliases for the previous ones:

  • --baz = --foo --bar=1
  • --no-baz = --no-foo --bar=0

and they would also override the individual options in POSIX style. So for example:

  • --baz --bar=0 yields only foo, and bar with value 0
  • --no-baz --foo also yields only foo, and bar with value 0
  • --no-foo --baz yields only foo, and bar with value 1
  • --bar=1 --no-baz yields only no-foo, and bar with value 0

Any way to accomplish this with current clap implementation just by using the args builder?

(Note for maybe another issue: Not sure if flag("bar").overrides_with("bar") is valid. The idea is that only the last provided value should be used. E.g. --bar=0 --bar=1 should yield bar with value 1. Curiously, it works when at least 2 of the same option are given. But it fails with The argument '--bar' was provided more than once, but cannot be used multiple times when 3 or more are given...)

@kbknapp
Copy link
Member

kbknapp commented May 29, 2017

This accomplishes 95% of what you're looking for:

        .arg(flag("foo"))
        .arg(flag("no-foo").overrides_with("foo"))
        .arg(flag("bar")
             .takes_value(true)
             .possible_values(&["0", "1"])
             .default_value("0")
             .default_value_if("baz", None, "1"))
        .arg(flag("baz"))
        .arg(flag("no-baz").overrides_with_all(&["baz", "foo"]))

The "bar" overrides with "bar" is the part that doesn't work at all with the above solution and I would say is a separate bug if you wouldn't mind filing it.

The the above solution meet what you're looking for, or are there additional details I'm missing?

@ericbn
Copy link
Author

ericbn commented May 29, 2017

The flag("bar").default_value_if("baz", None, "1")) will yield a bar with value 0 if --bar=0 --baz are provided, but I would expect the last to override the first, so to have a bar with value 1 (because baz is an "alias" for --foo --bar=1.

Also, if I just provide the option --baz, it will only yield the bar with value 1, but no foo (and I could not use default_value_if() with foo, because foo does not take values). I would expect both foo, and bar with value 1 to be present.

And if I just give the --no-baz, I would expect both the no-foo, and baz with value 0 to be present. But none would be yield.

Maybe other scenarios would fail too... Not sure if my description of the problem was clear enough.

The idea is that:

  • baz works as an alias for --foo --bar=1
  • no-baz works as an alias for --no-foo --bar=0
  • last options override previous ones, even the "aliased" ones

I sure didn't provide all possible scenarios in the examples I gave before. Let me try a complete list now:

Options provided foo no-foo bar
--foo ✔️
--no-foo ✔️
--bar=0 0
--bar=1 1
--baz ✔️ 1
--no-baz ✔️ 0
--no-foo --baz ✔️ 1
--bar=0 --baz ✔️ 1
--no-foo --bar=0 --baz ✔️ 1
--baz --no-foo ✔️ 1
--baz --bar=0 ✔️ 0
--baz --no-foo --bar=0 ✔️ 0
--foo --no-baz ✔️ 0
--bar=1 --no-baz ✔️ 0
--foo --bar=1 --no-baz ✔️ 0
--no-baz --foo ✔️ 0
--no-baz --bar=1 ✔️ 1
--no-baz --foo --bar=1 ✔️ 1

(empty means option is not present)

@ericbn
Copy link
Author

ericbn commented May 29, 2017

As for the separate issue, I opened #976.

@kbknapp
Copy link
Member

kbknapp commented May 30, 2017

I greatly appreciate the additional details! 👍

The parts that jump out at me are going to be things like --baz implying --foo. Currently that's not possible and happens purely in user code. I.e. If I'm designing a CLI and I know --baz implies foo, I just add that check manually when I'm checking for foo

struct Args {
    foo: bool,
    bar: i32 // for simplicity
}

let matches = /* skpped */.get_matches();
let a = Args {
    foo: matches.is_present("foo") || matches.is_present("baz"),
    bar: matches.value_of("bar").unwrap() // all the encoding in clap made this unwrap safe
};

That isn't to say I can't make a feature to imply another arg, as requiring already works. Right now, if one requires a flag it arbitrarily panic!s simply because at the time I had more opinionated views on CLI design. I'm willing to relax that though if people find it useful.

The way requirements work, if a required arg is missing it throws an error. But it'd be super easy to add a check if the required arg is actually a flag, and just include it as if it was used.

@ericbn
Copy link
Author

ericbn commented May 30, 2017

In a usual POSIX implementation I would be able to do so with something like (replaced foo, no-foo, bar, baz, no-baz by a, b, c, d, e respectively):

int c, errflg = 0;
bool foo = FALSE; // no-foo is the default
char* bar = NULL;
while ((c = getopt(argc, argv, ":abc:de")) != -1) {
  switch(c) {
  case 'a': // foo
    foo = TRUE;
    break;
  case 'b': // no-foo
    foo = FALSE;
    break;
  case 'c': // bar
    bar = optarg;
    break;
  case 'd': // baz
    foo = TRUE;
    bar = "1";
    break;
  case 'e': // no-baz
    foo = FALSE;
    bar = "0";
    break;
  case ':': // -c without operand
    fprintf(stderr, "Option -%c requires an operand\n", optopt);
    errflg++;
    break;
  case '?':
    fprintf(stderr, "Unrecognized option: '-%c'\n", optopt);
    errflg++;
  }
}

The loop iterating the options in order guarantees that later options will override previous ones, and I'll get the result from the table above, in the previous comment, for each case.

I could not figure out a way to achieve the same in clap. Declaring foo: matches.is_present("foo") || matches.is_present("baz"), for example, will not guarantee that a later baz replaces a foo that came before (or the other way around if I inverted the or expression operands), since this does take in account the order where the options appeared, just if they appeared at all...

BurntSushi pushed a commit to BurntSushi/ripgrep that referenced this issue Jun 12, 2017
to better organize options. These are the changes:
- color will have default value of "never" if --vimgrep is given,
  and only if no --color option is given
- last overrides previous: --line-number and --no-line-number, --heading
  and --no-heading, --with-filename and --no-filename, and --vimgrep and
  --count
- no heading will be show if --vimgrep is defined. This worked inside
  vim actually because heading is also only shown if tty is stdout
  (which is not the case when rg is called from vim).

Unfortunately, clap does not behave like a usual GNU/POSIX in some
cases, as reported in clap-rs/clap#970
and clap-rs/clap#976 (having all the bells
and whistles, on the other hand). So we still have issues like rg
failing when same argument is given more than once (unless for the few
ones marked with `multiple(true)`), or having unintuitive precedence
rules (and probably non-intentional, just there because of clap's
limitations) like:
- --no-filename over --vimgrep
- --no-line-number over --column, --pretty or --vimgrep
- --no-heading over --pretty
regardless of the order in which options where given, where the desired
behavior would be that the last option would override the previous ones
given.
@kbknapp
Copy link
Member

kbknapp commented Jun 16, 2017

since this does take in account the order where the options appeared, just if they appeared at all...

This was the piece of information I was missing. In my mind, a flag being either present or not doesn't really matter when it appeared in the argv, just that it either did or didn't. What you're saying that it matters where it appears, as to whether or not it should be counted as present (i.e. if another flag overrides it, etc.)

This change is doable by me relaxing the requirements for flags. Let me play with some implementations and I'll get this knocked out.

@kbknapp kbknapp added C: flags C-enhancement Category: Raise on the bar on expectations and removed T: RFC / question labels Jun 16, 2017
@kbknapp
Copy link
Member

kbknapp commented Feb 15, 2018

This should be good now with 2.30.0. Feel free to re-open if there is something we're missing.

@kbknapp kbknapp closed this as completed Feb 15, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-enhancement Category: Raise on the bar on expectations
Projects
None yet
Development

No branches or pull requests

2 participants