peterbourgon / ff

Flags-first package for configuration

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Allow use of alternative FlagSet implementations

AdamSLevy opened this issue · comments

Alternative FlagSet implementations such as https://github.com/spf13/pflag allow for different styles of flags.

If ff and ffcli used a minimal interface with only the functions they call, in place of flag.FlagSet, this would allow users to use the FlagSet implementation of their choosing.

This is a non-goal according to the README.

No promises, but I would be interested to see what a PR looked like.

I played with this a bit but found that the use of calls to flag.Flag is limiting. This is tricky to work around and I have a feeling you wouldn't want it in your repo.

The cleanest way to do this in Go is to use an adapter type that implements the same functions with an interface{} instead of *flag.Flag.

Something like

type flagSetAdapter {
         std *flag.FlagSet
         psx *pflag.FlagSet
}

func newFlagSetAdapter(flg interface{}) (flagSetAdapter, error) {
        switch flg := flg.(type) {
        case *flag.FlagSet:
                return flagSetAdapter{flg,nil}
// etc...
        }
}

This gets tricky when it comes to implementing an adapter method for Visit(fn func(*flag.Flag) and the like. In this case ff needs to implement two identical versions of any fn passed to Visit: one using flag.Flag and one using pflag.Flag. That also limits this to only being compatible with the specific flag libraries that it has fn implementations for. So we're basically up against the wall w.r.t. Go's typing system.

I think a minimal set of used FlagSet methods are as follows:

type FlagSet interface {
	Parse([]string) error
	Visit(fn func(name string))
	VisitAll(fn func(name string))
	Set(name string, value string) error
	Lookup(name string) (value string, ok bool)
}

Those can be easily implemented for both flag and pflag. For example:

type stdFS struct {
	base *flag.FlagSet
}

func (fs stdFS) Parse(arguments []string) error {
	return fs.base.Parse(arguments)
}

func (fs stdFS) Set(name, value string) error {
	return fs.base.Set(name, value)
}

func (fs stdFS) Visit(fn func(name string)) {
	fs.base.Visit(func(f *stdflag.Flag) {
		fn(f.Name)
	})
}
func (fs stdFS) VisitAll(fn func(name string)) {
	fs.base.VisitAll(func(f *stdflag.Flag) {
		fn(f.Name)
	})
}

func (fs stdFS) Lookup(s string) (value string, ok bool) {
	f := fs.base.Lookup(s)
	if f != nil {
		return f.Value.String(), true
	}
	return "", false
}

If this seems reasonable, I can work on a PR.

I haven't looked into it deeply, but I also think it ought to be feasible. If you want to do a preliminary PR I'd be happy to give it a review. It would be important that the existing API, using the stdlib flag.FlagSet, not change. That is, if you want to change

func Parse(fs *flag.FlagSet, ...)

to

func Parse(fs SomeInterface, ...)

it would be important that the flag.FlagSet satisfy that interface without an adapter.

I have two different approaches and I am not sure which one is better so I sent two PRs.

One(#63) with a FlagSet interface that does not use flag.Flag, so we need to define adapter for both flag.FlagSet and pflag.FlagSet.

Another one(#62) defines FlagSet with the same function signatures as the flag.Flag so only pflag.FlagSet needs an adapter.

This is cool. I prefer #62, can you run with that?

Ok, I will work on #62. I need to ask one more question.

Since Lookup and Visit* functions provide *flag.Flag but we copy values from pFlag.Flag, so modifying flag returned from those functions will not have any effect. Is that ok? In the current implementation, we do only read f.Name and f.Value.String().

Do whatever you need to get a working prototype, then we can assess next steps if anything needs to change. I noticed pflag has some "FromGoFlag" adapters, maybe we could PR pflag to expose some of that if necessary.

Just FYI the "official" upstream pflag repo is notoriously slow at merging PRs or addressing issues.

Looking at this again, I'm reasonably optimistic that there exists a FlagSet abstraction which would be satisfied by the stdlib flag.FlagSet via an adapter, and also permit other implementations like pflag. Stay tuned...

Hi @peterbourgon,

I can take a stab at this feature if you'd be okay with that, but was curious what your thoughts were.

I see from prior work #64 was creating a new wrapper around the pflag library, but it was closed with the following comment

I may still do this, but by another approach.

I wanted to align directionally with what you were thinking before I attempted. Any high level direction of what you are looking for?

So the main thing is that this module must not depend on pflag — I'll happily look at anything that satisfies that constraint. I had some thoughts about how to do that, abstracting over the standard library and pflag flag sets with generalized interfaces or perhaps specialized types, but they didn't quite pan out.

Out of curiosity, why is the dependency on pflag such a strong objection? Without being able to do that I think that this feature given the current flags API would be prohibitively difficult without abusing some reflection.

ff simply shouldn't have a dependency relationship to pflag — or any other third-party package, really. It should ideally remain ignorant of anything outside of the stdlib. The current dependencies on the YAML and TOML packages I consider bugs that I'd like to try to fix in the future.

I hope this is addressed by #113.