Skip to content

Commit

Permalink
Add support for mandatory options as well as radio groups.
Browse files Browse the repository at this point in the history
  • Loading branch information
pborman committed Aug 26, 2020
1 parent fd0d075 commit 6173d3f
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 19 deletions.
74 changes: 73 additions & 1 deletion v2/getopt.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,34 @@
// Unless an option type explicitly prohibits it, an option may appear more than
// once in the arguments. The last value provided to the option is the value.
//
// MANDATORY OPTIONS
//
// An option marked as mandatory and not seen when parsing will cause an error
// to be reported such as: "program: --name is a mandatory option". An option
// is marked mandatory by using the Mandatory method:
//
// getopt.FlagLong(&fileName, "path", 0, "the path").Mandatory()
//
// Mandatory options have (required) appended to their help message:
//
// --path=value the path (required)
//
// MUTUALLY EXCLUSIVE OPTIONS
//
// Options can be marked as part of a mutually exclusive group. When two or
// more options in a mutually exclusive group are both seen while parsing then
// an error such as "program: options -a and -b are mutually exclusive" will be
// reported. Mutually exclusive groups are declared using the SetGroup method:
//
// getopt.Flag(&a, 'a', "use method A").SetGroup("method")
// getopt.Flag(&a, 'b', "use method B").SetGroup("method")
//
// A set can have multiple mutually exclusive groups. Mutually exclusive groups
// are identified with their group name in {}'s appeneded to their help message:
//
// -a use method A {method}
// -b use method B {method}
//
// BUILTIN TYPES
//
// The Flag and FlagLong functions support most standard Go types. For the
Expand Down Expand Up @@ -318,7 +346,7 @@ func (s *Set) PrintOptions(w io.Writer) {
for _, opt := range s.options {
if opt.uname != "" {
opt.help = strings.TrimSpace(opt.help)
if len(opt.help) == 0 {
if len(opt.help) == 0 && !opt.mandatory && opt.group == "" {
fmt.Fprintf(w, " %s\n", opt.uname)
continue
}
Expand Down Expand Up @@ -350,6 +378,12 @@ func (s *Set) PrintOptions(w io.Writer) {
if def != "" {
helpMsg += " [" + def + "]"
}
if opt.group != "" {
helpMsg += " {" + opt.group + "}"
}
if opt.mandatory {
helpMsg += " (required)"
}

help := strings.Split(helpMsg, "\n")
// If they did not put in newlines then we will insert
Expand Down Expand Up @@ -444,6 +478,12 @@ func (s *Set) Getopt(args []string, fn func(Option) bool) (err error) {
}
}
}()

defer func() {
if err == nil {
err = s.checkOptions()
}
}()
if fn == nil {
fn = func(Option) bool { return true }
}
Expand Down Expand Up @@ -562,3 +602,35 @@ Parsing:
s.args = []string{}
return nil
}

func (s *Set) checkOptions() error {
groups := map[string]Option{}
for _, opt := range s.options {
if !opt.Seen() {
if opt.mandatory {
return fmt.Errorf("option %s is mandatory", opt.Name())
}
continue
}
if opt.group == "" {
continue
}
if opt2 := groups[opt.group]; opt2 != nil {
return fmt.Errorf("options %s and %s are mutually exclusive", opt2.Name(), opt.Name())
}
groups[opt.group] = opt
}
for _, group := range s.requiredGroups {
if groups[group] != nil {
continue
}
var flags []string
for _, opt := range s.options {
if opt.group == group {
flags = append(flags, opt.Name())
}
}
return fmt.Errorf("exactly one of the following options must be specified: %s", strings.Join(flags, ", "))
}
return nil
}
82 changes: 82 additions & 0 deletions v2/getopt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2020 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package getopt

import (
"testing"
)

func TestMandatory(t *testing.T) {
for _, tt := range []struct {
name string
in []string
err string
}{
{
name: "required option present",
in: []string{"test", "-r"},
},
{
name: "required option not present",
in: []string{"test", "-o"},
err: "test: option -r is mandatory",
},
{
name: "no options",
in: []string{"test"},
err: "test: option -r is mandatory",
},
} {
reset()
var val bool
Flag(&val, 'o')
Flag(&val, 'r').Mandatory()
parse(tt.in)
if s := checkError(tt.err); s != "" {
t.Errorf("%s: %s", tt.name, s)
}
}
}

func TestGroup(t *testing.T) {
for _, tt := range []struct {
name string
in []string
err string
}{
{
name: "no args",
in: []string{"test"},
err: "test: exactly one of the following options must be specified: -A, -B",
},
{
name: "one of each",
in: []string{"test", "-A", "-C"},
},
{
name: "Two in group One",
in: []string{"test", "-A", "-B"},
err: "test: options -A and -B are mutually exclusive",
},
{
name: "Two in group Two",
in: []string{"test", "-A", "-D", "-C"},
err: "test: options -C and -D are mutually exclusive",
},
} {
reset()
var val bool
Flag(&val, 'o')
Flag(&val, 'A').SetGroup("One")
Flag(&val, 'B').SetGroup("One")
Flag(&val, 'C').SetGroup("Two")
Flag(&val, 'D').SetGroup("Two")
RequiredGroup("One")
parse(tt.in)
if s := checkError(tt.err); s != "" {
t.Errorf("%s: %s", tt.name, s)
}
}
}
10 changes: 10 additions & 0 deletions v2/help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,16 @@ func TestHelpDefaults(t *testing.T) {
var fvo flagValue = true
set.FlagLong(&fvo, "vbool_on", 0, "value bool").SetFlag()

required := 17
set.FlagLong(&required, "required", 0, "a required option").Mandatory()

var a, b bool
set.Flag(&a, 'a', "use method A").SetGroup("method")
set.Flag(&b, 'b', "use method B").SetGroup("method")

want := `
-a use method A {method}
-b use method B {method}
--duration=value duration value
--duration_set=value set duration value [1s]
-f, --bool_false false bool value
Expand All @@ -120,6 +129,7 @@ func TestHelpDefaults(t *testing.T) {
--int8=value int8 value
--int8_set=value set int8 value [8]
--int_set=value set int value [1]
--required=value a required option [17] (required)
--string=value string value
--string_set=value set string value [string]
-t, --bool_true true bool value [true]
Expand Down
48 changes: 30 additions & 18 deletions v2/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,31 @@ type Option interface {
// yet been seen, including resetting the value of the option
// to its original default state.
Reset()

// Mandataory sets the mandatory flag of the option. Parse will
// fail if a mandatory option is missing.
Mandatory() Option

// SetGroup sets the option as part of a radio group. Parse will
// fail if two options in the same group are seen.
SetGroup(string) Option
}

type option struct {
short rune // 0 means no short name
long string // "" means no long name
isLong bool // True if they used the long name
flag bool // true if a boolean flag
defval string // default value
optional bool // true if we take an optional value
help string // help message
where string // file where the option was defined
value Value // current value of option
count int // number of times we have seen this option
name string // name of the value (for usage)
uname string // name of the option (for usage)
short rune // 0 means no short name
long string // "" means no long name
isLong bool // True if they used the long name
flag bool // true if a boolean flag
defval string // default value
optional bool // true if we take an optional value
help string // help message
where string // file where the option was defined
value Value // current value of option
count int // number of times we have seen this option
name string // name of the value (for usage)
uname string // name of the option (for usage)
mandatory bool // this option must be specified
group string // mutual exclusion group
}

// usageName returns the name of the option for printing usage lines in one
Expand Down Expand Up @@ -121,12 +131,14 @@ func (o *option) sortName() string {
return o.long[:1] + o.long
}

func (o *option) Seen() bool { return o.count > 0 }
func (o *option) Count() int { return o.count }
func (o *option) IsFlag() bool { return o.flag }
func (o *option) String() string { return o.value.String() }
func (o *option) SetOptional() Option { o.optional = true; return o }
func (o *option) SetFlag() Option { o.flag = true; return o }
func (o *option) Seen() bool { return o.count > 0 }
func (o *option) Count() int { return o.count }
func (o *option) IsFlag() bool { return o.flag }
func (o *option) String() string { return o.value.String() }
func (o *option) SetOptional() Option { o.optional = true; return o }
func (o *option) SetFlag() Option { o.flag = true; return o }
func (o *option) Mandatory() Option { o.mandatory = true; return o }
func (o *option) SetGroup(g string) Option { o.group = g; return o }

func (o *option) Value() Value {
if o == nil {
Expand Down
16 changes: 16 additions & 0 deletions v2/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Set struct {
shortOptions map[rune]*option
longOptions map[string]*option
options optionList
requiredGroups []string
}

// New returns a newly created option set.
Expand Down Expand Up @@ -291,3 +292,18 @@ func (s *Set) Reset() {
opt.Reset()
}
}

// RequiredGroup marks the group set with Option.SetGroup as required. At least
// one option in the group must be seen by parse. Calling RequiredGroup with a
// group name that has no options will cause parsing to always fail.
func (s *Set) RequiredGroup(group string) {
s.requiredGroups = append(s.requiredGroups, group)
}

// RequiredGroup marks the group set with Option.SetGroup as required on the
// command line. At least one option in the group must be seen by parse.
// Calling RequiredGroup with a group name that has no options will cause
// parsing to always fail.
func RequiredGroup(group string) {
CommandLine.requiredGroups = append(CommandLine.requiredGroups, group)
}
1 change: 1 addition & 0 deletions v2/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func reset() {
CommandLine.options = nil
CommandLine.args = nil
CommandLine.program = ""
CommandLine.requiredGroups = nil
errorString = ""
}

Expand Down

0 comments on commit 6173d3f

Please sign in to comment.