-
Notifications
You must be signed in to change notification settings - Fork 153
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. v3 will help to remove this custom context and rely on context.Context instead. https://github.com/urfave/cli/tree/main/docs/v3 migration: https://github.com/urfave/cli/blob/main/docs/migrate-v2-to-v3.md#clicontext-has-been-removed It also makes a much saner API for all the methods below. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
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) | ||
} | ||
Comment on lines
+44
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the user should provide flag to key mappings. I have looked into urfave/cli to see if there is a way to indicate which flags will be used as config and which aren't, but there isn't a way for this. So maybe there is a way to let user of Koanf the ability to say which flags are or aren't to be handled. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As per my understanding, you are suggesting to have a filter while reading the flags. I was referring how other flags are parsed. Here, with posflag, there are ways to modify given keys. Also, none of other koanf flag providers have an option to filter flags. My personal preference is also not to filter while reading the flags. filtering can be achieved using ko := koanf.New(.)
_ = ko.Load(cliflag.Provider(ctx, "."), nil)
_ = ko.Unmarshal("app", &cfg)
// or
someStr := ko.MustString("app.some_string") Please share your thoughts |
||
} | ||
|
||
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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
) |
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= |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
v3 is on the verge of being released:
It may well be worth it to either version this provider behind /v2 or jump to /v3 right away.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As v1, v2 are actively maintained (and widely used), and v3 is about to release, it will be really nice to have support for all the versions urfave/cli versions natively on koanf. But, this PR adds support only for
urfave/cli/v2
.I am still confused how different versions of the same
provider
(here,cliflag
) can be structured and maintained. Then I saw your feature request #333. I have added my comments/thoughts in that issue. Please help me how to structure different versions ofurfave/cli
inside koanf.