diff --git a/.VERSION b/.VERSION index aa33868..e2cfc99 100644 --- a/.VERSION +++ b/.VERSION @@ -1 +1 @@ -v0.1 \ No newline at end of file +v0.2 \ No newline at end of file diff --git a/ctl/bool.go b/ctl/bool.go new file mode 100644 index 0000000..d5ead2a --- /dev/null +++ b/ctl/bool.go @@ -0,0 +1,54 @@ +package ctl + +import ( + "reflect" + "strings" + + "github.com/alecthomas/kong" + "github.com/pkg/errors" +) + +type boolPtrMapper struct{} + +func (boolPtrMapper) Decode(ctx *kong.DecodeContext, target reflect.Value) error { + trueVal := true + falseVal := false + + truePtr := &trueVal + falsePtr := &falseVal + + peekType := ctx.Scan.Peek().Type + if peekType == kong.FlagValueToken { + token := ctx.Scan.Pop() + switch v := token.Value.(type) { + case string: + v = strings.ToLower(v) + switch v { + case "true", "1", "yes": + target.Set(reflect.ValueOf(truePtr)) + + case "false", "0", "no": + target.Set(reflect.ValueOf(falsePtr)) + + default: + return errors.Errorf("bool value must be true, 1, yes, false, 0 or no but got %q", v) + } + + case bool: + target.Set(reflect.ValueOf(&v)) + + default: + return errors.Errorf("expected bool but got %q (%T)", token.Value, token.Value) + } + } else { + target.Set(reflect.ValueOf(truePtr)) + } + return nil +} + +func (boolPtrMapper) IsBool() bool { return true } + +var b bool + +// BoolPtrMapper is an option to register a mapper to *bool type flag +var BoolPtrMapper = kong.TypeMapper(reflect.TypeOf(&b), boolPtrMapper{}) diff --git a/ctl/ctl.go b/ctl/ctl.go new file mode 100644 index 0000000..ba2c665 --- /dev/null +++ b/ctl/ctl.go @@ -0,0 +1,24 @@ +package ctl + +import ( + "fmt" + + "github.com/alecthomas/kong" + "github.com/effective-security/x/slices" +) + +// VersionFlag is a flag to print version +type VersionFlag string + +// Decode the flag +func (v VersionFlag) Decode(_ *kong.DecodeContext) error { return nil } + +// IsBool returns true for the flag +func (v VersionFlag) IsBool() bool { return true } + +// BeforeApply is executed before context is applied +func (v VersionFlag) BeforeApply(app *kong.Kong, vars kong.Vars) error { + fmt.Fprintln(app.Stdout, slices.StringsCoalesce(vars["version"], string(v))) + app.Exit(0) + return nil +} diff --git a/ctl/ctl_test.go b/ctl/ctl_test.go new file mode 100644 index 0000000..1a6ccdc --- /dev/null +++ b/ctl/ctl_test.go @@ -0,0 +1,91 @@ +package ctl + +import ( + "testing" + + "github.com/alecthomas/kong" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestVersionVal(t *testing.T) { + v := VersionFlag("1.2.3") + assert.True(t, v.IsBool()) + assert.NoError(t, v.Decode(nil)) +} + +func TestBool(t *testing.T) { + var bm boolPtrMapper + assert.True(t, bm.IsBool()) +} + +func TestParse(t *testing.T) { + var cl struct { + Version VersionFlag + Cmd struct { + Ptr *bool `help:"test bool ptr"` + } `kong:"cmd"` + } + + p := mustNew(t, &cl) + ctx, err := p.Parse([]string{"cmd", "--ptr=false"}) + require.NoError(t, err) + require.Equal(t, "cmd", ctx.Command()) + if assert.NotNil(t, cl.Cmd.Ptr) { + assert.False(t, *cl.Cmd.Ptr) + } + + ctx, err = p.Parse([]string{"cmd", "--ptr=1"}) + require.NoError(t, err) + require.Equal(t, "cmd", ctx.Command()) + if assert.NotNil(t, cl.Cmd.Ptr) { + assert.True(t, *cl.Cmd.Ptr) + } + + ctx, err = p.Parse([]string{"cmd", "--ptr"}) + require.NoError(t, err) + require.Equal(t, "cmd", ctx.Command()) + if assert.NotNil(t, cl.Cmd.Ptr) { + assert.True(t, *cl.Cmd.Ptr) + } + + _, err = p.Parse([]string{"cmd", "--ptr=invalid"}) + assert.EqualError(t, err, "--ptr: bool value must be true, 1, yes, false, 0 or no but got \"invalid\"") +} + +func TestVersionFlag(t *testing.T) { + var cl struct { + Version VersionFlag + } + cl.Version = "1.2.3" + + options := []kong.Option{ + kong.Name("test"), + kong.Exit(func(int) { + t.Helper() + + }), + BoolPtrMapper, + } + parser, err := kong.New(&cl, options...) + require.NoError(t, err) + + _, err = parser.Parse([]string{"--version"}) + require.NoError(t, err) +} + +func mustNew(t *testing.T, cli interface{}, options ...kong.Option) *kong.Kong { + t.Helper() + options = append([]kong.Option{ + kong.Name("test"), + kong.Exit(func(int) { + t.Helper() + t.Fatalf("unexpected exit()") + }), + BoolPtrMapper, + }, options...) + parser, err := kong.New(cli, options...) + require.NoError(t, err) + + return parser +} diff --git a/go.mod b/go.mod index 8e47239..929c74b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/effective-security/x go 1.21 require ( + github.com/alecthomas/kong v0.8.1 github.com/deckarep/golang-set v1.8.0 github.com/effective-security/xlog v0.6.0 github.com/oleiade/reflections v1.0.1 diff --git a/go.sum b/go.sum index b00c333..4c2aa89 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0= +github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA= +github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY= +github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= +github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= +github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= 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= @@ -8,6 +14,8 @@ github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS3 github.com/effective-security/xlog v0.6.0 h1:n1MzotZSHZ1+XMO3CQcc7xEO8y+0BMbNEHA0SsTLs/8= github.com/effective-security/xlog v0.6.0/go.mod h1:ZDG9qha5Mt18D5DNd/8WhHXzw3f9JeOUVcXHYvWu3/U= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=