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/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") +} 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 {