Skip to content
Snippets Groups Projects
Verified Commit 938e83fd authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

feat new opportunities for interaction with flags #1

parent a520513e
No related branches found
No related tags found
No related merge requests found
......@@ -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
......
......@@ -12,26 +12,53 @@ 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
}
if (reflect.ValueOf(&cnf).Elem().Type() != reflect.TypeOf(noShadow{})) {
instance.SetShadow(cnf)
if instance.HasErrors() {
return instance
}
}
instance.Parse(args)
if instance.HelpRequested() {
......@@ -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
}
......
......@@ -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) {
......
......@@ -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))
}
......@@ -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)
}
......@@ -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() {
shouldExecute = true
currentCommand = command.name
if len(command.commands) > 0 {
r := callCmdFunctions(settings, command.commands)
if r {
wasExecuted = true
}
}
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
}
callCmdFunctions(command.commands)
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
}
......@@ -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 {
......
......@@ -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())
}
......@@ -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
......
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)
}
......@@ -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
}
......@@ -30,6 +30,9 @@ type Settings[C any] struct {
config config
shadow any
wasExecuted bool
hint string
}
func (s *Settings[C]) GetValues() C {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment