Repository: alecthomas/kong
Pull request #35 (2019-07-19 23:44:57)

XOR groups for flags.
Prevents more than one flag from being used at once within an xor group:
```
	var cli struct {
		One   bool   `xor:"group"`
		Two   string `xor:"group"`
	}
```


Patch:
diff --git a/README.md b/README.md
index 68a8afb..356044b 100644
--- a/README.md
+++ b/README.md
@@ -403,6 +403,7 @@ Tag                    | Description
 `sep:"X"`              | Separator for sequences (defaults to ","). May be `none` to disable splitting.
 `enum:"X,Y,..."`       | Set of valid values allowed for this flag.
 `group:"X"`            | Logical group for a flag or command.
+`xor:"X"`              | Exclusive OR group for flags. Only one flag in the group can be used which is restricted within the same command.
 `prefix:"X"`           | Prefix for all sub-flags.
 `set:"K=V"`            | Set a variable for expansion by child elements. Multiples can occur.
 `embed`                | If present, this field's children will be embedded in the parent. Useful for composition.
diff --git a/build.go b/build.go
index e87393f..c629275 100644
--- a/build.go
+++ b/build.go
@@ -202,6 +202,7 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
 			PlaceHolder: tag.PlaceHolder,
 			Env:         tag.Env,
 			Group:       tag.Group,
+			Xor:         tag.Xor,
 			Hidden:      tag.Hidden,
 		}
 		value.Flag = flag
diff --git a/context.go b/context.go
index c9dae24..1b11f20 100644
--- a/context.go
+++ b/context.go
@@ -179,6 +179,9 @@ func (c *Context) Validate() error {
 	if err := checkMissingPositionals(positionals, node.Positional); err != nil {
 		return err
 	}
+	if err := checkXorDuplicates(c.Path); err != nil {
+		return err
+	}
 
 	if node.Type == ArgumentNode {
 		value := node.Argument
@@ -643,6 +646,25 @@ func checkMissingPositionals(positional int, values []*Value) error {
 	return fmt.Errorf("missing positional arguments %s", strings.Join(missing, " "))
 }
 
+func checkXorDuplicates(paths []*Path) error {
+	for _, path := range paths {
+		seen := map[string]*Flag{}
+		for _, flag := range path.Flags {
+			if !flag.Set {
+				continue
+			}
+			if flag.Xor == "" {
+				continue
+			}
+			if seen[flag.Xor] != nil {
+				return fmt.Errorf("--%s and --%s can't be used together", seen[flag.Xor].Name, flag.Name)
+			}
+			seen[flag.Xor] = flag
+		}
+	}
+	return nil
+}
+
 func findPotentialCandidates(needle string, haystack []string, format string, args ...interface{}) error {
 	if len(haystack) == 0 {
 		return fmt.Errorf(format, args...)
diff --git a/model.go b/model.go
index 0fda877..8023dce 100644
--- a/model.go
+++ b/model.go
@@ -351,6 +351,7 @@ type Positional = Value
 type Flag struct {
 	*Value
 	Group       string // Logical grouping when displaying. May also be used by configuration loaders to group options logically.
+	Xor         string
 	PlaceHolder string
 	Env         string
 	Short       rune
diff --git a/tag.go b/tag.go
index a5d7030..a642e99 100644
--- a/tag.go
+++ b/tag.go
@@ -27,6 +27,7 @@ type Tag struct {
 	Sep         rune
 	Enum        string
 	Group       string
+	Xor         string
 	Vars        Vars
 	Prefix      string // Optional prefix on anonymous structs. All sub-flags will have this prefix.
 	Embed       bool
@@ -151,6 +152,7 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
 	t.Format = t.Get("format")
 	t.Sep, _ = t.GetRune("sep")
 	t.Group = t.Get("group")
+	t.Xor = t.Get("xor")
 	t.Prefix = t.Get("prefix")
 	t.Embed = t.Has("embed")
 	if t.Sep == 0 {


Test patch:
diff --git a/kong_test.go b/kong_test.go
index 7f553a8..03fd2cb 100644
--- a/kong_test.go
+++ b/kong_test.go
@@ -749,3 +749,35 @@ func TestEnvarEnumValidated(t *testing.T) {
 	_, err := p.Parse(nil)
 	require.EqualError(t, err, "--flag must be one of \"valid\" but got \"invalid\"")
 }
+
+func TestXor(t *testing.T) {
+	var cli struct {
+		Hello bool   `xor:"another"`
+		One   bool   `xor:"group"`
+		Two   string `xor:"group"`
+	}
+	p := mustNew(t, &cli)
+	_, err := p.Parse([]string{"--hello", "--one", "--two=hi"})
+	require.EqualError(t, err, "--one and --two can't be used together")
+
+	p = mustNew(t, &cli)
+	_, err = p.Parse([]string{"--one", "--hello"})
+	require.NoError(t, err)
+}
+
+func TestXorChild(t *testing.T) {
+	var cli struct {
+		One bool `xor:"group"`
+		Cmd struct {
+			Two   string `xor:"group"`
+			Three string `xor:"group"`
+		} `cmd`
+	}
+	p := mustNew(t, &cli)
+	_, err := p.Parse([]string{"--one", "cmd", "--two=hi"})
+	require.NoError(t, err)
+
+	p = mustNew(t, &cli)
+	_, err = p.Parse([]string{"--two=hi", "cmd", "--three"})
+	require.Error(t, err, "--two and --three can't be used together")
+}