Skip to content

Commit

Permalink
Add urfave/cli/v2 flag provider (#330)
Browse files Browse the repository at this point in the history
  • Loading branch information
joicemjoseph authored Jan 1, 2025
1 parent fa64438 commit 4a21f9b
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ use (
./providers/s3
./providers/structs
./providers/vault
./providers/cliflagv2
./tests
)
162 changes: 162 additions & 0 deletions providers/cliflagv2/cliflagv2.go
Original file line number Diff line number Diff line change
@@ -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
}
79 changes: 79 additions & 0 deletions providers/cliflagv2/cliflagv2_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
20 changes: 20 additions & 0 deletions providers/cliflagv2/go.mod
Original file line number Diff line number Diff line change
@@ -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
)
24 changes: 24 additions & 0 deletions providers/cliflagv2/go.sum
Original file line number Diff line number Diff line change
@@ -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=

0 comments on commit 4a21f9b

Please sign in to comment.