From 938e83fd91218f921a58e5e4b5cacb461c9abb3a Mon Sep 17 00:00:00 2001 From: Volker Schukai <volker.schukai@schukai.com> Date: Thu, 13 Oct 2022 15:24:26 +0200 Subject: [PATCH] feat new opportunities for interaction with flags #1 --- README.md | 15 ++++- api.go | 44 +++++++++++--- api_test.go | 11 +++- error.go | 14 +++++ error_test.go | 6 ++ execute.go | 91 +++++++++++++++++++++++++--- execute_test.go | 13 ++-- flag_test.go | 7 ++- go.mod | 1 + go.sum | 4 ++ issue-1_test.go | 157 ++++++++++++++++++++++++++++++++++++++++++++++++ parse.go | 12 ---- setting.go | 5 +- 13 files changed, 343 insertions(+), 37 deletions(-) create mode 100644 issue-1_test.go diff --git a/README.md b/README.md index a65731f..0933ea7 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,9 @@ The following tags are supported: | `long` | Value | Long name of the flag. | | `description` | Value | Description of the flag. | | `required` | Value | Flag is required. | +| `shadow` | Value | Copy the value to the shadow structure. | | `command` | Command | Flag is a command. | | `call` | Command | Function to call when the command is used. | -| `shadow` | Value | Copy the value to the shadow structure. | | `ignore` | -/- | Property is ignored. | ### Callbacks @@ -170,6 +170,19 @@ func main() { } ``` +### Arguments + +the free arguments can be fetched with the method `Args()`. + +### Check Status + +The execution result can be queried with the functions: + +- `HelpRequested() bool` +- `WasExecuted() bool` +- `Error() error` +- `MissingCommand() bool` + ## Contributing Merge requests are welcome. For major changes, please open an issue first to discuss what diff --git a/api.go b/api.go index bec82e0..a480886 100644 --- a/api.go +++ b/api.go @@ -12,25 +12,52 @@ import ( "reflect" ) +// ExecuteWithShadow executes the command line arguments and calls the functions. +func ExecuteWithShadow[C any, D any](cmd C, cnf D) *Settings[C] { + s := execute(cmd, cnf, os.Args[0], os.Args[1:]) + + if s.HasErrors() { + for _, e := range s.Errors() { + fmt.Println(e) + } + } + return s +} + +type noShadow struct{} + // Execute executes the command line arguments and calls the functions. -func Execute[C any, D any](cmd C, cnf D) *Settings[C] { - return execute(cmd, cnf, os.Args[0], os.Args[1:]) +func Execute[C any](cmd C) *Settings[C] { + s := execute(cmd, noShadow{}, os.Args[0], os.Args[1:]) + if s.HasErrors() { + for _, e := range s.Errors() { + fmt.Println(e) + } + } + + if s.hint != "" { + fmt.Println(s.hint) + } + + return s } func (s *Settings[C]) PrintFlagOutput() { fmt.Println(s.command.flagSet.Output()) } -// execute is the internal implementation of Execute. +// execute is the internal implementation of ExecuteWithShadow. func execute[C any, D any](cmd C, cnf D, name string, args []string) *Settings[C] { instance := New(name, cmd) if instance.HasErrors() { return instance } - instance.SetShadow(cnf) - if instance.HasErrors() { - return instance + if (reflect.ValueOf(&cnf).Elem().Type() != reflect.TypeOf(noShadow{})) { + instance.SetShadow(cnf) + if instance.HasErrors() { + return instance + } } instance.Parse(args) @@ -40,7 +67,6 @@ func execute[C any, D any](cmd C, cnf D, name string, args []string) *Settings[C instance.errors = append(instance.errors[:i], instance.errors[i+1:]...) } } - instance.PrintFlagOutput() return instance } @@ -49,6 +75,10 @@ func execute[C any, D any](cmd C, cnf D, name string, args []string) *Settings[C } instance.Execute() + if instance.HasErrors() { + return instance + } + return instance } diff --git a/api_test.go b/api_test.go index 6b841c8..008958d 100644 --- a/api_test.go +++ b/api_test.go @@ -21,9 +21,16 @@ func TestUsage(t *testing.T) { assert.Equal(t, " -a\tMessage A\n -x int\n \tMessage X\n", usage) } -func TestExecute(t *testing.T) { +func TestExecuteTypeStringIsNotSupported(t *testing.T) { instance := execute("root", CmdTest1{}, "test", []string{"-a", "hello", "-x", "1"}) - assert.NotNil(t, instance) + + err := instance.Errors() + assert.True(t, instance.HasErrors()) + assert.Equal(t, 1, len(err)) + + e := err[0] + assert.Equal(t, "type string is not supported", e.Error()) + } func TestExecuteHelp(t *testing.T) { diff --git a/error.go b/error.go index a0eca94..0f14e2e 100644 --- a/error.go +++ b/error.go @@ -20,6 +20,14 @@ func (s *Settings[C]) HasErrors() bool { return len(s.errors) > 0 } +func (s *Settings[C]) HasHint() bool { + return s.hint != "" +} + +func (s *Settings[C]) GetHint() string { + return s.hint +} + // Get all errors func (s *Settings[C]) Errors() []error { return s.errors @@ -112,3 +120,9 @@ type StdoutError error func newStdoutError(message string) StdoutError { return StdoutError(errors.New(message)) } + +type MissingFunctionError error + +func newMissingFunctionError(missing string) MissingFunctionError { + return MissingFunctionError(errors.New("missing function " + missing)) +} diff --git a/error_test.go b/error_test.go index ddc568a..46fc294 100644 --- a/error_test.go +++ b/error_test.go @@ -90,3 +90,9 @@ func TestStdoutError(t *testing.T) { _, ok := err.(StdoutError) assert.True(t, ok) } + +func TestMissingFunctionError(t *testing.T) { + err := newMissingFunctionError("test") + _, ok := err.(MissingFunctionError) + assert.True(t, ok) +} diff --git a/execute.go b/execute.go index 447787b..e2fcd76 100644 --- a/execute.go +++ b/execute.go @@ -4,7 +4,10 @@ package xflags import ( + "flag" + "fmt" "reflect" + "strings" ) func (s *Settings[C]) Execute() *Settings[C] { @@ -20,25 +23,97 @@ func (s *Settings[C]) Execute() *Settings[C] { if !s.command.flagSet.Parsed() { s.errors = append(s.errors, NotParsedError) } else { - callCmdFunctions(s.command.commands) + s.wasExecuted = callCmdFunctions(s, s.command.commands) } return s } -func callCmdFunctions[C any](commands []*cmd[C]) { +func callCmdFunctions[C any](settings *Settings[C], commands []*cmd[C]) bool { + + //result := false + wasExecuted := false + shouldExecute := false + + var availableCommands []string + currentCommand := "" + for _, command := range commands { if command.flagSet.Parsed() { - f := reflect.ValueOf(&command.settings.definitions).MethodByName(command.functionName) - if f.IsValid() { - m := command.settings - in := []reflect.Value{reflect.ValueOf(m)} - f.Call(in) + shouldExecute = true + currentCommand = command.name + + if len(command.commands) > 0 { + r := callCmdFunctions(settings, command.commands) + if r { + wasExecuted = true + } } - callCmdFunctions(command.commands) + if !wasExecuted { + f := reflect.ValueOf(&command.settings.definitions).MethodByName(command.functionName) + if f.IsValid() { + m := command.settings + in := []reflect.Value{reflect.ValueOf(m)} + f.Call(in) + wasExecuted = true + } + } + + break + } else { + availableCommands = append(availableCommands, command.name) + } + + } + + if shouldExecute { + if !wasExecuted { + settings.errors = append(settings.errors, newMissingFunctionError(currentCommand)) + return false + } + + return true + } + + if len(availableCommands) > 0 { + if settings.hint == "" { + settings.hint = fmt.Sprintf("Did you mean: %v?", strings.Join(availableCommands, ", ")) + } + } + + settings.errors = append(settings.errors, MissingCommandError) + + return false + +} + +// HelpRequested indicates if the help flag was set. +func (s *Settings[C]) HelpRequested() bool { + + for _, err := range s.errors { + if err == flag.ErrHelp { + return true } } + return false +} + +// MissingCommandError is returned if no command was found. +func (s *Settings[C]) MissingCommand() bool { + + for _, err := range s.errors { + if err == MissingCommandError { + return true + } + } + + return false +} + +// WasExecuted returns true if the call function was executed +func (s *Settings[C]) WasExecuted() bool { + return s.wasExecuted } diff --git a/execute_test.go b/execute_test.go index aebd1bf..f44dfac 100644 --- a/execute_test.go +++ b/execute_test.go @@ -44,6 +44,7 @@ type testExecutionTestCases[C any] struct { name string args []string targetValue int + hasErrors bool } func TestExec(t *testing.T) { @@ -53,19 +54,22 @@ func TestExec(t *testing.T) { name: "test", args: []string{"command1", "-c"}, targetValue: 1, + hasErrors: true, }, { name: "test", args: []string{"command1", "command2", "-e"}, - targetValue: 2, + targetValue: 1, + hasErrors: true, }, { name: "test", args: []string{"-a", "command3"}, targetValue: 1, + hasErrors: true, }, } - for _, tt := range tests { + for i, tt := range tests { t.Run(tt.args[0], func(t *testing.T) { settings := New(tt.name, testExecutionStruct{}) assert.NotNil(t, settings) @@ -77,9 +81,10 @@ func TestExec(t *testing.T) { settings.Parse(tt.args) settings.Execute() - if settings.HasErrors() { - t.Error("Should not have errors") + if !tt.hasErrors && settings.HasErrors() { + t.Error("run " + strconv.Itoa(i) + ": should not have errors but has: " + strconv.Itoa(len(settings.Errors()))) t.Log(settings.Errors()) + return } if settings.definitions.callbackCounter != tt.targetValue { diff --git a/flag_test.go b/flag_test.go index b162036..6022459 100644 --- a/flag_test.go +++ b/flag_test.go @@ -21,17 +21,20 @@ type testExecuteCommandStruct struct { } `command:"command1" description:"Command 1" callback:"command1Callback" ` Command2 struct { Command3 struct { - } `command:"command3" description:"Command 3" callback:"command3Callback" ` + } `command:"command3" description:"Command 3" callback:"command3Callback" call:"DoCmd3"` } `command:"command2" description:"Command 2" callback:"command2Callback" ` } +func (c *testExecuteCommandStruct) DoCmd3(s *Settings[testExecuteCommandStruct]) { +} + func (c *testExecuteCommandStruct) command1Callback(args []string) { fmt.Println("command1Callback", args) } func TestExecute1(t *testing.T) { c := New("root", testExecuteCommandStruct{}) - c.Parse([]string{"root", "command2", "command3", "commandX"}) + c.Parse([]string{"command2", "command3", "commandX"}) c.Execute() assert.False(t, c.HasErrors()) } diff --git a/go.mod b/go.mod index d59d560..c30c8e2 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require github.com/stretchr/testify v1.8.0 require ( + github.com/agnivade/levenshtein v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 5164829..06cda3f 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,10 @@ +github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= +github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/issue-1_test.go b/issue-1_test.go new file mode 100644 index 0000000..931d4c2 --- /dev/null +++ b/issue-1_test.go @@ -0,0 +1,157 @@ +package xflags + +import ( + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +type testCmdStructIssue1 struct { + Cmd1 struct { + Cmd2 struct { + Cmd3 struct { + V3 bool `short:"v3"` + } `command:"cmd3" call:"DoCmd3"` + } `command:"cmd2"` + Cmd4 struct { + Cmd5 struct { + } `command:"cmd5" call:"DoCmd5"` + } `command:"cmd4"` + Cmd6 struct { + Cmd7 struct { + } `command:"cmd7"` + } `command:"cmd6"` + } `command:"cmd1"` +} + +func (c *testCmdStructIssue1) DoCmd3(s *Settings[testCmdStructIssue1]) { + +} +func (c *testCmdStructIssue1) DoCmd5(s *Settings[testCmdStructIssue1]) { + +} + +type testStructIssue1Config struct { +} + +// here it is tested whether the last and defined callback is called +func TestIssue1TestCallCMD4(tp *testing.T) { + + testData := []struct { + args []string + isExecuted bool + hasErrors bool + hasHint bool + }{ + {[]string{"cmd1"}, false, true, true}, + {[]string{}, false, true, true}, + {[]string{"cmd1", "cmd2"}, false, true, true}, + {[]string{"cmd1", "cmd2", "cmd3"}, true, false, false}, + {[]string{"cmd1", "cmd4", "cmd5"}, true, false, false}, + {[]string{"cmd1"}, false, true, true}, + {[]string{"cmd1", "cmd6"}, false, true, true}, + {[]string{"cmd1", "cmd6", "cmd7"}, false, true, false}, + } + + for i, tt := range testData { + tp.Run(strconv.Itoa(i)+":"+strings.Join(tt.args, ","), func(t *testing.T) { + s := execute(testCmdStructIssue1{}, noShadow{}, "test", tt.args) + assert.Equal(t, tt.hasErrors, s.HasErrors()) + assert.Equal(t, tt.hasHint, s.HasHint()) + + if !tt.hasErrors && s.HasErrors() { + t.Log(s.Errors()) + } + + assert.Equal(t, tt.isExecuted, s.WasExecuted()) + }) + } + +} + +func TestIssue1TestNoCallback(t *testing.T) { + s := execute(testCmdStructIssue1{}, noShadow{}, "test", []string{"cmd1", "cmd6", "cmd7"}) + assert.Equal(t, 3, len(s.Errors())) + assert.False(t, s.WasExecuted()) +} + +func TestIssue1TestToMuchCommands(t *testing.T) { + s := execute(testCmdStructIssue1{}, noShadow{}, "test", []string{"cmd1", "cmd9"}) + assert.True(t, s.MissingCommand()) + assert.False(t, s.WasExecuted()) +} + +// here it is tested whether the last and defined callback is called +func TestIssue1TestCallCMD3(t *testing.T) { + s := execute(testCmdStructIssue1{}, noShadow{}, "test", []string{"cmd1", "cmd2", "cmd3"}) + assert.Equal(t, 0, len(s.Errors())) + assert.True(t, s.WasExecuted()) +} + +// NoShadow is an internal Struct for testing +func TestIssue1MessageWithNoShadow(t *testing.T) { + s := execute(testCmdStructIssue1{}, noShadow{}, "test", []string{"cmd1", "cmd2", "cmd3", "-v3"}) + assert.Equal(t, 0, len(s.Errors())) +} + +func TestIssue1MRequestHelp(t *testing.T) { + + s := New("test", testCmdStructIssue1{}) + s.Parse([]string{"cmd1", "cmd2", "--help"}) + assert.True(t, s.HelpRequested()) +} + +func TestIssue1A(t *testing.T) { + + s := New("test", testCmdStructIssue1{}) + s.Parse([]string{"cmd1", "cmd2", "cmd3", "-v3"}) + assert.False(t, s.HelpRequested()) + + assert.Equal(t, 0, len(s.Errors())) + assert.True(t, s.GetValues().Cmd1.Cmd2.Cmd3.V3) +} +func TestIssue1HelpRequested(t *testing.T) { + + s := New("test", testCmdStructIssue1{}) + s.Parse([]string{"cmd1", "-h"}) + assert.True(t, s.HelpRequested()) + + assert.Equal(t, 1, len(s.Errors())) +} + +func TestIssue1Summary(tp *testing.T) { + + data := []struct { + args []string + errorCount int + helpRequested bool + wasExecuted bool + missingCommand bool + }{ + {[]string{"cmd1", "cmd2", "cmd3", "-v3"}, 0, false, true, false}, + } + + for _, tt := range data { + tp.Run(tt.args[0], func(t *testing.T) { + + s := execute(testCmdStructIssue1{}, noShadow{}, "test", tt.args) + assert.Equal(t, tt.helpRequested, s.HelpRequested()) + assert.Equal(t, tt.wasExecuted, s.WasExecuted()) + assert.Equal(t, tt.missingCommand, s.MissingCommand()) + assert.Equal(t, tt.errorCount, len(s.Errors())) + }) + } + +} + +func TestIssue1B(t *testing.T) { + + s := New("test", testCmdStructIssue1{}) + s.Parse([]string{"cmd1", "cmd2", "cmd3"}) + assert.False(t, s.HelpRequested()) + + assert.Equal(t, 0, len(s.Errors())) + assert.False(t, s.GetValues().Cmd1.Cmd2.Cmd3.V3) +} diff --git a/parse.go b/parse.go index 9a5b720..b659968 100644 --- a/parse.go +++ b/parse.go @@ -4,7 +4,6 @@ package xflags import ( - "flag" "os" ) @@ -41,14 +40,3 @@ func (s *Settings[C]) Parse(args []string) *Settings[C] { return s } - -func (s *Settings[C]) HelpRequested() bool { - - for _, err := range s.errors { - if err == flag.ErrHelp { - return true - } - } - - return false -} diff --git a/setting.go b/setting.go index d2e7445..3aed34a 100644 --- a/setting.go +++ b/setting.go @@ -29,7 +29,10 @@ type Settings[C any] struct { config config - shadow any + shadow any + wasExecuted bool + + hint string } func (s *Settings[C]) GetValues() C { -- GitLab