viper: Question: Cobra required flags don't work with Viper automatic env vars

Hi,

I already mentioned this here: https://github.com/spf13/cobra/pull/502 and @dixudx properly pointed that environment variables were out of the scope of Cobra.

However, we are using AutomaticEnv and we have a CLI that is using flags that can be set with environment variables. We did a hacky function shown in the previously mentioned issue to support required flags, however, after upgrading cobra, pflag et al we are getting an error because the required flags are not being set. That’s not fully true because we were setting them with environment variables.

So, my question is, are we using Cobra and Viper right and this is a bug that should be fixed or are we doing something wrong? In case it’s a bug, could you point me out how to help to fix and where it should be fixed? I suppose that here, in viper.

Thanks!

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 19
  • Comments: 15

Commits related to this issue

Most upvoted comments

Hey all, it’s 2020 now. Do we now have a proper fix for this?

I use the following snippet to use the required flag functionallity with viper environment support:

func init() {
	cobra.OnInitialize(func() {
		viper.AutomaticEnv()
		postInitCommands(root.Commands())
	})
}

func postInitCommands(commands []*cobra.Command) {
	for _, cmd := range commands {
		presetRequiredFlags(cmd)
		if cmd.HasSubCommands() {
			postInitCommands(cmd.Commands())
		}
	}
}

func presetRequiredFlags(cmd *cobra.Command) {
	viper.BindPFlags(cmd.Flags())
	cmd.Flags().VisitAll(func(f *pflag.Flag) {
		if viper.IsSet(f.Name) && viper.GetString(f.Name) != "" {
			cmd.Flags().Set(f.Name, viper.GetString(f.Name))
		}
	})
}

2022

Lame but I did the following, by forcing the validation through the Config struct. I can use env or flags to define the var and use the validator to mark required options.

import (
        "fmt"
        "os"

        "github.com/go-playground/validator/v10"
        "github.com/spf13/cobra"
        "github.com/spf13/viper"
)


type Config struct {
        ZkListenAddress string `mapstructure:"zk_listen_address"`
        ZkTimeout       string `mapstructure:"zk_timeout"`
        FwdTopics       string `mapstructure:"fwd_topics" validate:"required"`
}

func init() {
        rootCmd.AddCommand(serveCmd)

        serveCmd.PersistentFlags().StringP("zk-listen-address", "", "localhost:2181", "Address on which to acces Zookeeper.")
        serveCmd.PersistentFlags().StringP("zk-timeout", "", "5", "The longest to wait for a Zookeeper connection.")
        serveCmd.PersistentFlags().StringP("fwd-topics", "", "", "Space delimited list of topics to collect metrics on.")

        viper.BindPFlag("zk_listen_address", serveCmd.PersistentFlags().Lookup("zk-listen-address"))
        viper.BindPFlag("zk_timeout", serveCmd.PersistentFlags().Lookup("zk-timeout"))
        viper.BindPFlag("fwd_topics", serveCmd.PersistentFlags().Lookup("fwd-topics"))

        var c Config
        if err := viper.Unmarshal(&c); err != nil {
                fmt.Printf("Error unmarshalling config file: %s", err)
                os.Exit(1)
        }

        validate := validator.New()
        if err := validate.Struct(&c); err != nil {
                fmt.Printf("Missing required attributes: %v\n", err)
                os.Exit(1)
        }
}

I might be missing something obvious here but couldn’t you just mark the flag as not required if it’s set by viper?

cmd.Flags().VisitAll(func(f *pflag.Flag) {
	if viper.IsSet(f.Name) {
		cmd.Flags().SetAnnotation(f.Name, cobra.BashCompOneRequiredFlag, []string{"false"})
	}
})

This seems way easier than trying to set the values on the flags. Especially in the case where the flag is a slice for example.

Any updates?

@trobert2 I switched to kingpin, which I found simpler and doesn’t have this issue.

I agree with @agonzalezro - the current behaviour is confusing. Flags may be set via environment variables, so required flags should count as “present” if the environment variable is present.

This is difficult to work around cleanly because Viper.IsSet returns true even if the flag is only set via a default value.

in root.go:

func initConfig() {
    ...
    viper.SetEnvPrefix("PREFIX")
    ...
}

// CheckRequiredFlags exits with error when one ore more required flags are not set
func CheckRequiredFlags(prefixKey string, requiredFlags []string, cmd *cobra.Command) {
	unsetFlags := make([]string, 0, len(requiredFlags))
	for _, f := range requiredFlags {
		if !viper.GetViper().IsSet(prefixKey + f) {
			unsetFlags = append(unsetFlags, f)
		}
	}
	if len(unsetFlags) > 0 {
		fmt.Fprintln(os.Stderr, "Error: required flags are not set:")
		for _, f := range unsetFlags {
			fmt.Fprintf(os.Stderr, "  --%s\n", f)
		}
		fmt.Fprintf(os.Stderr, "\n")
		cmd.Usage()
		os.Exit(1)
	}
}

in subcommand.go

package cmd

import (
	"fmt"

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

var requiredSubcommandFlags = []string{"flag1", "flag2"}

var subcommandCmd = &cobra.Command{
	Use:   "subcommand",
	Short: "Subcommand description here",
	Long:  ``,
	Run: func(cmd *cobra.Command, args []string) {
		CheckRequiredFlags(cmd.Use+".", requiredSubcommandFlags, cmd)
		for _, f := range requiredSubcommandFlags {
			fmt.Printf("%s=%s\n", f, viper.GetViper().GetString(cmd.Use+"."+f))
		}
	},
}

func init() {
	rootCmd.AddCommand(subcommandCmd)

	subcommandCmd.Flags().StringP(requiredSubcommandFlags[0], "a", "", "[Required] Flag1")
	viper.BindPFlag(subcommandCmd.Use+"."+requiredSubcommandFlags[0], subcommandCmd.Flags().Lookup(requiredSubcommandFlags[0]))
	subcommandCmd.Flags().StringP(requiredSubcommandFlags[1], "b", "", "[Required] Flag2")
	viper.BindPFlag(subcommandCmd.Use+"."+requiredSubcommandFlags[1], subcommandCmd.Flags().Lookup(requiredSubcommandFlags[1]))
	subcommandCmd.Flags().StringP("Flag3", "c", "", "Flag3")
	viper.BindPFlag(subcommandCmd.Use+".Flag3", subcommandCmd.Flags().Lookup("Flag3"))
}
$> go run main.go subcommand
Error: required flags are not set:
  --flag1
  --flag2

Usage:
  myprogram subcommand [flags]

Flags:
  -c, --Flag3 string   Flag3
  -a, --flag1 string   [Required] Flag1
  -b, --flag2 string   [Required] Flag2
  -h, --help           help for subcommand


$> PREFIX_SUBCOMMAND_FLAG1=foo go run main.go subcommand
Error: required flags are not set:
  --flag2

Usage:
  myprogram subcommand [flags]

Flags:
  -c, --Flag3 string   Flag3
  -a, --flag1 string   [Required] Flag1
  -b, --flag2 string   [Required] Flag2
  -h, --help           help for subcommand


$> PREFIX_SUBCOMMAND_FLAG1=foo PREFIX_SUBCOMMAND_FLAG2=bar go run main.go subcommand
flag1=foo
flag2=bar

If like me, you expected Viper to be bi-directional with Cobra (setting an Viper environment variable would bind to Cobra and it’s variable) the following might be of some use. It’s based on both simonklb and v-braun’s snippets. It only works for the root commands (not children) as in my case it’s just the global settings I’m likely to provide via environment variables. I’ve left the SetAnnotation in there commented out for reference, as I found the rootCmd.Flags().Set() function did what I wanted.

func initConfig() {

	if cfgFile != "" {
		// Use config file from the flag.
		viper.SetConfigFile(cfgFile)
	} else {
		// Find home directory.
		home, err := os.UserHomeDir()
		cobra.CheckErr(err)

		// Search config in home directory with name ".cobra" (without extension).
		viper.AddConfigPath(home)
		viper.SetConfigType("yaml")
		viper.SetConfigName(".cobra")
	}

	if err := viper.ReadInConfig(); err == nil {
		fmt.Println("Using config file:", viper.ConfigFileUsed())
	}

	viper.AutomaticEnv()

	rootCmd.Flags().VisitAll(func(f *pflag.Flag) {
		if viper.IsSet(f.Name) {
			//rootCmd.Flags().SetAnnotation(f.Name, cobra.BashCompOneRequiredFlag, []string{"false"})
			rootCmd.Flags().Set(f.Name, viper.GetString(f.Name))
		}
	})

}