From 4a21f9b42785bb75e40cd9eced14dd5518cf3d96 Mon Sep 17 00:00:00 2001 From: Joice Date: Wed, 1 Jan 2025 08:08:48 +0000 Subject: [PATCH] Add urfave/cli/v2 flag provider (#330) --- README.md | 1 + go.work | 1 + providers/cliflagv2/cliflagv2.go | 162 ++++++++++++++++++++++++++ providers/cliflagv2/cliflagv2_test.go | 79 +++++++++++++ providers/cliflagv2/go.mod | 20 ++++ providers/cliflagv2/go.sum | 24 ++++ 6 files changed, 287 insertions(+) create mode 100644 providers/cliflagv2/cliflagv2.go create mode 100644 providers/cliflagv2/cliflagv2_test.go create mode 100644 providers/cliflagv2/go.mod create mode 100644 providers/cliflagv2/go.sum diff --git a/README.md b/README.md index 506678aa..dbf4d7a5 100644 --- a/README.md +++ b/README.md @@ -672,6 +672,7 @@ Install with `go get -u github.com/knadh/koanf/providers/$provider` | etcd/v2 | `etcd.Provider(etcd.Config{})` | CNCF etcd provider | | consul/v2 | `consul.Provider(consul.Config{})` | Hashicorp Consul provider | | parameterstore/v2 | `parameterstore.Provider(parameterstore.Config{})` | AWS Systems Manager Parameter Store provider | +| cliflagv2 | `cliflagv2.Provider(ctx *cli.Context, delimiter string)` | Reads commands and flags from urfave/cli/v2 context including global flags and nested command flags and provides a nested config map based on delim. | ### Bundled Parsers diff --git a/go.work b/go.work index b5337e02..eb66f3f8 100644 --- a/go.work +++ b/go.work @@ -27,5 +27,6 @@ use ( ./providers/s3 ./providers/structs ./providers/vault + ./providers/cliflagv2 ./tests ) diff --git a/providers/cliflagv2/cliflagv2.go b/providers/cliflagv2/cliflagv2.go new file mode 100644 index 00000000..b366483e --- /dev/null +++ b/providers/cliflagv2/cliflagv2.go @@ -0,0 +1,162 @@ +// Package cliflagv2 implements a koanf.Provider that reads commandline +// parameters as conf maps using ufafe/cli/v2 flag. +package cliflagv2 + +import ( + "errors" + "strings" + + "github.com/knadh/koanf/maps" + "github.com/urfave/cli/v2" +) + +// CliFlag implements a cli.Flag command line provider. +type CliFlag struct { + ctx *cli.Context + delim string +} + +// Provider returns a commandline flags provider that returns +// a nested map[string]interface{} of environment variable where the +// nesting hierarchy of keys are defined by delim. For instance, the +// delim "." will convert the key `parent.child.key: 1` +// to `{parent: {child: {key: 1}}}`. +func Provider(f *cli.Context, delim string) *CliFlag { + return &CliFlag{ + ctx: f, + delim: delim, + } +} + +// ReadBytes is not supported by the cliflagv2 provider. +func (p *CliFlag) ReadBytes() ([]byte, error) { + return nil, errors.New("cliflagv2 provider does not support this method") +} + +// Read reads the flag variables and returns a nested conf map. +func (p *CliFlag) Read() (map[string]interface{}, error) { + out := make(map[string]interface{}) + + // Get command lineage (from root to current command) + lineage := p.ctx.Lineage() + if len(lineage) > 0 { + // Build command path and process flags for each level + var cmdPath []string + for i := len(lineage) - 1; i >= 0; i-- { + cmd := lineage[i] + if cmd.Command == nil { + continue + } + cmdPath = append(cmdPath, cmd.Command.Name) + prefix := strings.Join(cmdPath, p.delim) + p.processFlags(cmd.Command.Flags, prefix, out) + } + } + + if p.delim == "" { + return out, nil + } + + return maps.Unflatten(out, p.delim), nil +} + +func (p *CliFlag) processFlags(flags []cli.Flag, prefix string, out map[string]interface{}) { + for _, flag := range flags { + name := flag.Names()[0] + if p.ctx.IsSet(name) { + value := p.getFlagValue(name) + if value != nil { + // Build the full path for the flag + fullPath := name + if prefix != "global" { + fullPath = prefix + p.delim + name + } + + p.setNestedValue(fullPath, value, out) + } + } + } +} + +// setNestedValue sets a value in the nested configuration structure +func (p *CliFlag) setNestedValue(path string, value interface{}, out map[string]interface{}) { + parts := strings.Split(path, p.delim) + current := out + + // Navigate/create the nested structure + for i := 0; i < len(parts)-1; i++ { + if _, exists := current[parts[i]]; !exists { + current[parts[i]] = make(map[string]interface{}) + } + current = current[parts[i]].(map[string]interface{}) + } + + // Set the final value + current[parts[len(parts)-1]] = value +} + +// getFlagValue extracts the typed value from the flag. +func (p *CliFlag) getFlagValue(name string) interface{} { + if !p.ctx.IsSet(name) { + return nil + } + + // Find the flag definition + flag := p.findFlag(name) + if flag == nil { + return nil + } + + // Use type switch to get the appropriate value + switch flag.(type) { + case *cli.StringFlag: + return p.ctx.String(name) + case *cli.StringSliceFlag: + return p.ctx.StringSlice(name) + case *cli.IntFlag: + return p.ctx.Int(name) + case *cli.Int64Flag: + return p.ctx.Int64(name) + case *cli.IntSliceFlag: + return p.ctx.IntSlice(name) + case *cli.Float64Flag: + return p.ctx.Float64(name) + case *cli.Float64SliceFlag: + return p.ctx.Float64Slice(name) + case *cli.BoolFlag: + return p.ctx.Bool(name) + case *cli.DurationFlag: + return p.ctx.Duration(name) + case *cli.TimestampFlag: + return p.ctx.Timestamp(name) + case *cli.PathFlag: + return p.ctx.Path(name) + default: + return p.ctx.Generic(name) + } +} + +// findFlag looks up a flag by name in both global and command-specific flags +func (p *CliFlag) findFlag(name string) cli.Flag { + // Check global flags + for _, f := range p.ctx.App.Flags { + for _, n := range f.Names() { + if n == name { + return f + } + } + } + + // Check command-specific flags if we're in a command + if p.ctx.Command != nil { + for _, f := range p.ctx.Command.Flags { + for _, n := range f.Names() { + if n == name { + return f + } + } + } + } + + return nil +} diff --git a/providers/cliflagv2/cliflagv2_test.go b/providers/cliflagv2/cliflagv2_test.go new file mode 100644 index 00000000..b4282d0b --- /dev/null +++ b/providers/cliflagv2/cliflagv2_test.go @@ -0,0 +1,79 @@ +package cliflagv2 + +import ( + "fmt" + "os" + "testing" + + "github.com/knadh/koanf/v2" + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v2" +) + +func TestCliFlag(t *testing.T) { + cliApp := cli.App{ + Name: "testing", + Action: func(ctx *cli.Context) error { + p := Provider(ctx, ".") + x, err := p.Read() + require.NoError(t, err) + require.NotEmpty(t, x) + + fmt.Printf("x: %v\n", x) + + k := koanf.New(".") + err = k.Load(p, nil) + + fmt.Printf("k.All(): %v\n", k.All()) + + return nil + }, + Flags: []cli.Flag{ + cli.HelpFlag, + cli.VersionFlag, + &cli.StringFlag{ + Name: "test", + Usage: "test flag", + Value: "test", + Aliases: []string{"t"}, + EnvVars: []string{"TEST_FLAG"}, + }, + }, + Commands: []*cli.Command{ + { + Name: "x", + Description: "yeah yeah testing", + Action: func(ctx *cli.Context) error { + p := Provider(ctx, ".") + x, err := p.Read() + require.NoError(t, err) + require.NotEmpty(t, x) + fmt.Printf("x: %s\n", x) + + k := koanf.New(".") + err = k.Load(p, nil) + + fmt.Printf("k.All(): %v\n", k.All()) + + require.Equal(t, k.String("testing.x.lol"), "dsf") + return nil + }, + Flags: []cli.Flag{ + cli.HelpFlag, + cli.VersionFlag, + &cli.StringFlag{ + Name: "lol", + Usage: "test flag", + Value: "test", + Required: true, + EnvVars: []string{"TEST_FLAG"}, + }, + }, + }, + }, + } + + x := append([]string{"testing", "--test", "gf", "x", "--lol", "dsf"}, os.Args...) + err := cliApp.Run(append(x, os.Environ()...)) + require.NoError(t, err) +} diff --git a/providers/cliflagv2/go.mod b/providers/cliflagv2/go.mod new file mode 100644 index 00000000..280aaf6b --- /dev/null +++ b/providers/cliflagv2/go.mod @@ -0,0 +1,20 @@ +module github.com/knadh/koanf/providers/cliflagv2 + +go 1.18 + +require ( + github.com/knadh/koanf/maps v0.1.1 + github.com/stretchr/testify v1.9.0 + github.com/urfave/cli/v2 v2.27.5 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/providers/cliflagv2/go.sum b/providers/cliflagv2/go.sum new file mode 100644 index 00000000..afdc2733 --- /dev/null +++ b/providers/cliflagv2/go.sum @@ -0,0 +1,24 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=