-
-
Notifications
You must be signed in to change notification settings - Fork 21.2k
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
Add command-line parser class #44594
Conversation
After adding tests from me, |
Perhaps somehow related to template fixture usage which may bloat it? I haven't done this to be honest... Possibly |
It seems to me that the file was large before that, and adding a lot of new tests just exceeded the maximum size allowed.
Thanks, it worked :) |
@akien-mga, I noticed that you changed the description. But this PR is not a refactoring of the current editor CLI. |
I have a feeling this PR is not going to get looked at by the devs if it doesn't make an attempt to refactor the Godot CLI. Right now, this PR has no use case other than exposure in the API, something only a few people have shown a little interest for. Refactoring main.cpp is (in my understanding) a more important goal. The initial proposal (godotengine/godot-proposals#137) follows this line of thinking as well. As it stands, this PR does not implement that proposal, and as no real need for an exposed command parser has been explicitely requested. That leads me to believe the devs will ignore your work, which is a shame because it looks promising. If you haven't already, take a look at Godot's best practices for contributing. |
While it's an important goal, adding a general-purpose command-line parser is a crucial step to achieve it, even if we don't end up exposing it as public API.
I'd definitely find the parser useful for a lot of things. Plugins like GUT would no longer need to implement their own command-line parsers. Game-specific options could also be implemented more easily: godotengine/godot-proposals#2726. Even Python has |
I don't disagree, in fact I believe this is what I said as well. All I am saying is that I think this PR should define a command line parser and refactor main.cpp to use it. Optionally it could be exposed to the api already, but that can also happen in another PR. |
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.
The functionality of this class is promising, but I think we should agree upon API and naming conventions to move forward first.
CommandParser
and CommandOption
could be renamed to CommandLineParser
and CommandLineOption
respectively, to be more explicit and avoid ambiguity with Command pattern in programming in general. It's more explicit and goes in accordance to OS.get_cmdline_args()
method.
Regarding naming conventions, I think it would be beneficial to rename command
parameters to option
in general. I'd use "option" because that's a noun which should be taken as a base for the entire API. So, instead of add_command(CommandLineOption p_command)
, it should be add_option(CommandLineOption p_option)
, etc.
Help formatting properties could be refactored into Dictionary
, to prevent API bloat in the parser class.
To make it useful for game-specific needs and make it more configurable, it may also be worth to implement a virtual _get_help_text(format_options: Dictionary)
function which can be overridden via script/C++. Implementing a virtual method can also alleviate the problem of having formatting properties hardcoded directly in the class.
Don't forget to go through hidden conversations, because GitHub didn't like the fact that I have left so many review comments...
I haven't reviewed the documentation yet (there are typos which can be fixed, though).
core/command_parser.h
Outdated
void set_static_checker(CheckFunction p_function, const String &p_error_msg); | ||
Error set_checker(const Callable &p_callable, const String &p_error_msg); | ||
const ArgumentChecker *get_checker() const; | ||
void remove_checker(); |
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.
Not sure if really needed, can just do:
set_checker(null)
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.
But we have set_static_checker
(for use in C++ only) and set_checker
helpers to create ArgumentChecker
. ArgumentChecker
is not exported to GDScript. I wanted to provide a similar interface to connect
/ disconnect
.
I have renamed the PR's title to reflect changes in this PR. Yeah this doesn't refactor the main.cpp argument parsing in the engine, and that could be made in the successive PRs. |
I don't disagree either, but exposed or not, the work done on refactoring I myself prefer to look into the future a bit, but I realize that Godot's development is extremely pragmatic so I'm not sure whether what I said even makes sense in terms of Godot's development principles. 😛 |
Comes back to the point of what you prioritise: engine or api. I am fine either way, whichever is preferred by the maintainers. I just think if you develop the parser first for the engine, you have a clear use case where you can judge the implementation. That can then help with designing the public API. |
The previous PR #26213 did refactoring but ended up not reviewed either. Since this PR is based on #26213, I'd assume that it is already somewhat battle-tested against use cases in Godot (which are not that advanced, to be honest), and the implementation in this PR is by far more flexible than Godot really needs. Unfortunately, this may be a problem to adoption, because as I said, Godot's development is extremely pragmatic and this PR may also end up in Limbo, just because it solves more problems than Godot currently has, which is pity to be honest... Because I think it does make sense to make functionality feature-complete, regardless whether we think it will be used often or not. I'm personally interested in such a class and it would be useful to have in core. However, if this won't happen, perhaps this can be implemented in Goost. |
Indeed, as @Xrayez mentions, there is not a proper proposal opened following proper procedure that can justify in real-world usage scenario integrating this kind of functionality. As such this PR will most likely end up in limbo. I suggest re-starting the process from scratch, opening a proper proposal and seeing if there is any interest. |
I opened a counter-proposal about this: |
@Xrayez, thank you a lot, will push discussed changes in a few days. I'm going to move this PR to Goost, if the developers don't change their minds. Have this class as a plugin sounds good to me too. |
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.
The exposed API looks much better to me now. As a user, I find it easier to figure out what can be done more on an intuitive level.
Again, haven't reviewed documentation because I think we could wait for Akien's planned review as well before suggesting documentation changes.
@Xrayez, thanks, yes, I pretty sure that the documentation should be improved. |
Port of godotengine/godot#44594 to Godot 3.x. Co-authored-by: Shatur95 <[email protected]> Co-authored-by: lupoDharkael <[email protected]>
Port of godotengine/godot#44594 to Godot 3.x. Co-authored-by: Shatur95 <[email protected]> Co-authored-by: lupoDharkael <[email protected]>
@Shatur I've started working on porting this PR to Goost in Godot 3.x as we've discussed at Godot contributors chat, see goostengine/goost#123. I'm writing test cases in GDScript and noticed that the following test case: func test_parse():
var args = ["--input path"]
var input = CommandLineOption.new()
input.names = ["input", "i"]
parser.add_option(input)
var err = parser.parse(args)
if err:
print(parser.get_error_text())
assert_eq(err, OK)
var i = parser.find_option("input")
assert_eq(input, i)
assert_eq(parser.get_value(i), "path") fails and prints:
According to documentation, both EDIT: nevermind, I've forgot again that args should be passed as a list of strings, not a single string. I wonder what could be done to avoid this mistake, as command-line arguments could be technically parsed from a single string, not just P.S. For those wondering why we've decided to port this PR to Goost: reduz already rejected the idea of having a general-purpose parser to be in core, see godotengine/godot-proposals#2797 (comment). |
I have no idea :) In theory we could have two methods, one for a single string, second for a list of strings. |
I think parse method could take |
I think it can confuse users a little :) |
Port of godotengine/godot#44594 to Godot 3.x. Co-authored-by: Shatur95 <[email protected]> Co-authored-by: lupoDharkael <[email protected]>
Port of godotengine/godot#44594 to Godot 3.x. Co-authored-by: Shatur95 <[email protected]> Co-authored-by: lupoDharkael <[email protected]>
I have discovered that prefix precedence in func test_get_prefix_precedence():
var debug = CommandLineOption.new()
debug.names = ["debug"]
debug.arg_count = 0
parser.add_option(debug)
parser.long_prefixes = ["--no-", "--"]
assert_eq(parser.parse_args(["--debug"]), OK)
assert_true(parser.is_set(debug))
assert_eq(parser.get_prefix(debug), "--")
assert_eq(parser.parse_args(["--no-debug"]), OK)
assert_true(parser.is_set(debug))
assert_eq(parser.get_prefix(debug), "--no-")
parser.long_prefixes = ["--", "--no-"] # Swap precedence.
assert_eq(parser.parse_args(["--debug"]), OK)
assert_true(parser.is_set(debug))
assert_eq(parser.get_prefix(debug), "--")
assert_eq(parser.parse_args(["--no-debug"]), ERR_PARSE_ERROR,
"Precedence of `--` prefix is higher than `--no-`, should fail.")
assert_false(parser.is_set(debug)) Note that both The above test case works, but I wonder if that's expected. Should this rely on precedence of prefixes? |
No, I think it can be fixed. We should check for each prefix before return the error. |
Found another test case, looks like a bug to me: func test_default_not_allowed_arg():
var filetype = CommandLineOption.new()
filetype.names = ["filetype"]
filetype.default_args = ["png"]
filetype.allowed_args = ["jpg"]
parser.add_option(filetype)
assert_eq(parser.parse_args([]), ERR_PARSE_ERROR, "Default argument `png` is not allowed one, should fail.")
assert_eq(parser.get_value(filetype), "") Note that this works as expected when you parse
I think parsing should fail immediately if default argument is not allowed according to |
Agree, we should have a check for it. |
Fixed in |
Fixed in goostengine/goost#123 is now merged in Goost as well. |
Found a test case which crashes the engine, see goostengine/goost#125 for reproduction and solution. |
Nice catch! |
Continuation of #26213.
Thank you a lot to @lupoDharkael for the initial work! I updated the code to the latest master and heavily redone the previous implementation. What I added:
notify()
)-abc
), sticky arguments (-dvalue
), adjacent arguments (-d=value
) (all this things can be disabled)./command
,+command
,-command
, etc, this can be useful for chat commands parsing, for example).parsed
signal.Simple demo project: TestParser.zip
Current API looks similar to Qt (API) and Boost.program_options (features).
But I haven't made any changes to
main.cpp
. I think it will be better to agree on API first and merge it as a class for GDScript because it can be used as is. Then, I will send a separate PR with changes tomain.cpp
and then another PR with the ability to disable engine arguments to override with custom one. I think that this approach will simplify review process.