rust-lang / getopts

The getopts repo maintained by the rust-lang project
https://docs.rs/getopts
Apache License 2.0
233 stars 62 forks source link

Bug in optflagopt #104

Open pecastro opened 3 years ago

pecastro commented 3 years ago

I've bumped into some weird behaviour in another project whereas the use of an optflagopt option seemed to render unexpected behaviour of the option not being properly parsed.

I decided to come to the source and I extended the mod.rs test to show that something was amiss https://github.com/pecastro/getopts/commit/de5e4db173b265bbf77c2d9ecfe4bff896913c46

I've managed to make the test panic when you use an optflagopt in combination with other options. Initial investigations from debugging the test show that the vals Vec of the Matches struct will have a Vec with Given rather than the option value specified and that is what's causing the return of None and triggering the panic.

self.vals:

[[(0, Val("foo"))], [(1, Val("bar"))], [], [(2, Given)]]
ghostd commented 3 years ago

Hi,

It seems to be expected: https://github.com/rust-lang/getopts/blob/c11eb65097d0dc9fe9ceb351d63c3ac8b5f05e0b/src/tests/mod.rs#L412-L427

pecastro commented 3 years ago

After digging a bit deeper the issue seems to be mainly to do with the parsing of an optflagopt whereby an -o=foo, -o foo, and --option=foo are parsed correctly whereas the --option foo is leaving foo as free an not consuming it as the argument to the option.

pecastro commented 3 years ago

@ghostd Yes, I've seen that whilst I was writing a fix and I'm correcting that test which I think it is wrong in my opinion. The documentation for the method https://docs.rs/getopts/0.2.21/getopts/struct.Options.html#method.optflagopt doesn't specify that when the long option is used any optional parameter should be ignored ...

pecastro commented 3 years ago

Upon further attentive reading I did spot under the Parsing section of the Linux getopt user command https://man7.org/linux/man-pages/man1/getopt.1.html on the second to last paragraph the rules of engagement regarding the handling of optional arguments. What tripped me was the absence in this documentation of any similar indication regarding the behaviour. The https://man7.org/linux/man-pages/man3/getopt.3.html library does not state any such behaviour.

I've close the associated MR for now and I'll close this afterwards if no other insightful comments materialize ...

correabuscar commented 3 months ago

After digging a bit deeper the issue seems to be mainly to do with the parsing of an optflagopt whereby an -o=foo, -o foo, and --option=foo are parsed correctly whereas the --option foo is leaving foo as free an not consuming it as the argument to the option.

This is still true in getopts = "0.2.21" however, the docs wrongly then state:

Single-character options are expected to appear on the command line with a single preceding dash; multiple-character options are expected to be proceeded by two dashes. Options that expect an argument accept their argument following either a space or an equals sign. Single-character options don't require the space.

therefore I expected(wrongly I see) that --option foo would be equivalent to --option=foo, but it is not.

Furthermore, in accordance with that above doc, the usage via opts.usage(&brief) for something declared as opts.opt("u", "unified", "output NUM (default 3) lines of unified contex", "NUM", HasArg::Maybe, Occur::Multi);, states:

Options:
    -u, --unified [NUM] output NUM (default 3) lines of unified contex

as if using a space there, even for the long option, is accepted.

But maybe that's kind of relaxed, and assumes docs were read by whoever uses the binary, i dno, because how would that be shown otherwise? -u, -u NUM, --unified[=NUM] (but misses -u[NUM], ie. lacking space and NUM being optional) or -u[NUM], -u [NUM], --unified[=NUM] or -u[ ][NUM], --unified[=NUM] (better, imho, but kind of confusing with that [ ] aka optional space)

That being said, gnu diff does the same thing too and treats it as free arg(in this case as the first filename):

$ /usr/bin/diff --unified 4 a b
/usr/bin/diff: extra operand 'b'
/usr/bin/diff: Try '/usr/bin/diff --help' for more information.

Upon further attentive reading I did spot under the Parsing section of the Linux getopt user command https://man7.org/linux/man-pages/man1/getopt.1.html on the second to last paragraph the rules of engagement regarding the handling of optional arguments.

that's this:

A long option normally begins with '--' followed by the long option name. If the option has a required argument, it may be written directly after the long option name, separated by '=', or as the next argument (i.e., separated by whitespace on the command line). If the option has an optional argument, it must be written directly after the long option name, separated by '=', if present (if you add the '=' but nothing behind it, it is interpreted as if no argument was present; this is a slight bug, see the BUGS). Long options may be abbreviated, as long as the abbreviation is not ambiguous.

therefore I guess the issue happens only for optional arguments of long options, not for required arguments of long options, which in retrospect makes sense now, because how could it know if --has-optional-arg this_arg is its optional arg or just a free arg that follows, whilst compared to --has-optional-arg=this_arg is obvious it's its optional arg.

Maybe the doc needs a little rewording then, to make sure long opts can't expect their optional arg after a space, only after an equals sign. And long opts that require an arg, can expect it after either a space or an equal sign. It's unclear from this Options that expect an argument accept their argument following either a space or an equals sign. that expect there means required argument and if it it did, it doesn't say that the optional argument works with equals sign, so I double expect there had the meaning of required argument.