diff --git a/README.md b/README.md index 6339210..e5b4ebe 100644 --- a/README.md +++ b/README.md @@ -446,7 +446,7 @@ Tag | Description `mapsep:"X"` | Separator for maps (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. +`xor:"X,Y,..."` | Exclusive OR groups 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/context.go b/context.go index 1a2ed15..1231009 100644 --- a/context.go +++ b/context.go @@ -871,13 +871,12 @@ func checkXorDuplicates(paths []*Path) error { if !flag.Set { continue } - if flag.Xor == "" { - continue + for _, xor := range flag.Xor { + if seen[xor] != nil { + return fmt.Errorf("--%s and --%s can't be used together", seen[xor].Name, flag.Name) + } + seen[xor] = flag } - 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 diff --git a/kong_test.go b/kong_test.go index 30dd7a5..d879967 100644 --- a/kong_test.go +++ b/kong_test.go @@ -868,6 +868,22 @@ func TestXorChild(t *testing.T) { require.Error(t, err, "--two and --three can't be used together") } +func TestMultiXor(t *testing.T) { + var cli struct { + Hello bool `xor:"one,two"` + One bool `xor:"one"` + Two string `xor:"two"` + } + + p := mustNew(t, &cli) + _, err := p.Parse([]string{"--hello", "--one"}) + require.EqualError(t, err, "--hello and --one can't be used together") + + p = mustNew(t, &cli) + _, err = p.Parse([]string{"--hello", "--two=foo"}) + require.EqualError(t, err, "--hello and --two can't be used together") +} + func TestEnumSequence(t *testing.T) { var cli struct { State []string `enum:"a,b,c" default:"a"` diff --git a/model.go b/model.go index 928c613..450f61c 100644 --- a/model.go +++ b/model.go @@ -377,7 +377,7 @@ type Positional = Value type Flag struct { *Value Group *Group // Logical grouping when displaying. May also be used by configuration loaders to group options logically. - Xor string + Xor []string PlaceHolder string Env string Short rune diff --git a/tag.go b/tag.go index 8c24a9f..9ecde85 100644 --- a/tag.go +++ b/tag.go @@ -29,7 +29,7 @@ type Tag struct { MapSep rune Enum string Group string - Xor string + Xor []string Vars Vars Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix. Embed bool @@ -125,6 +125,10 @@ func newEmptyTag() *Tag { return &Tag{items: map[string][]string{}} } +func tagSplitFn(r rune) bool { + return r == ',' || r == ' ' +} + func parseTag(fv reflect.Value, ft reflect.StructField) *Tag { if ft.Tag.Get("kong") == "-" { t := newEmptyTag() @@ -164,7 +168,9 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag { t.Sep, _ = t.GetSep("sep", ',') t.MapSep, _ = t.GetSep("mapsep", ';') t.Group = t.Get("group") - t.Xor = t.Get("xor") + for _, xor := range t.GetAll("xor") { + t.Xor = append(t.Xor, strings.FieldsFunc(xor, tagSplitFn)...) + } t.Prefix = t.Get("prefix") t.Embed = t.Has("embed") negatable := t.Has("negatable") @@ -172,12 +178,9 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag { fail("negatable can only be set on booleans") } t.Negatable = negatable - splitFn := func(r rune) bool { - return r == ',' || r == ' ' - } aliases := t.Get("aliases") if len(aliases) > 0 { - t.Aliases = append(t.Aliases, strings.FieldsFunc(aliases, splitFn)...) + t.Aliases = append(t.Aliases, strings.FieldsFunc(aliases, tagSplitFn)...) } t.Vars = Vars{} for _, set := range t.GetAll("set") {