-
Notifications
You must be signed in to change notification settings - Fork 105
feat: command - add support for typed cli arguments #1199
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
Conversation
1332987 to
9eae673
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1199 +/- ##
==========================================
+ Coverage 68.55% 68.73% +0.18%
==========================================
Files 228 232 +4
Lines 14588 15102 +514
==========================================
+ Hits 10001 10381 +380
- Misses 4229 4361 +132
- Partials 358 360 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
- https://cli.urfave.org/v3/examples/arguments/advanced/ - `--help` prints arguments according to https://pubs.opengroup.org/onlinepubs/9799919799/
87a4783 to
211794f
Compare
46452f9 to
e8d5afa
Compare
|
Amazing👍 |
console/application.go
Outdated
| previousIsRequired := true | ||
| for _, v := range args { | ||
| if v.MinOccurrences() != 0 && !previousIsRequired { | ||
| errString := fmt.Sprintf("Required argument with value '%+v' should be placed before any not-required arguments", v) |
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 would be better to consolidate all error messages into the existing errors/list.go file.
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.
updated
console/application.go
Outdated
| if testing.Testing() { | ||
| panic(errString) | ||
| } else { | ||
| color.Errorln(errString) | ||
| os.Exit(2) | ||
| } |
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 think it would be better to add a check to determine if the environment is an Artisan environment. If it is an Artisan environment, we can output the error message and perform os.Exit. For other cases, such as test environments or non-Artisan execution environments, we can only output the error message without making the argumentor command effective, ensuring that the application as a whole continues to run.
Additionally, it might be worth mentioning that os.Exit can be assigned to a variable, for example, var osExit = os.Exit, so that it can be replaced in unit tests to make the code testable.
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 think it would be better to add a check to determine if the environment is an Artisan environment.
I think it should be fine, the application can't be built successfully if there are problems, given that this function is called by Register, and Register is called when initiating the framework.
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 think it would be better to add a check to determine if the environment is an Artisan environment.
I think it should be fine, the application can't be built successfully if there are problems, given that this function is called by
Register, andRegisteris called when initiating the framework.
Regarding this point, the error here is due to having a required argument after an optional argument. This is a logical mistake, but it doesn't affect the build process. However, the application will call os.Exit(2) during startup, which is very unfriendly. It's acceptable to interrupt execution when running artisan commands, but if you're simply starting a web service (not running artisan), being forced to exit is terrible. You can refer to how Laravel handles this scenario.
The worst-case scenario is: you install a third-party package, everything works fine, and one day after upgrading, the package introduces a new command. If there is a configuration issue with the command arguments, os.Exit will be called—even if you never use the artisan commands from that package, your application won't be able to start properly.
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.
Good point, so there are two solutions when there is an argument issue:
- Exit the app,
go buildandgo runwill fail, user needs to fix the argument issue immediately. - Print the error with a graceful message, and the command will not be registered.
The second should be fine.
hwbrzzl
left a comment
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.
Awesome, thanks! A few of questions.
console/application.go
Outdated
| previousIsRequired = false | ||
| } | ||
| switch arg := v.(type) { | ||
| case *command.Float32Argument: |
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 what confuses me, is inconsistency in naming, i.e. type StringArgument but method ArgumentString(),
probably type also should be named ArgumentString ?
Yes, good point. Keeping consistency should be better: Argument*. By the way, about the Flags option, how about modifying it to Options? Keep it consistent with Option* functions. And type BoolFlag struct, etc. structs can be optimized as well. Adding the deprecated flag for them. cc @almas-x
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 like to confirm: Are you suggesting merging the existing Flag and the new Argument introduced in this PR into Option? If so, wouldn't this potentially introduce significant breaking changes? It seems that all existing artisan commands would encounter type errors, since the command.Extend method in command definitions currently refers to the xxxFlag structs.
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.
Are you suggesting merging the existing Flag and the new Argument introduced in this PR into Option?
No, I mean copy all flags to options, then add a DEPRECATED flag for flags. There is no breaking change.
Then the logic will be:
Set Options in Extend -> Use ctx.Option* to get data.
Set Arguments in Extend -> Use ctx.Argument* to get data.
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.
We can implement it in another PR if it's acceptable.
console/application.go
Outdated
| if testing.Testing() { | ||
| panic(errString) | ||
| } else { | ||
| color.Errorln(errString) | ||
| os.Exit(2) | ||
| } |
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 think it would be better to add a check to determine if the environment is an Artisan environment.
I think it should be fine, the application can't be built successfully if there are problems, given that this function is called by Register, and Register is called when initiating the framework.
console/cli_context_args_test.go
Outdated
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.
How about merging this file to cli_context_test.go.
| argsTemplate = `{{if .Arguments}}{{range .Arguments}}{{template "argTemplate" .}}{{end}}{{end}}` | ||
| argTemplate = ` {{if .Min}}<{{else}}[{{end}}{{.Name}}{{if (or (gt .Max 1) (eq .Max -1))}}...{{end}}{{if .Min}}>{{else}}]{{end}}` |
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.
Could you add a screenshot for this printing?
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.
Speaking of help information output, the current implementation uses Go templates, which are honestly very hard to maintain—and debugging is even more troublesome. That's why I've always wanted to refactor this part and directly use the framework's color package for printing. This way, handling command sorting, alignment, and indentation would be much easier. In fact, I even submitted a PR to urfave/cli specifically for this purpose: @urfave/cli/pull/2150.
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.
@hwbrzzl this is my testing command with various cases, required/optional/slice

|
summary for further changes:
|
Yes, thanks! |
|
@hwbrzzl @almas-x |
console/application.go
Outdated
| Min: arg.MinOccurrences(), | ||
| Max: arg.MaxOccurrences(), | ||
| Config: cli.TimestampConfig{ | ||
| Layouts: []string{time.RFC3339}, |
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.
Ditto
console/application.go
Outdated
| Min: arg.MinOccurrences(), | ||
| Max: arg.MaxOccurrences(), | ||
| Config: cli.TimestampConfig{ | ||
| Layouts: []string{time.RFC3339}, |
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.
How about adding a time format configuration option to command.ArgumentTimestamp? This would allow users to specify their desired format, making it more flexible and user-friendly.
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.
added support for Layouts
| item := item | ||
| arguments, err := argumentsToCliArgs(item.Extend().Arguments) | ||
| if err != nil { | ||
| color.Errorln(errors.ConsoleCommandRegistrationFailed.Args(item.Signature(), err).Error()) |
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.
After printing the error here, it might be better to use continue to skip the iteration, right?
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.
added continue
I did it without continue because for me it provides at least some context for user
./artisan smtp:add --help
ERROR Registration of command 'smtp:add' failed: required argument 'host' should be placed before any not-required arguments
Description:
Add SMTP server
Stores SMTP server by provided server_id into database
Usage:
artisan [global options] smtp:add [options]
with continue
./artisan smtp:add --help
ERROR Registration of command 'smtp:add' failed: required argument 'host' should be placed before any not-required arguments
ERROR Command 'smtp:add' is not defined. Did you mean one of these?
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.
Pull Request Overview
This PR adds support for typed CLI arguments to the console command system, enabling developers to define and access strongly-typed command arguments beyond the basic string arguments previously available.
- Introduces comprehensive argument type definitions (Int, String, Float, Timestamp, and their slice variants)
- Adds argument validation with proper error handling for required argument ordering
- Implements argument parsing and access methods in the CLI context
Reviewed Changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
contracts/console/command/command_args.go |
Defines the Argument interface and all typed argument structs |
contracts/console/command/command.go |
Adds Arguments field to command Extend struct |
contracts/console/command.go |
Adds argument accessor methods to Context interface |
console/application.go |
Implements argument conversion and registration logic |
console/cli_context.go |
Implements argument accessor methods |
console/cli_helper.go |
Updates help templates to display argument information |
errors/list.go |
Adds new error constants for argument validation |
mocks/ |
Generated mock files for new interfaces and methods |
| Test files | Comprehensive test coverage for new argument functionality |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| previousIsRequired := true | ||
| for _, v := range args { | ||
| if v.MinOccurrences() != 0 && !previousIsRequired { | ||
| return nil, errors.ConsoleCommandRequiredArgumentWrongOrder.Args(v.ArgumentName()) | ||
| } | ||
| if v.MinOccurrences() != 0 { | ||
| previousIsRequired = true | ||
| } else { | ||
| previousIsRequired = false |
Copilot
AI
Sep 19, 2025
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.
[nitpick] The logic for tracking previousIsRequired can be simplified. The variable should be set to false when encountering the first non-required argument, and the validation should check if any required arguments come after that point. Consider refactoring to use a single boolean flag hasNonRequiredArgument for clearer intent.
| previousIsRequired := true | |
| for _, v := range args { | |
| if v.MinOccurrences() != 0 && !previousIsRequired { | |
| return nil, errors.ConsoleCommandRequiredArgumentWrongOrder.Args(v.ArgumentName()) | |
| } | |
| if v.MinOccurrences() != 0 { | |
| previousIsRequired = true | |
| } else { | |
| previousIsRequired = false | |
| hasNonRequiredArgument := false | |
| for _, v := range args { | |
| if hasNonRequiredArgument && v.MinOccurrences() != 0 { | |
| return nil, errors.ConsoleCommandRequiredArgumentWrongOrder.Args(v.ArgumentName()) | |
| } | |
| if v.MinOccurrences() == 0 { | |
| hasNonRequiredArgument = true |
| Max: arg.MaxOccurrences(), | ||
| }) | ||
| default: | ||
| return nil, errors.ConsoleCommandArgumentUnknownType.Args(arg, arg) |
Copilot
AI
Sep 19, 2025
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 error formatting passes the same argument twice. The second parameter should be the argument's value or a different property. This will result in redundant information in the error message.
hwbrzzl
left a comment
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.
Awesome, thanks!
📑 Description
adds support for https://cli.urfave.org/v3/examples/arguments/advanced/
--helpprints arguments according to https://pubs.opengroup.org/onlinepubs/9799919799/currently it follows Flag/Options naming pattern , i.e.
but what confuses me, is inconsistency in naming, i.e. type
StringArgumentbut methodArgumentString(),probably type also should be named ArgumentString ?
(probably the same should be done for Flags as well,
BoolFlagvsOptionBool())also there are changes I am not sure if they are fine globally
✅ Checks