spf13 / pflag

Drop-in replacement for Go's flag package, implementing POSIX/GNU-style --flags.
BSD 3-Clause "New" or "Revised" License
2.41k stars 347 forks source link

Flag default assignments should be lazily applied #257

Open briandealwis opened 4 years ago

briandealwis commented 4 years ago

Re: https://github.com/GoogleContainerTools/skaffold/issues/4129, https://github.com/spf13/cobra/issues/1047

In Skaffold we use cobra and pflags to define our CLI. We have the flags toggle values directly in an options object. We have two commands that take the same flags but with different defaults. The following snippet demonstrates the issue:

var blowup bool

cmd1 := cobra.Command{}
cmd1.Flags().BoolVar(&blowup, "blowup", false, "When set to false, blowup")

cmd2 := cobra.Command{}
cmd2.Flags().BoolVar(&blowup, "blowup", true, "When set to false, blowup")

cmd1.ParseFlags(os.Args[1:])
fmt.Printf("blowup = %v\n", blowup)

So even though we have two FlagSets, because they reference the same value, the default value for the last flag defined "wins" unless the option is explicitly defined on the command-line.

The issue here is that pflags applies the default value immediately. BoolVar() just delegates BoolVarP(), and BoolVarP() uses newBoolValue():

https://github.com/spf13/pflag/blob/81378bbcd8a1005f72b1e8d7579e5dd7b2d612ab/bool.go#L47-L57

newBoolValue() allocates a container to map the provider pointer to the default value, but it immediately assigns the value:

https://github.com/spf13/pflag/blob/81378bbcd8a1005f72b1e8d7579e5dd7b2d612ab/bool.go#L15-L18

yufeiminds commented 4 years ago

I also need a lazily applied default value in my command-line tool.

Now I write default value as zero value and fill them at runtime, then write default to flag usage manually.

Is there any way to resolve it?

pasamio commented 3 years ago

I ran into this as well re-using an variable with two commands setting different defaults for the same variable as a convenience for the end user. I ended up isolating the variables and doing a small refactoring to make it work but it caught me off guard that a default value from a flag on a different command would be used based on essentially which one is configured last.

cornfeedhobo commented 3 years ago

I think this is user error. I would suggest using a lookup instead, and stop relying on passing a pointer around.

If someone can present some other use cases for lazy parsing, I'd maybe take a look at the issue on my fork.

pasamio commented 3 years ago

It could be classed as user error, I'm coming to this via Cobra that provided examples using this and I kept following that pattern. I think for someone running the code in two different commands via Cobra this provides a hard to understand and debug behaviour. It doesn't do what I expected and provides a surprise because the challenge is that this can influence the behaviour of another part of the program which is additionally confusing.

I ran into this twice before really realising what was going on. Using lookup instead is an alternative but the library provides a feature, I don't think it's an unreasonable expectation that when paired with Cobra that defaults are lazily applied when their commands are actually invoked. The current behaviour is in a sense hard to decipher because it isn't apparent that the two commands are interacting to change the default value which isn't entirely deterministic to the programmer at the time of writing. For people who know better they won't hit it however this is also a trap that at least a few people seem to have run into and I personally feel that in terms of being user friendly it doesn't seem wrong to defer setting the default value whilst removing a pitfall for a developer new to the library.

cornfeedhobo commented 3 years ago

@pasamio my point is that this is fundamental to golang, not an error with the package. You are passing around a pointer and expecting it to not be overwritten.

pasamio commented 3 years ago

I'm passing a pointer to a variable expecting that it will be filled with the value of my default argument for the flag that is currently being evaluated rather than it being filled with a value from a flag that is not currently being evaluated that is in another part of my Cobra application. I don't think it's an unreasonable expectation that when I hand the library a pointer to my variable that when my flags are evaluated that the variable is either populated with the value the user provided on the command line to the application or the default value I set for the flag in that command.

I think the challenge is the expectation between what pflag does and how that is used in Cobra.

outofforest commented 2 years ago

I digged into the source code of pflag to fix this but the implementation is so strange that solving this would require rewriting of entire library

outofforest commented 2 years ago

Assumption that value of pointer is overwritten immediately when flag is defined, not when Parse is called on FlagSet (here for example: https://github.com/spf13/pflag/blob/master/bool.go#L15) is really counter-intuitive. Pointer should be touched just before reading flags from CLI, not earlier.

I started working on fixing this by storing default value separately and applying it in Parse and ParseAll before reading values from CLI. But Value interface (https://github.com/spf13/pflag/blob/master/flag.go#L187) has no method to copy current value from one Value to another. I thought that doing sth like value.Set(defaultValue.String()) may work but due to reaaaaaalllyyy bad design of pflag library it doesn't work this way for all the types (built-in and custom ones). It would require adding new method Copy to all the types which would break compatibility.

I have to say that: During my research I found that pflag is soo poorly designed that it should be reimplemented completely. Take logic of this function as an example: https://github.com/spf13/pflag/blob/master/flag.go#L538