From 7f2e98d70185079d7d9af737488aa528aa4bd3eb Mon Sep 17 00:00:00 2001 From: Joice Joseph Date: Thu, 17 Oct 2024 12:52:56 +0530 Subject: [PATCH] adds support for urfave/cli/v2 --- providers/cliflag/cliflag.go | 118 ++++++++++++++++++++++++++++++ providers/cliflag/cliflag_test.go | 65 ++++++++++++++++ providers/cliflag/go.mod | 20 +++++ providers/cliflag/go.sum | 24 ++++++ 4 files changed, 227 insertions(+) create mode 100644 providers/cliflag/cliflag.go create mode 100644 providers/cliflag/cliflag_test.go create mode 100644 providers/cliflag/go.mod create mode 100644 providers/cliflag/go.sum diff --git a/providers/cliflag/cliflag.go b/providers/cliflag/cliflag.go new file mode 100644 index 00000000..ebed821c --- /dev/null +++ b/providers/cliflag/cliflag.go @@ -0,0 +1,118 @@ +// Package cliflag implements a koanf.Provider that reads commandline +// parameters as conf maps using ufafe/cli flag. +package cliflag + +import ( + "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, + } +} + +// 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{} { + switch { + case p.ctx.IsSet(name): + switch { + case p.ctx.String(name) != "": + return p.ctx.String(name) + case p.ctx.StringSlice(name) != nil: + return p.ctx.StringSlice(name) + case p.ctx.Int(name) != 0: + return p.ctx.Int(name) + case p.ctx.Int64(name) != 0: + return p.ctx.Int64(name) + case p.ctx.IntSlice(name) != nil: + return p.ctx.IntSlice(name) + case p.ctx.Float64(name) != 0: + return p.ctx.Float64(name) + case p.ctx.Bool(name): + return p.ctx.Bool(name) + case p.ctx.Duration(name).String() != "0s": + return p.ctx.Duration(name) + default: + return p.ctx.Generic(name) + } + } + return nil +} diff --git a/providers/cliflag/cliflag_test.go b/providers/cliflag/cliflag_test.go new file mode 100644 index 00000000..4a0f1a40 --- /dev/null +++ b/providers/cliflag/cliflag_test.go @@ -0,0 +1,65 @@ +package cliflag + +import ( + "fmt" + "os" + "testing" + + "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) + + 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) + 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/cliflag/go.mod b/providers/cliflag/go.mod new file mode 100644 index 00000000..2c1f5164 --- /dev/null +++ b/providers/cliflag/go.mod @@ -0,0 +1,20 @@ +module github.com/knadh/koanf/v2/providers/cliflag + +go 1.21.5 + +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/cliflag/go.sum b/providers/cliflag/go.sum new file mode 100644 index 00000000..afdc2733 --- /dev/null +++ b/providers/cliflag/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=