Skip to content

Commit 6173d3f

Browse files
committed
Add support for mandatory options as well as radio groups.
1 parent fd0d075 commit 6173d3f

File tree

6 files changed

+212
-19
lines changed

6 files changed

+212
-19
lines changed

v2/getopt.go

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,34 @@
152152
// Unless an option type explicitly prohibits it, an option may appear more than
153153
// once in the arguments. The last value provided to the option is the value.
154154
//
155+
// MANDATORY OPTIONS
156+
//
157+
// An option marked as mandatory and not seen when parsing will cause an error
158+
// to be reported such as: "program: --name is a mandatory option". An option
159+
// is marked mandatory by using the Mandatory method:
160+
//
161+
// getopt.FlagLong(&fileName, "path", 0, "the path").Mandatory()
162+
//
163+
// Mandatory options have (required) appended to their help message:
164+
//
165+
// --path=value the path (required)
166+
//
167+
// MUTUALLY EXCLUSIVE OPTIONS
168+
//
169+
// Options can be marked as part of a mutually exclusive group. When two or
170+
// more options in a mutually exclusive group are both seen while parsing then
171+
// an error such as "program: options -a and -b are mutually exclusive" will be
172+
// reported. Mutually exclusive groups are declared using the SetGroup method:
173+
//
174+
// getopt.Flag(&a, 'a', "use method A").SetGroup("method")
175+
// getopt.Flag(&a, 'b', "use method B").SetGroup("method")
176+
//
177+
// A set can have multiple mutually exclusive groups. Mutually exclusive groups
178+
// are identified with their group name in {}'s appeneded to their help message:
179+
//
180+
// -a use method A {method}
181+
// -b use method B {method}
182+
//
155183
// BUILTIN TYPES
156184
//
157185
// The Flag and FlagLong functions support most standard Go types. For the
@@ -318,7 +346,7 @@ func (s *Set) PrintOptions(w io.Writer) {
318346
for _, opt := range s.options {
319347
if opt.uname != "" {
320348
opt.help = strings.TrimSpace(opt.help)
321-
if len(opt.help) == 0 {
349+
if len(opt.help) == 0 && !opt.mandatory && opt.group == "" {
322350
fmt.Fprintf(w, " %s\n", opt.uname)
323351
continue
324352
}
@@ -350,6 +378,12 @@ func (s *Set) PrintOptions(w io.Writer) {
350378
if def != "" {
351379
helpMsg += " [" + def + "]"
352380
}
381+
if opt.group != "" {
382+
helpMsg += " {" + opt.group + "}"
383+
}
384+
if opt.mandatory {
385+
helpMsg += " (required)"
386+
}
353387

354388
help := strings.Split(helpMsg, "\n")
355389
// If they did not put in newlines then we will insert
@@ -444,6 +478,12 @@ func (s *Set) Getopt(args []string, fn func(Option) bool) (err error) {
444478
}
445479
}
446480
}()
481+
482+
defer func() {
483+
if err == nil {
484+
err = s.checkOptions()
485+
}
486+
}()
447487
if fn == nil {
448488
fn = func(Option) bool { return true }
449489
}
@@ -562,3 +602,35 @@ Parsing:
562602
s.args = []string{}
563603
return nil
564604
}
605+
606+
func (s *Set) checkOptions() error {
607+
groups := map[string]Option{}
608+
for _, opt := range s.options {
609+
if !opt.Seen() {
610+
if opt.mandatory {
611+
return fmt.Errorf("option %s is mandatory", opt.Name())
612+
}
613+
continue
614+
}
615+
if opt.group == "" {
616+
continue
617+
}
618+
if opt2 := groups[opt.group]; opt2 != nil {
619+
return fmt.Errorf("options %s and %s are mutually exclusive", opt2.Name(), opt.Name())
620+
}
621+
groups[opt.group] = opt
622+
}
623+
for _, group := range s.requiredGroups {
624+
if groups[group] != nil {
625+
continue
626+
}
627+
var flags []string
628+
for _, opt := range s.options {
629+
if opt.group == group {
630+
flags = append(flags, opt.Name())
631+
}
632+
}
633+
return fmt.Errorf("exactly one of the following options must be specified: %s", strings.Join(flags, ", "))
634+
}
635+
return nil
636+
}

v2/getopt_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2020 Google Inc. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package getopt
6+
7+
import (
8+
"testing"
9+
)
10+
11+
func TestMandatory(t *testing.T) {
12+
for _, tt := range []struct {
13+
name string
14+
in []string
15+
err string
16+
}{
17+
{
18+
name: "required option present",
19+
in: []string{"test", "-r"},
20+
},
21+
{
22+
name: "required option not present",
23+
in: []string{"test", "-o"},
24+
err: "test: option -r is mandatory",
25+
},
26+
{
27+
name: "no options",
28+
in: []string{"test"},
29+
err: "test: option -r is mandatory",
30+
},
31+
} {
32+
reset()
33+
var val bool
34+
Flag(&val, 'o')
35+
Flag(&val, 'r').Mandatory()
36+
parse(tt.in)
37+
if s := checkError(tt.err); s != "" {
38+
t.Errorf("%s: %s", tt.name, s)
39+
}
40+
}
41+
}
42+
43+
func TestGroup(t *testing.T) {
44+
for _, tt := range []struct {
45+
name string
46+
in []string
47+
err string
48+
}{
49+
{
50+
name: "no args",
51+
in: []string{"test"},
52+
err: "test: exactly one of the following options must be specified: -A, -B",
53+
},
54+
{
55+
name: "one of each",
56+
in: []string{"test", "-A", "-C"},
57+
},
58+
{
59+
name: "Two in group One",
60+
in: []string{"test", "-A", "-B"},
61+
err: "test: options -A and -B are mutually exclusive",
62+
},
63+
{
64+
name: "Two in group Two",
65+
in: []string{"test", "-A", "-D", "-C"},
66+
err: "test: options -C and -D are mutually exclusive",
67+
},
68+
} {
69+
reset()
70+
var val bool
71+
Flag(&val, 'o')
72+
Flag(&val, 'A').SetGroup("One")
73+
Flag(&val, 'B').SetGroup("One")
74+
Flag(&val, 'C').SetGroup("Two")
75+
Flag(&val, 'D').SetGroup("Two")
76+
RequiredGroup("One")
77+
parse(tt.in)
78+
if s := checkError(tt.err); s != "" {
79+
t.Errorf("%s: %s", tt.name, s)
80+
}
81+
}
82+
}

v2/help_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,16 @@ func TestHelpDefaults(t *testing.T) {
102102
var fvo flagValue = true
103103
set.FlagLong(&fvo, "vbool_on", 0, "value bool").SetFlag()
104104

105+
required := 17
106+
set.FlagLong(&required, "required", 0, "a required option").Mandatory()
107+
108+
var a, b bool
109+
set.Flag(&a, 'a', "use method A").SetGroup("method")
110+
set.Flag(&b, 'b', "use method B").SetGroup("method")
111+
105112
want := `
113+
-a use method A {method}
114+
-b use method B {method}
106115
--duration=value duration value
107116
--duration_set=value set duration value [1s]
108117
-f, --bool_false false bool value
@@ -120,6 +129,7 @@ func TestHelpDefaults(t *testing.T) {
120129
--int8=value int8 value
121130
--int8_set=value set int8 value [8]
122131
--int_set=value set int value [1]
132+
--required=value a required option [17] (required)
123133
--string=value string value
124134
--string_set=value set string value [string]
125135
-t, --bool_true true bool value [true]

v2/option.go

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,31 @@ type Option interface {
5959
// yet been seen, including resetting the value of the option
6060
// to its original default state.
6161
Reset()
62+
63+
// Mandataory sets the mandatory flag of the option. Parse will
64+
// fail if a mandatory option is missing.
65+
Mandatory() Option
66+
67+
// SetGroup sets the option as part of a radio group. Parse will
68+
// fail if two options in the same group are seen.
69+
SetGroup(string) Option
6270
}
6371

6472
type option struct {
65-
short rune // 0 means no short name
66-
long string // "" means no long name
67-
isLong bool // True if they used the long name
68-
flag bool // true if a boolean flag
69-
defval string // default value
70-
optional bool // true if we take an optional value
71-
help string // help message
72-
where string // file where the option was defined
73-
value Value // current value of option
74-
count int // number of times we have seen this option
75-
name string // name of the value (for usage)
76-
uname string // name of the option (for usage)
73+
short rune // 0 means no short name
74+
long string // "" means no long name
75+
isLong bool // True if they used the long name
76+
flag bool // true if a boolean flag
77+
defval string // default value
78+
optional bool // true if we take an optional value
79+
help string // help message
80+
where string // file where the option was defined
81+
value Value // current value of option
82+
count int // number of times we have seen this option
83+
name string // name of the value (for usage)
84+
uname string // name of the option (for usage)
85+
mandatory bool // this option must be specified
86+
group string // mutual exclusion group
7787
}
7888

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

124-
func (o *option) Seen() bool { return o.count > 0 }
125-
func (o *option) Count() int { return o.count }
126-
func (o *option) IsFlag() bool { return o.flag }
127-
func (o *option) String() string { return o.value.String() }
128-
func (o *option) SetOptional() Option { o.optional = true; return o }
129-
func (o *option) SetFlag() Option { o.flag = true; return o }
134+
func (o *option) Seen() bool { return o.count > 0 }
135+
func (o *option) Count() int { return o.count }
136+
func (o *option) IsFlag() bool { return o.flag }
137+
func (o *option) String() string { return o.value.String() }
138+
func (o *option) SetOptional() Option { o.optional = true; return o }
139+
func (o *option) SetFlag() Option { o.flag = true; return o }
140+
func (o *option) Mandatory() Option { o.mandatory = true; return o }
141+
func (o *option) SetGroup(g string) Option { o.group = g; return o }
130142

131143
func (o *option) Value() Value {
132144
if o == nil {

v2/set.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type Set struct {
4646
shortOptions map[rune]*option
4747
longOptions map[string]*option
4848
options optionList
49+
requiredGroups []string
4950
}
5051

5152
// New returns a newly created option set.
@@ -291,3 +292,18 @@ func (s *Set) Reset() {
291292
opt.Reset()
292293
}
293294
}
295+
296+
// RequiredGroup marks the group set with Option.SetGroup as required. At least
297+
// one option in the group must be seen by parse. Calling RequiredGroup with a
298+
// group name that has no options will cause parsing to always fail.
299+
func (s *Set) RequiredGroup(group string) {
300+
s.requiredGroups = append(s.requiredGroups, group)
301+
}
302+
303+
// RequiredGroup marks the group set with Option.SetGroup as required on the
304+
// command line. At least one option in the group must be seen by parse.
305+
// Calling RequiredGroup with a group name that has no options will cause
306+
// parsing to always fail.
307+
func RequiredGroup(group string) {
308+
CommandLine.requiredGroups = append(CommandLine.requiredGroups, group)
309+
}

v2/util_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ func reset() {
2121
CommandLine.options = nil
2222
CommandLine.args = nil
2323
CommandLine.program = ""
24+
CommandLine.requiredGroups = nil
2425
errorString = ""
2526
}
2627

0 commit comments

Comments
 (0)