-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
OptionParser should allow flag with optional value #5338
Comments
It depends options handler order. For example: require "option_parser"
def parse(args)
p [:parse, args]
OptionParser.parse(args) do |opts|
opts.on("-a [val]", "") do |val|
p [:a, val]
end
opts.on("-b [val]", "") do |val|
p [:b, val]
end
opts.unknown_args do |args|
p [:unknown_args, args]
end
end
puts
end
parse(%w(-a -b b))
# [:parse, ["-a", "-b", "b"]
# [:a, "-b"]
# [:unknown_args, ["b"]]
parse(%w(-b -a a))
# [:parse, ["-b", "-a", "a"]
# [:a, "a"]
# [:unknown_args, []]
# NOTE: why "-b" is not called? |
Wow, this looks like there are some serious issues with optional argument values. |
This is patched in commit 0c306ca, which fixes 4 issues - see new specs in this commit against crystal main branch:
Three fixed problems are mentioned here - flag matched as optional value, separate argument matched when optional value is defined together with flag, and shifted argument matched as value because arguments were removed by previous matches. I also added improvement requested for example in #6632, because removing of consumed arguments is now additional work, but this is evidently done poorly to maintain backward compatibility - it should probably work like in Ruby's OptionParser, eg #parse should be non-destructive and #parse! should consume flags and their values:
But I would agree with #4809 (comment), that optionparser needs a rewrite and this could be part of such work |
For now we should probably move |
Something should be done, because these bugs are not minor. Else I would not try to push them upstream - I have not good experience with trying it, I almost always maintain my own fork. And I'm also working on more interesting things now - possibility of building shared libraries in crystal, generating .h files for them and so on. If you want, I can cut down these fixes to minimum, so it will be simpler to apply. Also I can remove new optional argument for non-destructive parsing. This patch should make parsing much faster for huge amount of arguments, but problem is overall algorithm, which scans all non-consumed arguments again and again for each handler instead of going sequentially through arguments and match them against all handlers. |
What is your decision @RX14? |
I'd like an opinion from the other core team members - I'm too busy with exams to rewrite |
This is understandable, I also almost started to work on it. But I also know that that would be pointless work. Do you want to rewrite it, rather than apply this fast fix? Or you just don't like something about it? Are you afraid of new bugs? |
Does this really need a rewrite? It seems to be just a bug with OptionParser... |
I'd think that minimally mentioned algorithm needs rework, which I can do. I didn't started to rewrite it to minimize changes. I meant, that new interface of whole OptionParser would by pointless work from me, because you have probably another ideas. My idea is, that OptionParser in stdlib should be suited for most common simple programs, and also allow to serve as basic building block for more advanced "CLI Builders". So for example it should be also able to stop parsing on first unknown argument (I found this request two times here) and non-destructive parsing. |
@asterite to me, just looking at |
Have you already decided? I hope I don't push you too much to stress you. |
So are you usually trying to similarly discourage new contributors? Or should I take it as a special discriminatory honor? Or you just don't care? |
@urde-graven If you have a patch that fixes some issue in OptionParser, please open a PR. If it behaves like Ruby's OptionParser then it will likely be accepted. |
@asterite Only patch already mentioned in #5338 (comment). I have not worked on it further nor opened PR due to the discussion here. If It's ok, I'll open it. |
@urde-graven If it works like Ruby's OptionParser, then yes, please open a PR. Thank you! |
@straight-shoota I'm sorry that it took me so long, but could you explain the issue with some example code and command line invocation? |
require "option_parser"
def parse(args)
p [:parse, args]
OptionParser.parse(args) do |opts|
opts.on("-c[always|never|auto]", "--color=[always|never|auto]", "") do |val|
p [:color, val]
end
opts.unknown_args do |args|
p [:unknown_args, args]
end
end
puts
end
parse(%w(--color a.cr))
# old OptionParser:
# [:parse, ["--color", "a.cr"]]
# [:color, "a.cr"]
# [:unknown_args, []]
# with patch:
# [:parse, ["--color", "a.cr"]]
# [:color, ""]
# [:unknown_args, ["a.cr"]]
parse(%w(-c a.cr))
# old OptionParser:
# [:parse, ["-c", "a.cr"]]
# [:color, "a.cr"]
# [:unknown_args, []]
# with patch:
# [:parse, ["-c", "a.cr"]]
# [:color, ""]
# [:unknown_args, ["a.cr"]]
[:parse, ["-a", "-b", "b"]]
[:a, ""]
[:b, "b"]
[:unknown_args, []]
[:parse, ["-b", "-a", "a"]]
[:a, "a"]
[:b, ""]
[:unknown_args, []]
require "option_parser"
def parse(args)
p [:parse, args]
OptionParser.parse(args) do |opts|
opts.on("-a [val]", "") do |val|
p [:a, val]
end
opts.on("-b [val]", "") do |val|
p [:b, val]
end
opts.unknown_args do |args|
p [:unknown_args, args]
end
end
puts
end
parse(%w(-b -a a c)
# old OptionParser:
# [:parse, ["-b", "-a", "a", "c"]]
# [:a, "a"]
# [:b, "c"]
# [:unknown_args, []]
# with patch:
# [:parse, ["-b", "-a", "a", "c"]]
# [:a, "a"]
# [:b, ""]
# [:unknown_args, ["c"]]
args = ["-f", "12", "-g", "13", "-h", "14"]
captured_f = captured_g = captured_z = nil
parser = OptionParser.parse(args) do |opts|
opts.on("-f", "--flag X", "some flag") { |flag_value| captured_f = flag_value }
opts.on("-g", "--gflag=Y", "another flag") { |flag_value| captured_g = flag_value }
opts.on("-h", "--hopt=[Z]", "optional flag") { |flag_value| captured_z = flag_value }
end
captured_f.should eq "12"
captured_g.should eq "13"
captured_z.should eq ""
args.should eq ["14"]
parser.to_s.should contain " -f, --flag X" |
@urde-graven Thank you! I think all the existing problems in
With that we can easily allow optional values:
Is what I'm saying correct? Is there any program out there where you can say |
There is no standard concept for option parsing. There are different styles employed by different programs.
Yes, of course. Loads of them. I'd say this is actually more common than |
Cool, then. Feel free to continue forward with this, then (I won't have any opinion at all because I don't know about this subject). |
There is one related issue; an empty argument is indistinguishable from a missing argument: require "option_parser"
def parse_args(args)
OptionParser.parse(args) do |parser|
parser.on("-c [x]", "--color [x]", "optional arg") { |x| p x }
end
end
# argument missing from first set of args only
parse_args %w(-c) # => ""
parse_args ["-c", ""] # => ""
# argument missing from first set of args only
parse_args %w(--color) # => ""
parse_args %w(--color=) # => ""
parse_args ["--color", ""] # => "" That means if a shell script does something like
#include <stdio.h>
#include <argp.h>
static struct argp_option options[] = {
{.name = "a", .key = 'a', .arg = "x", .flags = OPTION_ARG_OPTIONAL},
{0},
};
static error_t parse_opt (int key, char *arg, struct argp_state *state) {
switch (key) {
case 'a':
printf("\"%s\"\n", arg);
break;
default:
return ARGP_ERR_UNKNOWN;
}
return 0;
}
static struct argp argp = {.options = options, .parser = parse_opt};
int main(int argc, char **argv) {
char *args1[] = {"./a.out", "--a"}; /* ./a.out --a */
argp_parse(&argp, 2, args1, ARGP_NO_ERRS, 0, NULL); /* "(null)" */
char *args2[] = {"./a.out", "--a="}; /* ./a.out --a= */
argp_parse(&argp, 2, args2, ARGP_NO_ERRS, 0, NULL); /* "" */
/* for completeness */
char *args3[] = {"./a.out", "--a", ""}; /* ./a.out --a '' */
argp_parse(&argp, 3, args3, ARGP_NO_ERRS, 0, NULL); /* "(null)" */
return 0;
} |
In order to differentiate no value and empty value, the argument value could represent that with A possible workaround for that would be to change the method signature, for example by introducing a new parameter for determining optionality which would allow to change the block type to yield def on(short_flag : String, long_flag : String, description : String, *, optional_value : Bool, &block : String? ->) An alternative would be an overload that accepts a proc instance instead of capturing a block. def on(short_flag : String, long_flag : String, description : String, proc : String? ->) This would be a bit more tedious to use though, because you need to create an explicit parser.on("-c", "--color", "", -> (value : String?) do
end) |
In #4499 it has come up that
OptionParser
does not allow for a flag to be used either with a value (--flag=value
) or without a value (--flag
). In the referenced PR it happend that--color a.cr
was misinterpreted as flag--color
with valuea.cr
, whereas it is meant to be a value-less flag--color
and an unnamed argumenta.cr
.Currently, the format
--flag [value]
allows a flag value to be missing, but this only works if it is the last argument or the next argument is also a flag. It would also match an unnamed argument. There should be a way to prevent that.Now, obviously, this can't work with an empty space as separater between flag an value as this would always be ambiguous, but it would work if it was enforced to be an equals sign:
--foo arg
would be an value-less flag,--flag=value
would be a flag with valuevalue
.The text was updated successfully, but these errors were encountered: