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

How to make it works for subcommands #7

Open
rgarrigue opened this issue Feb 28, 2023 · 3 comments
Open

How to make it works for subcommands #7

rgarrigue opened this issue Feb 28, 2023 · 3 comments

Comments

@rgarrigue
Copy link

Hi there

Long story short, I started from cobra-cli boilerplate, and taking snippets of this repository I got a root command working.

But my issue now is that subcommands don't take env / config file in account, just flags. After trials & errors, seems I would need to add ViperCfg.ReadInConfig() in all the subcommands' init. Not really elegant, I guess I'm missing something as PersistentPreRun should propagate the viper config reading. But well, beginner's struggles.

So wondering what I'm missing here / how would you deal with subcommands in your setup ?

Here's my root.go

package cmd

import (
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
	"github.com/spf13/viper"
)

var logLevel string

var rootCmd = &cobra.Command{
	Use:   "tool",
	Short: "tool is a tool.",
	Long:  `tool allow you to .`,
	PersistentPreRun: func(cmd *cobra.Command, args []string) {

		if strings.ToLower(logFormat) == "text" {
			log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).
				With().
				Timestamp().
				Caller().
				Logger()
		}

		switch strings.ToLower(logLevel) {
		case "trace":
			zerolog.SetGlobalLevel(zerolog.TraceLevel)
		}
	},
}

func Execute() {
	err := rootCmd.Execute()
	if err != nil {
		os.Exit(1)
	}
}

func init() {
	cobra.OnInitialize(initializeConfig)

	rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "Configuration file (default /etc/tool/config.yaml > $HOME/.tool/config.yaml > ./.config.yaml)")
	rootCmd.PersistentFlags().StringVarP(&logLevel, "loglevel", "l", "INFO", "Log level. From the less verbose to the most, panic, fatal, error, warn, info, debug, trace")
}

var viperCfg viper.Viper

func initializeConfig() {
	viperCfg = *viper.New()

	if configFile != "" {
		viperCfg.SetConfigFile(configFile)
	} else {
		viperCfg.SetConfigName("config")

		home, _ := os.UserHomeDir()
		viperCfg.AddConfigPath(".")
		viperCfg.AddConfigPath(home + ".config/tool")
		viperCfg.AddConfigPath("/etc/tool")
	}

	if err := viperCfg.ReadInConfig(); err != nil {
		if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
			log.Fatal().Msgf("Couldn't parse configuration file %s", viperCfg.ConfigFileUsed())
		}
	}

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

	bindFlags()
}

func bindFlags() {
	rootCmd.Flags().VisitAll(func(f *pflag.Flag) {
		configName := f.Name
		
		if !f.Changed && viperCfg.IsSet(configName) {
			val := viperCfg.Get(configName)
			rootCmd.Flags().Set(f.Name, fmt.Sprintf("%v", val))
		}
	})
}

And the shortened generate.go

package cmd

import (	
	"os"
	"strings"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
)

var valuesFile string

var generateCmd = &cobra.Command{
	Use:   "generate",
	Short: "Generate",
	Long: `Create the
	`,
	Run: generate,
}

func init() {
	rootCmd.AddCommand(generateCmd)

	generateCmd.Flags().StringVarP(&valuesFile, "values", "f", "./values.yaml", "Path of the")

        // insert ViperCfg.ReadInConfig() to get the config file settings taken in account
}

func generate(cmd *cobra.Command, args []string) {
	
	_, errVal := os.Stat(valuesFiles)
	if os.IsNotExist(errVal) {
		log.Fatal().Msgf("Values file '%s' doesn't exist", valuesFiles)
	}

	// ...
}
@rgarrigue
Copy link
Author

@carolynvs sorry for bothering you, but back on the topic and still stuck, so any help would be welcomed 😖

@carolynvs
Copy link
Owner

Hey @rgarrigue I can't provide support / troubleshooting for cobra. But I can point you towards my codebase, https://github.com/getporter/porter/blob/main/cmd/porter/main.go and https://github.com/getporter/porter/blob/main/pkg/cli/config.go (helpers for cobra) which works with subcommands and everything outlined in stingoftheviper.

Hopefully that helps give you an idea of what's different that is causing your subcommands to not work! 👍

@Inveracity
Copy link

This is how I ended up solving it

package main

import (
	"fmt"
	"os"
	"strings"

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

type App struct {
}

func main() {
	cli := New()
	cli.HelloCli()
}

func New() *App {
	return &App{}
}

func (c *App) HelloCli() {

	rootCmd := &cobra.Command{
		Use:   "do",
		Short: "do things",
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			return InitializeConfig(cmd)
		},
		Run: func(cmd *cobra.Command, args []string) {
			cmd.Help()
		},
	}

	hello := rootCmd.PersistentFlags().String("hello", "name", "your name")
	rootCmd.AddCommand((&DoStuff{}).Hello(hello))
	err := rootCmd.Execute()
	if err != nil {
		os.Exit(1)
	}
}

type DoStuff struct {
}

func (c *DoStuff) Hello(hello *string) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "hello",
		Short: "say hello",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Printf("hello %s\n", *hello)
		},
	}

	return cmd
}

func InitializeConfig(cmd *cobra.Command) error {
	v := viper.New()
	v.SetEnvPrefix("MYCLI")
	v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
	v.AutomaticEnv()
	bindFlags(cmd, v)
	return nil
}

// Bind each cobra flag to its associated viper configuration environment variable
func bindFlags(cmd *cobra.Command, v *viper.Viper) {
	cmd.Flags().VisitAll(func(f *pflag.Flag) {
		configName := f.Name
		if !f.Changed && v.IsSet(configName) {
			val := v.Get(configName)
			cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val))
		}
	})
}
MYCLI_HELLO=bob go run main.go hello
# hello bob
go run main.go hello --hello=world
# hello world

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

3 participants