diff --git a/README.md b/README.md index ff6e5bb..4b94c0d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ +[![GoDoc](https://godoc.org/github.com/jamiealquiza/envy?status.svg)](https://godoc.org/github.com/jamiealquiza/envy) + + # envy -Automatically exposes environment variables for all of your flags. +Automatically exposes environment variables for all of your flags. It supports the standard flags package along with limited support for [Cobra](https://github.com/spf13/cobra) commands. -Envy takes one parameter: a namespace prefix that will be used for environment variable lookups. Each flag registered in your app will be prefixed, uppercased, and hyphens exchanged for underscores; if a matching environment variable is found, it will set the respective flag value as long as the value is not otherwise explicitly set (see usage for precedence). +Envy takes a namespace prefix that will be used for environment variable lookups. Each flag registered in your app will be prefixed, uppercased, and hyphens exchanged for underscores; if a matching environment variable is found, it will set the respective flag value as long as the value is not otherwise explicitly set (see usage for precedence). -### Example +### Example: flag Code: ```go @@ -21,7 +24,7 @@ func main() { var address = flag.String("address", "127.0.0.1", "Some random address") var port = flag.String("port", "8131", "Some random port") - envy.Parse("MYAPP") // looks for MYAPP_ADDRESS & MYAPP_PORT + envy.Parse("MYAPP") // Expose environment variables. flag.Parse() fmt.Println(*address) @@ -36,12 +39,83 @@ Output: 127.0.0.1 8131 -# Flag defaults overridden +# Setting flags via env vars. % MYAPP_ADDRESS="0.0.0.0" MYAPP_PORT="9080" ./example 0.0.0.0 9080 ``` +### Example: Cobra + +Code: +```go + +// Where to execute envy depends on the structure +// of your Cobra implementation. A common pattern +// is to define a root command and an 'Execute' function +// that's called from the application main. We can call +// envy ParseCobra here and configure it to recursively +// update all child commands. Alternatively, it can be +// scoped to child commands at some point in their +// initialization. + +var rootCmd = &cobra.Command{ + Use: "myapp", +} + +func Execute() { + // Configure envy. + cfg := CobraConfig{ + // The env var prefix. + Prefix: "MYAPP", + // Whether to parse persistent flags. + Persistent: true, + // Whether to recursively update child command FlagSets. + Recursive: true, + } + + // Apply. + envy.ParseCobra(rootCmd, cfg) + + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} +``` + +Output: +```sh +# Root command flags. +% myapp +Usage: + myapp [command] + +Available Commands: + help Help about any command + doathing This is a subcommand + +Flags: + -h, --help help for myapp + --some-config A global config [MYAPP_SOME_CONFIG] + +Use "myapp [command] --help" for more information about a command. + +# Child command flags. Notice that the prefix +# has the subcommand name automatically appended +# while preserving global/parent env vars. +% myapp doathing +Usage: + myapp doathing [flags] + +Flags: + -h, --help help for myapp + --subcmd-config Another config [MYAPP_DOATHING_SUBCMD_CONFIG] + +Global Flags: + --some-flag A global flag [MYAPP_SOME_FLAG] +``` + ### Usage **Variable precedence:** @@ -49,7 +123,7 @@ Output: Envy results in the following order of precedence, each item overwriting the previous: `flag default` -> `Envy generated env var` -> `flag set at the CLI`. -Results referencing the example code: +Results referencing the stdlib flag example code: - `./example` will result in `port` being set to `8131` - `MYAPP_PORT=5678 ./example` will result in `port` being set to `5678` - `MYAPP_PORT=5678 ./example -port=1234` will result in `port` being set to `1234` @@ -94,3 +168,9 @@ Environment variables should be defined using a type that satisfies the respecti **Side effects:** Setting a flag through an Envy generated environment variable will have the same effects on the default `flag.CommandLine` as if the flag were set via the command line. This only affect users that may rely on `flag.CommandLine` methods that make distinctions between set and to-be set flags (such as the `Visit` method). + +**Cobra compatibility:** + +The extensive types in Cobra's underlying [pflag](https://github.com/spf13/pflag) have not been tested, hence the "limited support" reference. + +Also, keep in mind that Cobra can change in a way that breaks support with envy. Functionality was tested as of 2018-11-19. diff --git a/cobra.go b/cobra.go new file mode 100644 index 0000000..d20bb11 --- /dev/null +++ b/cobra.go @@ -0,0 +1,87 @@ +package envy + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// CobraConfig holds configurations for calls +// to ParseCobra. +type CobraConfig struct { + // The environment variable prefix. + Prefix string + // Expose flags for child commands. + Recursive bool + // Whether to expose flags for persistent FlagSets. + Persistent bool +} + +// ParseCobra takes a *cobra.Command and exposes environment variables +// for all local flags in the command FlagSet in the form of PREFIX_FLAGNAME +// where PREFIX is set to that of CobraConfig.Prefix. Environment variables are +// exposed for persistent flags if the CobraConfig.Persistent is set to true. +// If CobraConfig.Recursive is set to true, all child command FlagSets will +// have environment variables exposed in the form of PREFIX_SUBCOMMMAND_FLAGNAME. +func ParseCobra(c *cobra.Command, cfg CobraConfig) { + // Check if this is the root command. + switch c.Root() == c { + case false: + // If not, append subcommand names to the prefix. + cfg.Prefix = fmt.Sprintf("%s_%s", cfg.Prefix, strings.ToUpper(c.Name())) + case true && cfg.Persistent: + // If this is the root command, update the + // persistent FlagSet, if configured. + updateCobra(cfg.Prefix, c.PersistentFlags()) + } + + // Update the current command local FlagSet. + updateCobra(cfg.Prefix, c.Flags()) + + // Recursively update child commands. + if cfg.Recursive { + for _, child := range c.Commands() { + if child.Name() == "help" { + continue + } + + ParseCobra(child, cfg) + } + } +} + +func updateCobra(p string, fs *pflag.FlagSet) { + // Build a map of explicitly set flags. + set := map[string]interface{}{} + fs.Visit(func(f *pflag.Flag) { + set[f.Name] = nil + }) + + fs.VisitAll(func(f *pflag.Flag) { + if f.Name == "help" { + return + } + + // Create an env var name + // based on the supplied prefix. + envVar := fmt.Sprintf("%s_%s", p, strings.ToUpper(f.Name)) + envVar = strings.Replace(envVar, "-", "_", -1) + + // Update the Flag.Value if the + // env var is non "". + if val := os.Getenv(envVar); val != "" { + // Update the value if it hasn't + // already been set. + if _, defined := set[f.Name]; !defined { + fs.Set(f.Name, val) + } + } + + // Append the env var to the + // Flag.Usage field. + f.Usage = fmt.Sprintf("%s [%s]", f.Usage, envVar) + }) +} diff --git a/main.go b/envy.go similarity index 50% rename from main.go rename to envy.go index 929879f..2f44213 100644 --- a/main.go +++ b/envy.go @@ -9,17 +9,25 @@ import ( "strings" ) -// Parse takes a string p that is used -// as the environment variable prefix -// for each flag configured. +// Parse takes a prefix string and exposes environment variables +// for all flags in the default FlagSet (flag.CommandLine) in the +// form of PREFIX_FLAGNAME. func Parse(p string) { + update(p, flag.CommandLine) +} + +// update takes a prefix string p and *flag.FlagSet. Each flag +// in the FlagSet is exposed as an upper case environment variable +// prefixed with p. Any flag that was not explicitly set by a user +// is updated to the environment variable, if set. +func update(p string, fs *flag.FlagSet) { // Build a map of explicitly set flags. - set := map[string]bool{} - flag.CommandLine.Visit(func(f *flag.Flag) { - set[f.Name] = true + set := map[string]interface{}{} + fs.Visit(func(f *flag.Flag) { + set[f.Name] = nil }) - flag.CommandLine.VisitAll(func(f *flag.Flag) { + fs.VisitAll(func(f *flag.Flag) { // Create an env var name // based on the supplied prefix. envVar := fmt.Sprintf("%s_%s", p, strings.ToUpper(f.Name)) @@ -30,8 +38,8 @@ func Parse(p string) { if val := os.Getenv(envVar); val != "" { // Update the value if it hasn't // already been set. - if defined := set[f.Name]; !defined { - flag.CommandLine.Set(f.Name, val) + if _, defined := set[f.Name]; !defined { + fs.Set(f.Name, val) } }