-
Notifications
You must be signed in to change notification settings - Fork 4
Fix overwrite by SetDefault for options that share Value #23
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
Changes from 4 commits
5276b64
e053fda
457cbbe
ad8fe14
ee63bee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ import ( | |
"bytes" | ||
"encoding/json" | ||
"os" | ||
"slices" | ||
"strings" | ||
|
||
"github.com/hashicorp/go-multierror" | ||
|
@@ -21,6 +22,14 @@ const ( | |
ValueSourceDefault ValueSource = "default" | ||
) | ||
|
||
var valueSourcePriority = []ValueSource{ | ||
ValueSourceFlag, | ||
ValueSourceEnv, | ||
ValueSourceYAML, | ||
ValueSourceDefault, | ||
ValueSourceNone, | ||
} | ||
|
||
// Option is a configuration option for a CLI application. | ||
type Option struct { | ||
Name string `json:"name,omitempty"` | ||
|
@@ -305,16 +314,12 @@ func (optSet *OptionSet) SetDefaults() error { | |
|
||
var merr *multierror.Error | ||
|
||
for i, opt := range *optSet { | ||
// Skip values that may have already been set by the user. | ||
if opt.ValueSource != ValueSourceNone { | ||
continue | ||
} | ||
Comment on lines
-310
to
-312
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Review: This was removed in this refactor because we have a different way of handling values that have been set by the user. However, this breaks one test in coder/coder: https://github.com/coder/coder/actions/runs/11782339194/job/32817210417?pr=15476#step:7:621 I could add a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't spent time to try and figure this out. This felt vaguely familiar, so I looked up when it was added. It was added in this commit: coder/coder@7edea99 And this PR: coder/coder#6934 So it has to relate with YAML support somehow? I was hoping the commit when it was added would have more context. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for looking and sharing, that was very helpful! It lead me to this: |
||
|
||
if opt.Default == "" { | ||
continue | ||
} | ||
|
||
// It's common to have multiple options with the same value to | ||
// handle deprecation. We group the options by value so that we | ||
// don't let other options overwrite user input. | ||
groupByValue := make(map[pflag.Value][]*Option) | ||
for i := range *optSet { | ||
opt := &(*optSet)[i] | ||
if opt.Value == nil { | ||
merr = multierror.Append( | ||
merr, | ||
|
@@ -325,13 +330,75 @@ func (optSet *OptionSet) SetDefaults() error { | |
) | ||
continue | ||
} | ||
(*optSet)[i].ValueSource = ValueSourceDefault | ||
if err := opt.Value.Set(opt.Default); err != nil { | ||
groupByValue[opt.Value] = append(groupByValue[opt.Value], opt) | ||
} | ||
|
||
sortOptionByValueSourcePriorityOrDefault := func(a, b *Option) int { | ||
if a.ValueSource != b.ValueSource { | ||
for _, vs := range valueSourcePriority { | ||
if a.ValueSource == vs { | ||
return -1 | ||
} | ||
if b.ValueSource == vs { | ||
return 1 | ||
} | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is totally fine, and an optimized solution. My morning brain just took a second to understand it. Can you just throw a comment like, I like using subtraction like this for compare functions.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's clean, thanks! 👍🏻 |
||
if a.Default != b.Default { | ||
if a.Default == "" { | ||
return 1 | ||
} | ||
if b.Default == "" { | ||
return -1 | ||
} | ||
} | ||
return 0 | ||
} | ||
for _, opts := range groupByValue { | ||
// Sort the options by priority and whether or not a default is | ||
// set. This won't affect the value but represents correctness | ||
// from whence the value originated. | ||
slices.SortFunc(opts, sortOptionByValueSourcePriorityOrDefault) | ||
|
||
// If the first option has a value source, then we don't need to | ||
// set the default, but mark the source for all options. | ||
if opts[0].ValueSource != ValueSourceNone { | ||
for _, opt := range opts[1:] { | ||
opt.ValueSource = opts[0].ValueSource | ||
} | ||
continue | ||
} | ||
|
||
var optWithDefault *Option | ||
for _, opt := range opts { | ||
if opt.Default == "" { | ||
continue | ||
} | ||
if optWithDefault != nil && optWithDefault.Default != opt.Default { | ||
merr = multierror.Append( | ||
merr, | ||
xerrors.Errorf( | ||
"parse %q: multiple defaults set for the same value: %q and %q (%q)", | ||
opt.Name, opt.Default, optWithDefault.Default, optWithDefault.Name, | ||
), | ||
) | ||
continue | ||
} | ||
optWithDefault = opt | ||
Comment on lines
+368
to
+381
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: this also looks like it could be extracted to its own function and tested independently There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see a benefit currently as this is pretty much "the meat" of this function and we don't need to call it elsewhere. |
||
} | ||
if optWithDefault == nil { | ||
continue | ||
} | ||
if err := optWithDefault.Value.Set(optWithDefault.Default); err != nil { | ||
merr = multierror.Append( | ||
merr, xerrors.Errorf("parse %q: %w", opt.Name, err), | ||
merr, xerrors.Errorf("parse %q: %w", optWithDefault.Name, err), | ||
) | ||
} | ||
for _, opt := range opts { | ||
opt.ValueSource = ValueSourceDefault | ||
} | ||
} | ||
|
||
return merr.ErrorOrNil() | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we try to handle cases where both are set? Currently it defaults to the last one set. Should we at least make this deterministic, like by alphabetical order?
I have not tested if 1 is set with an env var, the other the flag.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think alphabetical order is going to be intuitive. The current behaviour of "last one wins" makes more sense to me.
Alternatively, we could say that specifying a non-default value for both is an error? That would remove any ambiguity, but would be a breaking change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Emyrk it's typical behavior for CLI programs to use the last value of any given flag, for example:
So I would say the failing test-case is working as intended, just the assertion that should change.
I added some more test-cases to cover this and also the env/flag priority one in 457cbbe.
@johnstcn In this PR we already handle the case where flags specify multiple defaults, and that's OK as long as:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This made me curious. In the case of envs, it seems to be "first wins". I tested with the following test-cases.
I'm not sure if we need to do anything about that, but just keep in mind that this behavior has not changed in this PR.