Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

viper and cobra sub commands #801

Open
lburgazzoli opened this issue Nov 27, 2019 · 3 comments
Open

viper and cobra sub commands #801

lburgazzoli opened this issue Nov 27, 2019 · 3 comments

Comments

@lburgazzoli
Copy link

lburgazzoli commented Nov 27, 2019

I'm trying to write a tool for which I would let user set defaults or custom CLI arguments through a configuration file or env vars. What I've done so far is something like:

package main

import (
	"errors"
	"log"
	"strings"

	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
	"github.com/spf13/viper"
)

func bindPFlagsHierarchy(cmd *cobra.Command) error {
	for _, c := range cmd.Commands() {
		if err := bindPFlags(c); err != nil {
			return err
		}

		if err := bindPFlagsHierarchy(c); err != nil {
			return err
		}
	}

	return nil
}

func bindPFlags(cmd *cobra.Command) error {
	prefix := cmd.Name()

	for current := cmd.Parent(); current != nil; current = current.Parent() {
		name := current.Name()
		name = strings.ReplaceAll(name, "_", "-")
		name = strings.ReplaceAll(name, ".", "-")
		prefix = name + "." + prefix
	}

	cmd.Flags().VisitAll(func(flag *pflag.Flag) {
		name := flag.Name
		name = strings.ReplaceAll(name, "_", "-")
		name = strings.ReplaceAll(name, ".", "-")

		if err := viper.BindPFlag(prefix+"."+name, flag); err != nil {
			log.Fatalf("error binding flag %s with prefix %s to viper", flag.Name, prefix)
		}
	})

	return nil
}

type MainOptions struct {
}

type SubOptions struct {
	Info string `mapstructure:"info"`
}

func main() {
	mopt := MainOptions{}
	sopt := SubOptions{}

	viper.AutomaticEnv()
	viper.SetEnvKeyReplacer(strings.NewReplacer("_", ".", "-", "."))

	mainCmd := &cobra.Command{
		Use:   "main",
		PreRunE: func(cmd *cobra.Command, args []string) error {
			if err := viper.UnmarshalKey("main", &mopt); err != nil {
				return nil
			}

			return nil
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			log.Printf("mopt: %+v", mopt)
			return nil
		},
	}

	subCmd1 := &cobra.Command{
		Use:   "sub1",
		PreRunE: func(cmd *cobra.Command, args []string) error {
			v := viper.Sub("main.sub1")
			if v == nil {
				return errors.New("no main.sub1")
			}

			if err := v.Unmarshal(&sopt); err != nil {
				return nil
			}

			return nil
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			log.Printf("sopt: %+v", sopt)
			return nil
		},
	}

	subCmd1.Flags().StringP("info", "i", "", "shows info")
	mainCmd.AddCommand(subCmd1)

	bindPFlagsHierarchy(mainCmd)

	mainCmd.Execute()
}

What the code does it to walk the command hierarchy and bind pflags to viper so as example the command

main sub1 --info=something

should be equivalent to

MAIN_SUB1_INFO=something main sub1

The problem I'm facing is that either viper.UnmarshalKey("main.sub1, &sopt) and viper.Sub("main.sub1").Unmarshal(&sopt) fail (note that the Sub variant panics as Sub returns nil)

@sagikazarmark
Copy link
Collaborator

Unmarshaling works a bit differently than viper.GetX. When you try to Get a key, it goes through the data loaded into Viper and tries to find a match, whereas Unmarshal grabs everything in Viper and tries to unmarshal that onto a structure.

The different behavior is caused by the fact that internally not everything is a map[string]interface{}. Flags in particular are stored in a flat map, so the different key parts are in a single key: main.sub1.info.

That's why sub does not return anything, and that's why unmarshaling doesn't work (neither way).

Usually the solution is to set a default value.

In your case you could do the following in your flag loop:

viper.SetDefault(prefix+"."+name, flag.DefValue)

Hope that helps.

@lburgazzoli
Copy link
Author

It partially solves the issue but it looks like that flags are not taken into account but only values from env vars or configuration files are set when using

viper.Sub(...).Unmarshal(...)

@sagikazarmark
Copy link
Collaborator

Yeah, Sub will probably not work with flags that way either.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants