-
-
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 rewrite with subcommands #9009
Conversation
it "has required option with = (3) raises" do | ||
expect_missing_option ["--flag="], "--flag=FLAG", "--flag" | ||
it "has required option with = (3) handles empty" do | ||
expect_capture_option ["--flag="], "--flag=FLAG", "" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed the behaviour of OptionParser
here because I thought this behaviour was incorrect.
I think this fixes #8937 |
Yeah, me too I'll add a spec. |
Awesome! This might also enable to implement #5338 (could be a follow-up though). |
Ah, that seems rather difficult with the current code because there's no way to "peek" at the next argument and see if it's valid or not I might just get rid of the "iterator" business and use indexing in the array, then. |
I suppose it might be best to support only |
It's definitely doable to have a "next argument doesn't start with -" heuristic. I switched the code to use array indexing and it's neater anyway. |
be69622
to
dd59847
Compare
Thoughts on allowing multiple shortflags to be grouped? require "option_parser"
OptionParser.parse do |opts|
opts.on("-f", "Prints foo") { pp "foo" }
opts.on("-b", "Prints bar") { pp "bar" }
end Works: Does not work: |
@Blacksmoke16 I thought about adding that feature, but I'd rather leave it for another PR. |
Would also close #3357. Another thought I had, probably better for another PR; but what about parsing options with more than one value? An example of this is the |
@Blacksmoke16 It is indeed unrelated and is also not part of any spec or common use. |
I wrote this reply before realizing that something else was being asked, but if one wants to support an arbitrary number of values, it should definitely not be done as And this is already supported, just push it to an array: Coderequire "option_parser"
argv = ["--stuff", "foo", "--stuff=bar"]
stuff = [] of String
OptionParser.parse(argv) do |parser|
parser.on("--stuff=NAME", "") { |name| stuff << name }
end
p! stuff # => ["foo", "bar"] |
@oprypin I wasn't imagining supporting an arbitrary number of values, just a way to handle a fixed number that is more than one. Like |
|
@Blacksmoke16 IMO that looks like a very uncommon feature. I'd rather not have such specialized things in OptionParser. Btw. why not just use |
Oh I don't really have a use case. Was just something I noticed in |
Thank you for the great improvement of the OptionParser! Currently working examplerequire "option_parser"
ary = [] of String
options = %w[-ufoo -tbar]
OptionParser.parse(options) do |p|
p.on("-u FOO", "Description") { |o| ary << o }
p.on("-t FOO", "Description") { |o| ary << o }
end
pp ary # => ["foo", "bar"] |
You can just copy paste the file before your example to try. Yes, it works before and after. |
I've just added commits to unregister subcommands when they're called, this means that when you call a subcommand I've also added two new features: This PR is still WIP because I need to fix the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love it =)
5aa0451
to
a0b5595
Compare
|
I'm not entirely sure about using the same method for defining a subcommands and flags when the only difference is whether the string starts with a dash. |
I mean, we could have different methods for short and long flags because they have differet behaviours too. I think this is fine. |
Yes, it's unexpected. OptionParser defines how options should be parsed. I find it pretty strange that if I use it twice, it behaves differently. |
Crystal is not a functional programming language, but we tend to use and like purity. That is, methods shouldn't have unexpected side effects. |
I made I tried going with the route of cloning the |
Can you have an additional
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather merge this without the last commit, but I'm fine with it.
I'm still convinced this is completely unnecessary and more a maintenance burdon than anything useful.
I went through 14 pages of search results for OptionParser.new
on GitHub and couldn't find a single shard that re-uses an OptionParser
instance. Only in two or three cases it would be technically possible from the code to parse multiple times, but that's in CLI builders that wrap OptionParser
. I'm sure no application would use it that way.
I guess with that proof we can go ahead without the last commit, and add it later if needed. |
@didactic-drunk in short, no. |
We might as well leave the last commit. Why not? |
src/option_parser.cr
Outdated
ensure | ||
@flags = old_flags.not_nil! | ||
@handlers = old_handlers.not_nil! | ||
@stop = false | ||
@banner = old_banner | ||
@unknown_args = old_unknown_args | ||
@missing_option = old_missiong_option.not_nil! | ||
@invalid_option = old_invalid_option.not_nil! | ||
@before_each = old_before_each |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't be better to use begin ... end
block instead of using .not_nil!
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about it but I don't think it's worth the extra indent. I prefer it this way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK then how about clumping it all into one variable (tuple? record? maybe store the whole state in a sub-record in the first place?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be either as is or without the reset.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@RX14 Using .not_nil!
should be treated as a last resort, so if there's a way to avoid it, even at the "cost" of an extra indent, then I'd say it's worth it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just put the begin after you save all those variables. I agree with Sija.
I am really ambivalent about leaving the last commit in or not, since it's less work to do so, I'll leave it in. I'll merge this today if there's no more API-level objections. |
src/option_parser.cr
Outdated
old_handlers = @handlers.clone | ||
old_banner = @banner | ||
old_unknown_args = @unknown_args | ||
old_missiong_option = @missing_option |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
old_missiong_option = @missing_option | |
old_missing_option = @missing_option |
@RX14 thanks for working on this. I agree that the current Now, I wouldn't consider this implementation "good code". I don't need to check usage stats to understand that the approach used for subcommands is not well designed. Mutating the parser is unnecessary and TBH relying on string comparison with a specific number of spaces from a formatted text to do so, makes me cringe. I think it could actually just make it store the options more structured, format those options just when it's needed (on Also, I wish the subcommands implementation could work somehow with separate I could understand that you don't feel like doing it right now, but I wouldn't say this PR is the ultimate design and good code that we could be looking for. |
Should subcommands be marked experimental? Should |
On that, I totally agree. The If you'd like to redesign this a bit more I'd love to see your ideas! |
1ea4f06
to
91b8dda
Compare
91b8dda
to
9d7f128
Compare
I got angry with
OptionParser
for being a pile of bad code so I rewrote the implementation. It's simpler now, and easier to understand the implementation, and notO(n^2)
so probably miles faster. And it has a completely compatible interface, no changes were required to any existing usages across the stdlib.While I was rewriting it I had a universe brain moment and realised that I could make it handle subcommands very simply, so I threw that in as a bonus.
Example subcommand usage:
There's some tweaks still required to get the
to_s
output to not suck when using subcommands but I wanted to submit this for comment before I forgot about it tomorrow morning.Fixes #8937.