Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ func main() {
Names: []string{"bootloader"},
Description: "Use the specified bootloader (grub, grub2, or lilo)",
Decoder: writ.NewOptionDecoder(&config.bootloader),
Default: writ.StringDefault("grub2"),
Placeholder: "NAME",
})
platform := cmd.GroupOptions("bootloader")
Expand Down
50 changes: 33 additions & 17 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ func (p Path) findOption(name string) *Option {
return nil
}

// options returns a slice of all options on the command path
func (p Path) options() []*Option {
var options []*Option
for _, command := range p {
options = append(options, command.Options...)
}
return options
}

// New reads the input spec, searching for fields tagged with "option",
// "flag", or "command". The field type and tags are used to construct
// a corresponding Command instance, which can be used to decode program
Expand Down Expand Up @@ -136,7 +145,6 @@ func (c *Command) String() string {
// parameters.
func (c *Command) Decode(args []string) (path Path, positional []string, err error) {
c.validate()
c.setDefaults()
return parseArgs(c, args)
}

Expand Down Expand Up @@ -287,18 +295,6 @@ func (c *Command) validate() {
}
}

func (c *Command) setDefaults() {
for _, opt := range c.Options {
defaulter, ok := opt.Decoder.(OptionDefaulter)
if ok {
defaulter.SetDefault()
}
}
for _, sub := range c.Subcommands {
sub.setDefaults()
}
}

/*
* Argument parsing
*/
Expand Down Expand Up @@ -349,6 +345,22 @@ func parseArgs(c *Command, args []string) (path Path, positional []string, err e
parseCmd = false
positional = append(positional, a)
}

// Set defaults for unspecified options
for _, opt := range path.options() {
_, present := seen[opt]
if !present && opt.Default != nil {
val := opt.Default.Default()
if val == "" {
continue
}
err = opt.Decoder.Decode(val)
if err != nil {
err = optionError{err: fmt.Errorf("Option %s initialized with invalid default value %q", opt, val)}
return
}
}
}
return
}

Expand Down Expand Up @@ -598,13 +610,17 @@ func parseOptionField(field reflect.StructField, fieldVal reflect.Value) *Option
opt.Decoder = NewOptionDecoder(fieldVal.Addr().Interface())
}

var chain ChainedDefault
envName := field.Tag.Get(envTag)
if envName != "" {
chain = append(chain, NewEnvDefault(envName))
}
defaultArg := field.Tag.Get(defaultTag)
if defaultArg != "" {
opt.Decoder = NewDefaulter(opt.Decoder, defaultArg)
chain = append(chain, StringDefault(defaultArg))
}
envName := field.Tag.Get(envTag)
if envName != "" {
opt.Decoder = NewEnvDefaulter(opt.Decoder, envName)
if len(chain) > 0 {
opt.Default = chain
}

opt.validate()
Expand Down
22 changes: 6 additions & 16 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,17 +347,17 @@ var defaultFieldTests = []defaultFieldTest{
// Field with an environment default
{Args: []string{""}, Valid: true, Field: "EnvDefault", Value: 0},
{Args: []string{""}, Valid: true, EnvKey: "ENV_DEFAULT", EnvValue: "2", Field: "EnvDefault", Value: 2},
{Args: []string{""}, Valid: true, EnvKey: "ENV_DEFAULT", EnvValue: "foo", Field: "EnvDefault", Value: 0},
{Args: []string{"-e", "4"}, Valid: true, EnvKey: "ENV_DEFAULT", EnvValue: "2", Field: "EnvDefault", Value: 4},
{Args: []string{"-e", "4"}, Valid: true, EnvKey: "ENV_DEFAULT", EnvValue: "foo", Field: "EnvDefault", Value: 4},
{Args: []string{""}, Valid: false, EnvKey: "ENV_DEFAULT", EnvValue: "foo"},
{Args: []string{"-e", "foo"}, Valid: false, EnvKey: "ENV_DEFAULT", EnvValue: "2"},

// Field with both a default value and an environment default
{Args: []string{""}, Valid: true, Field: "StackedDefault", Value: 84},
{Args: []string{""}, Valid: true, EnvKey: "STACKED_DEFAULT", EnvValue: "2", Field: "StackedDefault", Value: 2},
{Args: []string{""}, Valid: true, EnvKey: "STACKED_DEFAULT", EnvValue: "foo", Field: "StackedDefault", Value: 84},
{Args: []string{"-s", "4"}, Valid: true, EnvKey: "STACKED_DEFAULT", EnvValue: "2", Field: "StackedDefault", Value: 4},
{Args: []string{"-s", "4"}, Valid: true, EnvKey: "STACKED_DEFAULT", EnvValue: "foo", Field: "StackedDefault", Value: 4},
{Args: []string{""}, Valid: false, EnvKey: "STACKED_DEFAULT", EnvValue: "foo"},
{Args: []string{"-s", "foo"}, Valid: false, EnvKey: "STACKED_DEFAULT", EnvValue: "foo"},
{Args: []string{"-s", "foo"}, Valid: false},
}
Expand Down Expand Up @@ -405,21 +405,11 @@ func TestBogusDefaultField(t *testing.T) {
BogusDefault int `option:"b" description:"An int field with a bogus default" default:"bogus"`
}{}

defer func() {
r := recover()
if r != nil {
switch r.(type) {
case commandError, optionError:
// Intentional No-op
default:
panic(r)
}
}
}()

cmd := New("test", spec)
cmd.Decode([]string{})
t.Errorf("Expected decoding to panic on bogus default value, but this didn't happen.")
_, _, err := cmd.Decode([]string{})
if err == nil {
t.Errorf("Expected decoding error on bogus default value, but this didn't happen.")
}
}

/*
Expand Down
6 changes: 2 additions & 4 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,7 @@ The New() function recognizes the following combinations of field tags:
- description: the description to display for help output

If both "default" and "env" are specified for an option field, the environment
variable is consulted first. If the environment variable is present and
decodes without error, that value is used. Otherwise, the value for the
"default" tag is used. Values specified via parsed arguments take precedence
over both types of defaults.
variable is consulted first. Values specified via parsed arguments take
precedence over both types of defaults.
*/
package writ
1 change: 1 addition & 0 deletions example_explicit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func Example_explicit() {
Names: []string{"bootloader"},
Description: "Use the specified bootloader (grub, grub2, or lilo)",
Decoder: writ.NewOptionDecoder(&config.bootloader),
Default: writ.StringDefault("grub2"),
Placeholder: "NAME",
})
platform := cmd.GroupOptions("bootloader")
Expand Down
78 changes: 33 additions & 45 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ type Option struct {
Decoder OptionDecoder

// Optional
Flag bool // If set, the Option takes no arguments
Plural bool // If set, the Option may be specified multiple times
Description string // Options without descriptions are hidden
Placeholder string // Displayed next to option in help output (e.g. FILE)
Default Defaulter // The Default value is used when no explicit value is provided
Flag bool // If set, the Option takes no arguments
Plural bool // If set, the Option may be specified multiple times
Description string // Options without descriptions are hidden
Placeholder string // Displayed next to option in help output (e.g. FILE)
}

// ShortNames returns a filtered slice of the names that are exactly one rune in length.
Expand Down Expand Up @@ -340,58 +341,45 @@ type flagAccumulator struct {
value *int
}

// OptionDefaulter initializes option values to defaults. If an OptionDecoder
// implements the OptionDefaulter interface, its SetDefault() method is called
// prior to decoding options.
type OptionDefaulter interface {
SetDefault()
// Defaulter returns a default value for an Option.
type Defaulter interface {
Default() string
}

// NewDefaulter builds an OptionDecoder that implements OptionDefaulter.
// SetDefault calls decoder.Decode() with the value of defaultArg. If the
// value fails to decode, SetDefault panics.
func NewDefaulter(decoder OptionDecoder, defaultArg string) OptionDecoder {
return defaulter{decoder, defaultArg}
}

type defaulter struct {
OptionDecoder
defaultArg string
}
// StringDefault returns its value when Default() is called. This makes it easy to
// pass string values as defaults for the Option.Default field.
type StringDefault string

func (d defaulter) SetDefault() {
err := d.Decode(d.defaultArg)
if err != nil {
// Default values should be known correct values, so we panic on error
panicOption("error setting default value: decoder rejected arg %q", d.defaultArg)
}
// Default returns the string value of d
func (d StringDefault) Default() string {
return string(d)
}

// NewEnvDefaulter builds an OptionDecoder that implements OptionDefaulter.
// SetDefault calls decoder.Decode() with the value of the environment
// variable named by key. If the environment variable isn't set or fails to
// decode, SetDefault checks if decoder implements OptionDefault. If so,
// SetDefault calls decoder.SetDefault(). Otherwise, no action is taken.
func NewEnvDefaulter(decoder OptionDecoder, key string) OptionDecoder {
return envDefaulter{decoder, key}
// NewEnvDefault builds a Defaulter that returns the value of the
// environment variable named by key when it's Default() method is called.
func NewEnvDefault(key string) Defaulter {
return envDefaulter{key}
}

type envDefaulter struct {
OptionDecoder
key string
}

func (d envDefaulter) SetDefault() {
val := os.Getenv(d.key)
if val != "" {
err := d.Decode(val)
if err == nil {
return
}
}
func (d envDefaulter) Default() string {
return os.Getenv(d.key)
}

defaulter, ok := d.OptionDecoder.(OptionDefaulter)
if ok {
defaulter.SetDefault()
// ChainedDefault checks the Default() value of each element in it's slice
// and returns the first non-empty value, or "" if all values are empty.
type ChainedDefault []Defaulter

// Default returns the first non-empty value for the elements in d, or "".
func (d ChainedDefault) Default() string {
for _, defaulter := range d {
value := defaulter.Default()
if value != "" {
return value
}
}
return ""
}