Skip to content

Commit 15431e3

Browse files
n0cloudGabriel P.
andauthored
Add urfave/cli/v3 provider (#361)
--------- Co-authored-by: Gabriel P. <[email protected]>
1 parent 6e430e8 commit 15431e3

File tree

6 files changed

+257
-0
lines changed

6 files changed

+257
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,7 @@ Install with `go get -u github.com/knadh/koanf/providers/$provider`
673673
| consul/v2 | `consul.Provider(consul.Config{})` | Hashicorp Consul provider |
674674
| parameterstore/v2 | `parameterstore.Provider(parameterstore.Config{})` | AWS Systems Manager Parameter Store provider |
675675
| 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. |
676+
| cliflagv3 | `cliflagv3.Provider(ctx *cli.Context, delimiter string)` | Reads commands and flags from urfave/cli/v3 and provides a nested config map based on delim. |
676677

677678

678679
### Bundled Parsers

go.work

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use (
1818
./providers/azkeyvault
1919
./providers/basicflag
2020
./providers/cliflagv2
21+
./providers/cliflagv3
2122
./providers/confmap
2223
./providers/consul
2324
./providers/env

providers/cliflagv3/cliflagv3.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Package cliflagv3 implements a koanf.Provider that reads commandline
2+
// parameters as conf maps using urfave/cli/v3 flag.
3+
package cliflagv3
4+
5+
import (
6+
"errors"
7+
"slices"
8+
"strings"
9+
10+
"github.com/knadh/koanf/maps"
11+
"github.com/urfave/cli/v3"
12+
)
13+
14+
// CliFlag implements a cli.Flag command line provider.
15+
type CliFlag struct {
16+
cmd *cli.Command
17+
delim string
18+
config *Config
19+
}
20+
21+
type Config struct {
22+
Defaults []string
23+
}
24+
25+
// Provider returns a commandline flags provider that returns
26+
// a nested map[string]interface{} of environment variable where the
27+
// nesting hierarchy of keys are defined by delim. For instance, the
28+
// delim "." will convert the key `parent.child.key: 1`
29+
// to `{parent: {child: {key: 1}}}`.
30+
func Provider(f *cli.Command, delim string) *CliFlag {
31+
return &CliFlag{
32+
cmd: f,
33+
delim: delim,
34+
config: &Config{
35+
Defaults: []string{},
36+
},
37+
}
38+
}
39+
40+
// ProviderWithConfig returns a commandline flags provider with a
41+
// Configuration struct attached.
42+
func ProviderWithConfig(f *cli.Command, delim string, config *Config) *CliFlag {
43+
return &CliFlag{
44+
cmd: f,
45+
delim: delim,
46+
config: config,
47+
}
48+
}
49+
50+
// ReadBytes is not supported by the cliflagv3 provider.
51+
func (p *CliFlag) ReadBytes() ([]byte, error) {
52+
return nil, errors.New("cliflagv3 provider does not support this method")
53+
}
54+
55+
// Read reads the flag variables and returns a nested conf map.
56+
func (p *CliFlag) Read() (map[string]interface{}, error) {
57+
out := make(map[string]interface{})
58+
59+
// Get command lineage (from root to current command)
60+
lineage := p.cmd.Lineage()
61+
if len(lineage) > 0 {
62+
// Build command path and process flags for each level
63+
var cmdPath []string
64+
for i := len(lineage) - 1; i >= 0; i-- {
65+
cmd := lineage[i]
66+
cmdPath = append(cmdPath, cmd.Name)
67+
prefix := strings.Join(cmdPath, p.delim)
68+
p.processFlags(cmd.Flags, prefix, out)
69+
}
70+
}
71+
72+
if p.delim == "" {
73+
return out, nil
74+
}
75+
76+
return maps.Unflatten(out, p.delim), nil
77+
}
78+
79+
func (p *CliFlag) processFlags(flags []cli.Flag, prefix string, out map[string]interface{}) {
80+
for _, flag := range flags {
81+
name := flag.Names()[0]
82+
if p.cmd.IsSet(name) || slices.Contains(p.config.Defaults, name) {
83+
value := p.getFlagValue(name)
84+
if value != nil {
85+
// Build the full path for the flag
86+
fullPath := name
87+
if prefix != "global" {
88+
fullPath = prefix + p.delim + name
89+
}
90+
91+
p.setNestedValue(fullPath, value, out)
92+
}
93+
}
94+
}
95+
}
96+
97+
// setNestedValue sets a value in the nested configuration structure
98+
func (p *CliFlag) setNestedValue(path string, value interface{}, out map[string]interface{}) {
99+
parts := strings.Split(path, p.delim)
100+
current := out
101+
102+
// Navigate/create the nested structure
103+
for i := 0; i < len(parts)-1; i++ {
104+
if _, exists := current[parts[i]]; !exists {
105+
current[parts[i]] = make(map[string]interface{})
106+
}
107+
current = current[parts[i]].(map[string]interface{})
108+
}
109+
110+
// Set the final value
111+
current[parts[len(parts)-1]] = value
112+
}
113+
114+
// getFlagValue extracts the typed value from the flag.
115+
func (p *CliFlag) getFlagValue(name string) interface{} {
116+
// Find the flag definition
117+
flag := p.findFlag(name)
118+
if flag == nil {
119+
return nil
120+
}
121+
return flag.Get()
122+
}
123+
124+
// findFlag looks up a flag by name
125+
func (p *CliFlag) findFlag(name string) cli.Flag {
126+
// Check global flags
127+
for _, f := range p.cmd.Flags {
128+
if slices.Contains(f.Names(), name) {
129+
return f
130+
}
131+
}
132+
133+
return nil
134+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package cliflagv3
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"testing"
8+
9+
"github.com/knadh/koanf/v2"
10+
"github.com/stretchr/testify/require"
11+
"github.com/urfave/cli/v3"
12+
)
13+
14+
func TestCliFlag(t *testing.T) {
15+
cliApp := cli.Command{
16+
Name: "testing",
17+
Action: func(ctx context.Context, cmd *cli.Command) error {
18+
p := Provider(cmd, ".")
19+
x, err := p.Read()
20+
require.NoError(t, err)
21+
require.NotEmpty(t, x)
22+
23+
fmt.Printf("x: %v\n", x)
24+
25+
k := koanf.New(".")
26+
err = k.Load(p, nil)
27+
28+
fmt.Printf("k.All(): %v\n", k.All())
29+
30+
return nil
31+
},
32+
Flags: []cli.Flag{
33+
cli.HelpFlag,
34+
cli.VersionFlag,
35+
&cli.StringFlag{
36+
Name: "test",
37+
Usage: "test flag",
38+
Value: "test",
39+
Aliases: []string{"t"},
40+
Sources: cli.EnvVars("TEST_FLAG"),
41+
},
42+
},
43+
Commands: []*cli.Command{
44+
{
45+
Name: "x",
46+
Description: "yeah yeah testing",
47+
Action: func(ctx context.Context, cmd *cli.Command) error {
48+
p := Provider(cmd, ".")
49+
x, err := p.Read()
50+
require.NoError(t, err)
51+
require.NotEmpty(t, x)
52+
fmt.Printf("x: %s\n", x)
53+
54+
k := koanf.New(".")
55+
err = k.Load(p, nil)
56+
57+
fmt.Printf("k.All(): %v\n", k.All())
58+
59+
require.Equal(t, k.String("testing.x.lol"), "dsf")
60+
return nil
61+
},
62+
Flags: []cli.Flag{
63+
cli.HelpFlag,
64+
cli.VersionFlag,
65+
&cli.StringFlag{
66+
Name: "lol",
67+
Usage: "test flag",
68+
Value: "test",
69+
Required: true,
70+
Sources: cli.EnvVars("TEST_FLAG"),
71+
},
72+
},
73+
},
74+
},
75+
}
76+
77+
x := []string{"testing", "--test", "gf", "x", "--lol", "dsf"}
78+
err := cliApp.Run(context.Background(), append(x, os.Environ()...))
79+
require.NoError(t, err)
80+
}

providers/cliflagv3/go.mod

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module github.com/knadh/koanf/providers/cliflagv3
2+
3+
go 1.23.0
4+
5+
require (
6+
github.com/knadh/koanf/maps v0.1.2
7+
github.com/knadh/koanf/v2 v2.1.2
8+
github.com/stretchr/testify v1.10.0
9+
github.com/urfave/cli/v3 v3.3.3
10+
)
11+
12+
require (
13+
github.com/davecgh/go-spew v1.1.1 // indirect
14+
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
15+
github.com/mitchellh/copystructure v1.2.0 // indirect
16+
github.com/mitchellh/reflectwalk v1.0.2 // indirect
17+
github.com/pmezard/go-difflib v1.0.0 // indirect
18+
gopkg.in/yaml.v3 v3.0.1 // indirect
19+
)

providers/cliflagv3/go.sum

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
4+
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
5+
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
6+
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
7+
github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ=
8+
github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo=
9+
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
10+
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
11+
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
12+
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
13+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
14+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
15+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
16+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
17+
github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I=
18+
github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
19+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
20+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
21+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
22+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)