Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • oss/libraries/go/application/xflags
1 result
Select Git revision
Show changes
Commits on Source (4)
<a name="v1.5.0"></a>
## [v1.5.0] - 2022-10-13
### Add Features
- feat new opportunities for interaction with flags [#1](https://gitlab.schukai.com/oss/libraries/go/application/xflags/issues/1)
### Changes
- chore add license
<a name="v1.4.0"></a>
## [v1.4.0] - 2022-10-09
### Add Features
......@@ -59,6 +68,7 @@
<a name="v1.0.0"></a>
## v1.0.0 - 2022-10-04
[v1.5.0]: https://gitlab.schukai.com/oss/libraries/go/application/xflags/compare/v1.4.0...v1.5.0
[v1.4.0]: https://gitlab.schukai.com/oss/libraries/go/application/xflags/compare/v1.3.1...v1.4.0
[v1.3.1]: https://gitlab.schukai.com/oss/libraries/go/application/xflags/compare/v1.3.0...v1.3.1
[v1.3.0]: https://gitlab.schukai.com/oss/libraries/go/application/xflags/compare/v1.2.3...v1.3.0
......
......@@ -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,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
}
......
......@@ -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() {
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
}
......@@ -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
......
// Copyright 2022 schukai GmbH
// SPDX-License-Identifier: AGPL-3.0
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
}
{"version":"1.4.0"}
{"version":"1.5.0"}
......@@ -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 {
......