Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds urfave/cli/v2 flag parser #330

Merged
merged 5 commits into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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=
Loading