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