From 2e6825862420516bbf8fda33fc794a71f8b648b1 Mon Sep 17 00:00:00 2001 From: Hedzr Yeh Date: Sat, 26 Oct 2024 09:19:14 +0800 Subject: [PATCH] cmdr: added auto-env-vars-bindings feature --- cli/baseopt.go | 17 +++++++++++++++++ cli/commandmatch.go | 1 + cli/config.go | 2 ++ cli/worker/pre.go | 43 ++++++++++++++++++++++++++++++------------- cmdr.go | 35 +++++++++++++++++++++++++++++++++++ tiny/main.go | 2 ++ 6 files changed, 87 insertions(+), 13 deletions(-) diff --git a/cli/baseopt.go b/cli/baseopt.go index faa56f9..f0d5473 100644 --- a/cli/baseopt.go +++ b/cli/baseopt.go @@ -174,6 +174,23 @@ func (c *BaseOpt) GetCommandTitles() string { return backtraceCmdNamesG(c, " ", false) } +func (c *BaseOpt) GetAutoEnvVarName(prefix string, upperCase ...bool) string { + t := backtraceCmdNamesG(c, "_", false) + // last := c.Name() + u := false + for _, b := range upperCase { + u = b + } + if u { + t = strings.ToUpper(t) + // last = strings.ToUpper(last) + } + if prefix != "" { + return prefix + "_" + t // + "_" + last + } + return t // + "_" + last +} + // GetTitleName returns name/full/short string func (c *BaseOpt) GetTitleName() string { if c.name != "" { diff --git a/cli/commandmatch.go b/cli/commandmatch.go index ee6dc0b..b6d6772 100644 --- a/cli/commandmatch.go +++ b/cli/commandmatch.go @@ -320,6 +320,7 @@ func (c *CmdS) matchedForTG(ctx context.Context, ff *Flag) *Flag { } } } + // mutual exclusives if len(ff.mutualExclusives) > 0 { root := ff.Root() diff --git a/cli/config.go b/cli/config.go index c610141..9dfc43d 100644 --- a/cli/config.go +++ b/cli/config.go @@ -41,6 +41,8 @@ type Config struct { DebugScreenWriter HelpWriter `json:"debug_screen_writer,omitempty"` // redirect stdout for debugging outputs Args []string `json:"args,omitempty"` // for testing Env map[string]string `json:"env,omitempty"` // inject env var & values + AutoEnv bool `json:"auto_env,omitempty"` // enable envvars auto-binding? + AutoEnvPrefix string `json:"auto_env_prefix,omitempty"` // envvars auto-binding prefix } // Opt for cmdr system diff --git a/cli/worker/pre.go b/cli/worker/pre.go index 756e568..7e846a0 100644 --- a/cli/worker/pre.go +++ b/cli/worker/pre.go @@ -167,25 +167,42 @@ func (w *workerS) commandsToStoreR(ctx context.Context, root *cli.RootCommand, c } return } - if cx, ok := w.root.Cmd.(*cli.CmdS); ok { - conf.WithinLoading(func() { - cx.WalkEverything(ctx, func(cc, pp cli.Cmd, ff *cli.Flag, cmdIndex, flgIndex, level int) { //nolint:revive - if ff != nil { - if evs := ff.EnvVars(); len(evs) > 0 { - for _, ev := range evs { - if v, has := os.LookupEnv(ev); has { - data := fromString(v, ff.DefaultValue()) - ff.SetDefaultValue(data) - } + if w.Config.AutoEnvPrefix == "" { + w.Config.AutoEnvPrefix = "APP" + } + worker := func(cx cli.Cmd) { + // lookup all flags... + // and bind the value with its envvars field; + // also bind the auto-binding env vars; + cx.WalkEverything(ctx, func(cc, pp cli.Cmd, ff *cli.Flag, cmdIndex, flgIndex, level int) { //nolint:revive + if ff != nil { + if evs := ff.EnvVars(); len(evs) > 0 { + for _, ev := range evs { + if v, has := os.LookupEnv(ev); has { + data := fromString(v, ff.DefaultValue()) + ff.SetDefaultValue(data) } } - if conf != nil { - conf.Set(ff.GetDottedPath(), ff.DefaultValue()) + } + if w.Config.AutoEnv { + ev := ff.GetAutoEnvVarName(w.Config.AutoEnvPrefix, true) + if v, has := os.LookupEnv(ev); has { + data := fromString(v, ff.DefaultValue()) + ff.SetDefaultValue(data) } } - }) + if conf != nil { + conf.Set(ff.GetDottedPath(), ff.DefaultValue()) + } + } }) } + if cx, ok := w.root.Cmd.(*cli.CmdS); ok && cx != nil && conf != nil { + // using store.WithinLoading to disable onSet callbacks and reset internal modified states. + conf.WithinLoading(func() { worker(cx) }) + } else if cx := w.root.Cmd; cx != nil { + worker(cx) + } return } diff --git a/cmdr.go b/cmdr.go index cea5c14..585a49a 100644 --- a/cmdr.go +++ b/cmdr.go @@ -313,6 +313,41 @@ func WithDontExecuteAction(b bool) cli.Opt { } } +// WithAutoEnvBindings enables the feature which can auto-bind env-vars +// to flags default value. +// +// For example, APP_JUMP_TO_FULL=1 -> Cmd{"jump.to"}.Flag{"full"}.default-value = true. +// In this case, `APP` is default prefix so the env-var is different +// than other normal OS env-vars (like HOME, etc.). +// +// You may specify another prefix optionally. For instance, prefix +// "CT" will cause CT_JUMP_TO_FULL=1 binding to +// Cmd{"jump.to"}.Flag{"full"}.default-value = true. +// +// Also you can specify the prefix with multiple section just +// like "CT_ACCOUNT_SERVICE", it will be treated as a normal +// plain string and concatted with the rest parts, so +// "CT_ACCOUNT_SERVICE_JUMP_TO_FULL=1" will be bound in. +func WithAutoEnvBindings(b bool, prefix ...string) cli.Opt { + return func(s *cli.Config) { + s.AutoEnv = b + for _, p := range prefix { + s.AutoEnvPrefix = p + } + } +} + +// WithConfig allows you passing a [*cli.Config] object directly. +func WithConfig(conf *cli.Config) cli.Opt { + return func(s *cli.Config) { + if conf == nil { + *s = cli.Config{} + } else { + *s = *conf + } + } +} + // func WithOnInterpretLeadingPlusSign(cb func()) cli.Opt { // return func(s *cli.Config) { // s.onInterpretLeadingPlusSign = cb diff --git a/tiny/main.go b/tiny/main.go index cdc7a99..75ff3a9 100644 --- a/tiny/main.go +++ b/tiny/main.go @@ -44,6 +44,8 @@ func main() { cmdr.WithSortInHelpScreen(true), // default it's false cmdr.WithDontGroupInHelpScreen(false), // default it's false + + cmdr.WithAutoEnvBindings(true), ) // // simple run the parser of app and trigger the matched command's action