go-micro.dev/v5@v5.12.0/config/source/cli/cli.go (about) 1 package cli 2 3 import ( 4 "flag" 5 "io" 6 "os" 7 "strings" 8 "time" 9 10 "dario.cat/mergo" 11 "github.com/urfave/cli/v2" 12 "go-micro.dev/v5/cmd" 13 "go-micro.dev/v5/config/source" 14 ) 15 16 type cliSource struct { 17 opts source.Options 18 ctx *cli.Context 19 } 20 21 func (c *cliSource) Read() (*source.ChangeSet, error) { 22 var changes map[string]interface{} 23 24 // directly using app cli flags, to access default values of not specified options 25 for _, f := range c.ctx.App.Flags { 26 name := f.Names()[0] 27 tmp := toEntry(name, c.ctx.Generic(name)) 28 if err := mergo.Map(&changes, tmp, mergo.WithOverride); err != nil { 29 return nil, err 30 } 31 } 32 33 b, err := c.opts.Encoder.Encode(changes) 34 if err != nil { 35 return nil, err 36 } 37 38 cs := &source.ChangeSet{ 39 Format: c.opts.Encoder.String(), 40 Data: b, 41 Timestamp: time.Now(), 42 Source: c.String(), 43 } 44 cs.Checksum = cs.Sum() 45 46 return cs, nil 47 } 48 49 func toEntry(name string, v interface{}) map[string]interface{} { 50 n := strings.ToLower(name) 51 keys := strings.FieldsFunc(n, split) 52 reverse(keys) 53 tmp := make(map[string]interface{}) 54 for i, k := range keys { 55 if i == 0 { 56 tmp[k] = v 57 continue 58 } 59 60 tmp = map[string]interface{}{k: tmp} 61 } 62 return tmp 63 } 64 65 func reverse(ss []string) { 66 for i := len(ss)/2 - 1; i >= 0; i-- { 67 opp := len(ss) - 1 - i 68 ss[i], ss[opp] = ss[opp], ss[i] 69 } 70 } 71 72 func split(r rune) bool { 73 return r == '-' || r == '_' 74 } 75 76 func (c *cliSource) Watch() (source.Watcher, error) { 77 return source.NewNoopWatcher() 78 } 79 80 // Write is unsupported. 81 func (c *cliSource) Write(cs *source.ChangeSet) error { 82 return nil 83 } 84 85 func (c *cliSource) String() string { 86 return "cli" 87 } 88 89 // NewSource returns a config source for integrating parsed flags from a urfave/cli.Context. 90 // Hyphens are delimiters for nesting, and all keys are lowercased. The assumption is that 91 // command line flags have already been parsed. 92 // 93 // Example: 94 // 95 // cli.StringFlag{Name: "db-host"}, 96 // 97 // 98 // { 99 // "database": { 100 // "host": "localhost" 101 // } 102 // } 103 func NewSource(opts ...source.Option) source.Source { 104 options := source.NewOptions(opts...) 105 106 var ctx *cli.Context 107 108 if c, ok := options.Context.Value(contextKey{}).(*cli.Context); ok { 109 ctx = c 110 } else { 111 // no context 112 // get the default app/flags 113 app := cmd.App() 114 flags := app.Flags 115 116 // create flagset 117 set := flag.NewFlagSet(app.Name, flag.ContinueOnError) 118 119 // apply flags to set 120 for _, f := range flags { 121 f.Apply(set) 122 } 123 124 // parse flags 125 set.SetOutput(io.Discard) 126 set.Parse(os.Args[1:]) 127 128 // normalise flags 129 normalizeFlags(app.Flags, set) 130 131 // create context 132 ctx = cli.NewContext(app, set, nil) 133 } 134 135 return &cliSource{ 136 ctx: ctx, 137 opts: options, 138 } 139 } 140 141 // WithContext returns a new source with the context specified. 142 // The assumption is that Context is retrieved within an app.Action function. 143 func WithContext(ctx *cli.Context, opts ...source.Option) source.Source { 144 return &cliSource{ 145 ctx: ctx, 146 opts: source.NewOptions(opts...), 147 } 148 }