-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Fix:(issue_2066) Remove dependency on golang flag library #2074
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
Hi @dearchap. It would be useful to document all the bad sides, because eventually it may improve upstream state of things for everybody. Like the fresh story here https://www.dolthub.com/blog/2025-03-07-archiving-the-dolthub-swiss-github-repository/ We just need one page to do that. TBH I am puzzled what's wrong with Go |
@abitrolly There is no issue itself with golang flag. However its parsing is not exposed so we have had to workaround to support short options, skipping flag parsing, positional arguments, detect underlying errors based on error string and so on. This gives us an opportunity to get rid of it altogether. The flag imports are still needed since we support ExtFlags i.e flags defined in the golang flag libary can co-exist with cli flag. |
Thanks for your work! Since this PR is too large for me to review in its entirety. My expectations are as follows:
In summary: If Regarding array-type flag inputs, I am unsure whether v3 still supports a delimiter option (as the documentation does not mention it). My primary concern is fixing the related issues in v2. Additionally, one very important requirement is the ability to configure the subcommand separator—either by exposing an API for it or by simplifying the configuration while keeping it consistent with the current declarative API. As for the More Test cases
|
Name | Input | Expected --flag value |
Expected args | error? | Expected error message |
---|---|---|---|---|---|
flag and args after terminator | ["test", "--flag", "x", "--", "test", "a1", "a2", "a3"] |
"x" |
["test", "a1", "a2", "a3"] |
No | - |
missing flag value due to terminator | ["test", "--flag", "--", "x"] |
- | - | Yes | "flag needs an argument" |
terminator with no trailing args | ["test", "--flag", "x", "--"] |
"x" |
[] |
No | - |
no terminator, only flag | ["test", "--flag", "x"] |
"x" |
[] |
No | - |
flag defined after -- |
["test", "--", "x", "--flag=value"] |
"" |
["x", "--flag=value"] |
No | - |
flag and without -- |
["test", "--flag", "value", "x"] |
"value" |
["x"] |
No | - |
TestBoolFlagTerminator
Name | Input | Expected --flag value |
Expected args | error? | Expected error message |
---|---|---|---|---|---|
bool flag with invalid non-bool value | ["test", "--flag", "x", "--", "test", "a1", "a2", "a3"] |
- | - | Yes | "invalid syntax" |
bool flag omitted value defaults to true | ["test", "--flag", "--", "x"] |
true |
["x"] |
No | - |
bool flag explicitly set to false | ["test", "--flag=false", "--", "x"] |
false |
["x"] |
No | - |
bool flag defined after -- |
["test", "--", "x", "--flag=true"] |
false |
["x", "--flag=true"] |
No | - |
bool flag and without -- |
["test", "--flag=true", "x"] |
true |
["x"] |
No | - |
TestSliceStringFlagParsing
Name | Input | Expected --flag value |
Expected args | error? | Expected error message |
---|---|---|---|---|---|
single value without delimiter (no split) | cmdNoDelimiter |
["test", "--flag", "x"] |
["x"] |
No | - |
multiple values with comma (default split) | cmdNoDelimiter |
["test", "--flag", "x,y"] |
["x", "y"] |
No | - |
without delimiter specified, value remains unsplit | cmdNoDelimiter |
["test", "--flag", "x:y"] |
["x:y"] |
No | - |
Code
I had an AI generate a unit test based on my provided test cases and ran it briefly. It appears that in the latest PR version, --
is still always retained in args
, requiring users to handle it manually.
package cli_test
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v3"
)
// TestStringFlagTerminator tests the string flag "--flag" with "--" terminator.
func TestStringFlagTerminator(t *testing.T) {
var flagVal string
var argsVal []string
// build minimal command with a StringFlag "flag"
cmd := &cli.Command{
Name: "test",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "flag",
Usage: "a string flag",
Destination: &flagVal,
},
},
Action: func(ctx context.Context, c *cli.Command) error {
argsVal = c.Args().Slice()
return nil
},
}
tests := []struct {
name string
input []string
expectFlag string
expectArgs []string
expectErr bool
errorContain string
}{
{
name: "flag and args after terminator",
input: []string{"test", "--flag", "x", "--", "test", "a1", "a2", "a3"},
expectFlag: "x",
expectArgs: []string{"test", "a1", "a2", "a3"},
},
{
name: "missing flag value due to terminator",
input: []string{"test", "--flag", "--", "x"},
expectErr: true,
errorContain: "flag needs an argument",
},
{
name: "terminator with no trailing args",
input: []string{"test", "--flag", "x", "--"},
expectFlag: "x",
expectArgs: []string{},
},
{
name: "no terminator, only flag",
input: []string{"test", "--flag", "x"},
expectFlag: "x",
expectArgs: []string{},
},
{
name: "flag defined after --",
input: []string{"test", "--", "x", "--flag=value"},
expectFlag: "",
expectArgs: []string{"x", "--flag=value"},
},
{
name: "flag and without --",
input: []string{"test", "--flag", "value","x"},
expectFlag: "value",
expectArgs: []string{"x"},
},
}
for _, tc := range tests {
// Reset values
flagVal = ""
argsVal = nil
t.Run(tc.name, func(t *testing.T) {
err := cmd.Run(context.Background(), tc.input)
if tc.expectErr {
assert.Error(t, err)
if err != nil {
assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.errorContain))
}
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectFlag, flagVal)
assert.Equal(t, tc.expectArgs, argsVal)
}
})
}
}
// TestBoolFlagTerminator tests the bool flag
func TestBoolFlagTerminator(t *testing.T) {
var flagVal bool
var argsVal []string
// build minimal command with a BoolFlag "flag"
cmd := &cli.Command{
Name: "test",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "flag",
Usage: "a bool flag",
Destination: &flagVal,
},
},
Action: func(ctx context.Context, c *cli.Command) error {
argsVal = c.Args().Slice()
return nil
},
}
tests := []struct {
name string
input []string
expectFlag bool
expectArgs []string
expectErr bool
errorContain string
}{
{
name: "bool flag with invalid non-bool value",
input: []string{"test", "--flag", "x", "--", "test", "a1", "a2", "a3"},
expectErr: true,
errorContain: "invalid syntax",
},
{
name: "bool flag omitted value defaults to true",
input: []string{"test", "--flag", "--", "x"},
expectFlag: true,
expectArgs: []string{"x"},
},
{
name: "bool flag explicitly set to false",
input: []string{"test", "--flag=false", "--", "x"},
expectFlag: false,
expectArgs: []string{"x"},
},
{
name: "bool flag defined after --",
input: []string{"test", "--", "x", "--flag=true"},
expectFlag: false,
expectArgs: []string{"x", "--flag=true"},
},
{
name: "bool flag and without --",
input: []string{"test", "--flag=true", "x"},
expectFlag: true,
expectArgs: []string{"x"},
},
}
for _, tc := range tests {
// Reset flag and args
flagVal = false
argsVal = nil
t.Run(tc.name, func(t *testing.T) {
err := cmd.Run(context.Background(), tc.input)
if tc.expectErr {
assert.Error(t, err)
if err != nil {
assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.errorContain))
}
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectFlag, flagVal)
assert.Equal(t, tc.expectArgs, argsVal)
}
})
}
}
// TestSliceStringFlagParsing tests the StringSliceFlag
func TestSliceStringFlagParsing(t *testing.T) {
var sliceVal []string
cmdNoDelimiter := &cli.Command{
Name: "test",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "flag",
Usage: "a string slice flag without delimiter",
},
},
Action: func(ctx context.Context, c *cli.Command) error {
sliceVal = c.StringSlice("flag")
return nil
},
}
/*cmdWithDelimiter := &cli.Command{
Name: "test",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "flag",
Usage: "a string slice flag with delimiter",
Delimiter: ':',
},
},
Action: func(ctx context.Context, c *cli.Command) error {
sliceVal = c.StringSlice("flag")
return nil
},
}*/
tests := []struct {
name string
cmd *cli.Command
input []string
expectSlice []string
expectErr bool
errorContain string
}{
{
name: "single value without delimiter (no split)",
cmd: cmdNoDelimiter,
input: []string{"test", "--flag", "x"},
expectSlice: []string{"x"},
},
{
name: "multiple values with comma (default split)",
cmd: cmdNoDelimiter,
input: []string{"test", "--flag", "x,y"},
expectSlice: []string{"x", "y"},
},
/*{
name: "Case 10: with delimiter specified ':'",
cmd: cmdWithDelimiter,
input: []string{"test", "--flag", "x:y"},
expectSlice: []string{"x", "y"},
},*/
{
name: "without delimiter specified, value remains unsplit",
cmd: cmdNoDelimiter,
input: []string{"test", "--flag", "x:y"},
expectSlice: []string{"x:y"},
},
}
for _, tc := range tests {
// Reset sliceVal
sliceVal = nil
t.Run(tc.name, func(t *testing.T) {
err := tc.cmd.Run(context.Background(), tc.input)
if tc.expectErr {
assert.Error(t, err)
if err != nil {
assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.errorContain))
}
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectSlice, sliceVal)
}
})
}
}
Output
--- FAIL: TestStringFlagTerminator (0.00s)
--- FAIL: TestStringFlagTerminator/flag_and_args_after_terminator (0.00s)
my_test.go:95:
Error Trace: /workspaces/cli/tests/my_test.go:95
Error: Not equal:
expected: []string{"test", "a1", "a2", "a3"}
actual : []string{"--", "test", "a1", "a2", "a3"}
Diff:
--- Expected
+++ Actual
@@ -1,2 +1,3 @@
-([]string) (len=4) {
+([]string) (len=5) {
+ (string) (len=2) "--",
(string) (len=4) "test",
Test: TestStringFlagTerminator/flag_and_args_after_terminator
--- FAIL: TestStringFlagTerminator/missing_flag_value_due_to_terminator (0.00s)
my_test.go:87:
Error Trace: /workspaces/cli/tests/my_test.go:87
Error: An error is expected but got nil.
Test: TestStringFlagTerminator/missing_flag_value_due_to_terminator
--- FAIL: TestStringFlagTerminator/terminator_with_no_trailing_args (0.00s)
my_test.go:95:
Error Trace: /workspaces/cli/tests/my_test.go:95
Error: Not equal:
expected: []string{}
actual : []string{"--"}
Diff:
--- Expected
+++ Actual
@@ -1,2 +1,3 @@
-([]string) {
+([]string) (len=1) {
+ (string) (len=2) "--"
}
Test: TestStringFlagTerminator/terminator_with_no_trailing_args
--- FAIL: TestStringFlagTerminator/flag_defined_after_-- (0.00s)
my_test.go:95:
Error Trace: /workspaces/cli/tests/my_test.go:95
Error: Not equal:
expected: []string{"x", "--flag=value"}
actual : []string{"--", "x", "--flag=value"}
Diff:
--- Expected
+++ Actual
@@ -1,2 +1,3 @@
-([]string) (len=2) {
+([]string) (len=3) {
+ (string) (len=2) "--",
(string) (len=1) "x",
Test: TestStringFlagTerminator/flag_defined_after_--
--- FAIL: TestBoolFlagTerminator (0.00s)
--- FAIL: TestBoolFlagTerminator/bool_flag_with_invalid_non-bool_value (0.00s)
my_test.go:170:
Error Trace: /workspaces/cli/tests/my_test.go:170
Error: An error is expected but got nil.
Test: TestBoolFlagTerminator/bool_flag_with_invalid_non-bool_value
--- FAIL: TestBoolFlagTerminator/bool_flag_omitted_value_defaults_to_true (0.00s)
my_test.go:177:
Error Trace: /workspaces/cli/tests/my_test.go:177
Error: Not equal:
expected: []string{"x"}
actual : []string{"--", "x"}
Diff:
--- Expected
+++ Actual
@@ -1,2 +1,3 @@
-([]string) (len=1) {
+([]string) (len=2) {
+ (string) (len=2) "--",
(string) (len=1) "x"
Test: TestBoolFlagTerminator/bool_flag_omitted_value_defaults_to_true
--- FAIL: TestBoolFlagTerminator/bool_flag_explicitly_set_to_false (0.00s)
my_test.go:177:
Error Trace: /workspaces/cli/tests/my_test.go:177
Error: Not equal:
expected: []string{"x"}
actual : []string{"--", "x"}
Diff:
--- Expected
+++ Actual
@@ -1,2 +1,3 @@
-([]string) (len=1) {
+([]string) (len=2) {
+ (string) (len=2) "--",
(string) (len=1) "x"
Test: TestBoolFlagTerminator/bool_flag_explicitly_set_to_false
--- FAIL: TestBoolFlagTerminator/bool_flag_defined_after_-- (0.00s)
my_test.go:177:
Error Trace: /workspaces/cli/tests/my_test.go:177
Error: Not equal:
expected: []string{"x", "--flag=true"}
actual : []string{"--", "x", "--flag=true"}
Diff:
--- Expected
+++ Actual
@@ -1,2 +1,3 @@
-([]string) (len=2) {
+([]string) (len=3) {
+ (string) (len=2) "--",
(string) (len=1) "x",
Test: TestBoolFlagTerminator/bool_flag_defined_after_--
FAIL
FAIL command-line-arguments 0.004s
FAIL
@System233 Thank you for the exhaustive test cases and the test code. Much appreciated. I have added it to the test suite and they all pass with this PR changes with 2 exceptions, both very tricky cases
|
Name | Input | Expected --flag value |
Expected args | error? | Expected error message |
---|---|---|---|---|---|
missing flag value due to terminator | ["test", "--flag", "--", "x"] |
- | - | Yes | "flag needs an argument" |
How do we know that the user doesnt want the "--" to be the value for the string flag ?
TestBoolFlagTerminator
Name | Input | Expected --flag value |
Expected args | error? | Expected error message |
---|---|---|---|---|---|
bool flag with invalid non-bool value | ["test", "--flag", "x", "--", "test", "a1", "a2", "a3"] |
- | - | Yes | "invalid syntax" |
How do we know the 'x' is not actually an arg that the user wants ?
This issue here has been raised before for example bool flags are not expected to have args and sometime users do type
--boolFlag true
and get a cryptic error due to true being parsed as an arg subsequently.
Would love your thoughts on both these cases .
I agree with your view. In the first case, treating Regarding the |
This library does not support |
What type of PR is this?
(REQUIRED)
What this PR does / why we need it:
(REQUIRED)
Remove dependence of this library on golang's flag library. We are being constrained by parsing capabilities of golang's flag library and have to create workarounds. This PR does away with all workarounds and does the parsing of the arguments from scratch. The command code has been split into multiple files
All prior workaround from parse.go have been folded into command_parse.go. The BoolWithInverseFlag has been rewritten from scratch to do away with hacky behaviour of using a BoolFlag underneath. All tests have been updated to remove flag.FlagSet requirement. The public interface for Flags has been kept the same with the exception of addition of lifecycle functions for the Flag interface. (TBD. should those be moved into a separate Lifecycle interface)
Which issue(s) this PR fixes:
(REQUIRED)
Fixes #2066
Fixes #2078
Special notes for your reviewer:
(fill-in or delete this section)
Testing
(fill-in or delete this section)
go test -run=.
Release Notes
(REQUIRED)