Skip to content

Commit

Permalink
Merge pull request #6 from jamiealquiza/jamie/cobra
Browse files Browse the repository at this point in the history
jamie/cobra
  • Loading branch information
jamiealquiza authored Nov 19, 2018
2 parents 51efe00 + 8cadb96 commit 8296e64
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 15 deletions.
92 changes: 86 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -36,20 +39,91 @@ 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:**

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`
Expand Down Expand Up @@ -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.
87 changes: 87 additions & 0 deletions cobra.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
26 changes: 17 additions & 9 deletions main.go → envy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)
}
}

Expand Down

0 comments on commit 8296e64

Please # to comment.