peterbourgon / ff

Flags-first package for configuration

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Not sure how to properly to share config file between root flag and subcommands.

thomasf opened this issue · comments

I want something like this to work: mycmd -config configfile subcommand -subarg ... where one config file has values for both root and any subcommands.

Having one config file per subcommand with it's own -config argument is too messy and user unfriendly to consider, the one root level -config should be enough and the config file should preferably not have to be reread multiple times.

I'm not sure how to express this in a clean way using ffcli.

Maybe it's not even currently possible without hacking it in in some ugly way since it would have to run code between parsing the root flags and the subcommand flags?

maybe an API along the lines of this or something similar to connect the same config file flag provides configuration to multiple commands via a shared instance of some kind?

	configFileFlagOption, configProvider := ff.NewConfigFileFlagProvider("config")

	server := &ffcli.Command{
		Name: "server",
		Exec: func(ctx context.Context, args []string) error {
			return runServer(ctx, rootFlags, serverFlags, args)
		},
		FlagSet: serverFS,
		Options: []ff.Option{
			ff.WithEnvVarPrefix("FOO"),
			ff.WithConfig(configProvider),
			ff.WithIgnoreUndefined(true),
		},
	}

	root := &ffcli.Command{
		Exec: func(ctx context.Context, args []string) error {
			return flag.ErrHelp
		},
		Options: []ff.Option{
			ff.WithEnvVarPrefix("FOO"),
			configFileFlagOption,
			ff.WithConfigFileParser(ff.PlainParser),
			ff.WithAllowMissingConfigFile(true),
			ff.WithIgnoreUndefined(true),
		},
		FlagSet:     rootFS,
		Subcommands: []*ffcli.Command{server},
	}

If it's possible maybe the only thing needed is a WithInheritConfigFile(true) that can be used on subcommands to inherit config from parents or something simpler if it's possible to construct without having to create too many new API's.

Any hint towards what an acceptable API could look like would be nice, then I can start implementing it and create a pull request.

package main

import (
	"context"
	"errors"
	"flag"
	"log"
	"os"

	"github.com/peterbourgon/ff/v3"
	"github.com/peterbourgon/ff/v3/ffcli"
)

func main() {
	log.SetFlags(0)

	var (
		configFile string
		verbose    bool
	)
	registerGlobals := func(fs *flag.FlagSet) {
		fs.StringVar(&configFile, "config-file", "", "config-file (optional)")
		fs.BoolVar(&verbose, "v", false, "verbose output")
	}

	rootFS := flag.NewFlagSet("foo", flag.ContinueOnError)
	alpha := rootFS.Int("alpha", 1, "an integer defined with the root command")
	registerGlobals(rootFS)

	serverFS := flag.NewFlagSet("foo server", flag.ContinueOnError)
	beta := serverFS.Int("beta", 2, "an integer associated with the server subcommand")
	registerGlobals(serverFS)

	options := []ff.Option{
		ff.WithEnvVarPrefix("FOO"),
		ff.WithConfigFileParser(ff.PlainParser),
		ff.WithConfigFileFlag("config-file"),
		ff.WithIgnoreUndefined(true),
	}

	exec := func(ctx context.Context, args []string) error {
		log.Printf("Exec: args=%v configFile=%q alpha=%d beta=%d", args, configFile, *alpha, *beta)
		return nil
	}

	server := &ffcli.Command{
		Name:      "server",
		ShortHelp: "Run server component",
		FlagSet:   serverFS,
		Options:   options,
		Exec:      exec,
	}

	root := &ffcli.Command{
		Name:        "foo",
		ShortUsage:  "A demo program",
		FlagSet:     rootFS,
		Options:     options,
		Subcommands: []*ffcli.Command{server},
		Exec:        exec,
	}

	err := root.ParseAndRun(context.Background(), os.Args[1:])
	switch {
	case err == nil:
		return
	case errors.Is(err, flag.ErrHelp):
		return
	default:
		log.Fatalf("Error: %v", err)
	}
}
$ cat my.conf
alpha 123
beta 456
$ cat other.conf
alpha 1000
beta 2000
$ ./configdemo
Exec: args=[] configFile="" alpha=1 beta=2
$ ./configdemo -config-file my.conf server
Exec: args=[] configFile="my.conf" alpha=123 beta=456
$ ./configdemo server -config-file my.conf
Exec: args=[] configFile="my.conf" alpha=1 beta=456
$ ./configdemo -config-file my.conf server -config-file other.conf
Exec: args=[] configFile="other.conf" alpha=123 beta=2000

yeah, I specifically don't want ./configdemo -config-file my.conf server -config-file other.conf to be supported because it is weird and not user friendly (I'm not sure I have ever seen a cli this uses that pattern) . Only the root -config-file flag should exist for all subcommands.

Well, typically you want flags that affect more than 1 subcommand to be specifiable from anywhere in the command line. Verbose is a good example: it would be annoying if fooctl -v admin instance run worked but fooctl admin instance run -v didn't, for example.

But, that's fine. I think you just need something like func WithConfigFileVia(*string) Option which would defers resolution of the config file name until the Parse phase, rather than capturing it immediately when the option is invoked. Make sense?

But, that's fine. I think you just need something like func WithConfigFileVia(*string) Option which would defers resolution of the config file name until the Parse phase, rather than capturing it immediately when the option is invoked. Make sense?

Yes, that sound about right.