Skip to content

Commit ff6d5ba

Browse files
tinytyranidStautis
andauthored
Feature: Add xand tag (#442)
* Feat: Add xand group and check for missing * Fix: Split and combine err in TestMultiand for consistency * Feat: Check missing required flags in xand groups * Feat: Handle combined xor and xand * Docs: Add info about combined xand and required use * Docs: Fix language error in xand description Co-authored-by: Stautis <[email protected]> * Feat: Rename xand to and * Refactor: Switch from fmt.Sprintf to err.Error * Refactor: Get requiredAndGroup map in separate function --------- Co-authored-by: Stautis <[email protected]>
1 parent 5f9c5cc commit ff6d5ba

File tree

7 files changed

+195
-14
lines changed

7 files changed

+195
-14
lines changed

Diff for: README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,7 @@ Tags can be in two forms:
554554
Both can coexist with standard Tag parsing.
555555

556556
| Tag | Description |
557-
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
557+
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
558558
| `cmd:""` | If present, struct is a command. |
559559
| `arg:""` | If present, field is an argument. Required by default. |
560560
| `env:"X,Y,..."` | Specify envars to use for default value. The envs are resolved in the declared order. The first value found is used. |
@@ -566,7 +566,7 @@ Both can coexist with standard Tag parsing.
566566
| `default:"1"` | On a command, make it the default. |
567567
| `default:"withargs"` | On a command, make it the default and allow args/flags from that command |
568568
| `short:"X"` | Short name, if flag. |
569-
| `aliases:"X,Y"` | One or more aliases (for cmd or flag). |
569+
| `aliases:"X,Y"` | One or more aliases (for cmd or flag). |
570570
| `required:""` | If present, flag/arg is required. |
571571
| `optional:""` | If present, flag/arg is optional. |
572572
| `hidden:""` | If present, command or flag is hidden. |
@@ -577,6 +577,7 @@ Both can coexist with standard Tag parsing.
577577
| `enum:"X,Y,..."` | Set of valid values allowed for this flag. An enum field must be `required` or have a valid `default`. |
578578
| `group:"X"` | Logical group for a flag or command. |
579579
| `xor:"X,Y,..."` | Exclusive OR groups for flags. Only one flag in the group can be used which is restricted within the same command. When combined with `required`, at least one of the `xor` group will be required. |
580+
| `and:"X,Y,..."` | Exclsuive AND groups for flags. All flags in the group must be used in the same command. When combined with `required`, all flags in the group will be required. |
580581
| `prefix:"X"` | Prefix for all sub-flags. |
581582
| `envprefix:"X"` | Envar prefix for all sub-flags. |
582583
| `set:"K=V"` | Set a variable for expansion by child elements. Multiples can occur. |

Diff for: build.go

+1
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
323323
Envs: tag.Envs,
324324
Group: buildGroupForKey(k, tag.Group),
325325
Xor: tag.Xor,
326+
And: tag.And,
326327
Hidden: tag.Hidden,
327328
}
328329
value.Flag = flag

Diff for: context.go

+77-2
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ func (c *Context) Validate() error { //nolint: gocyclo
259259
if err := checkMissingPositionals(positionals, node.Positional); err != nil {
260260
return err
261261
}
262-
if err := checkXorDuplicates(c.Path); err != nil {
262+
if err := checkXorDuplicatedAndAndMissing(c.Path); err != nil {
263263
return err
264264
}
265265

@@ -831,23 +831,35 @@ func (c *Context) PrintUsage(summary bool) error {
831831
func checkMissingFlags(flags []*Flag) error {
832832
xorGroupSet := map[string]bool{}
833833
xorGroup := map[string][]string{}
834+
andGroupSet := map[string]bool{}
835+
andGroup := map[string][]string{}
834836
missing := []string{}
837+
andGroupRequired := getRequiredAndGroupMap(flags)
835838
for _, flag := range flags {
839+
for _, and := range flag.And {
840+
flag.Required = andGroupRequired[and]
841+
}
836842
if flag.Set {
837843
for _, xor := range flag.Xor {
838844
xorGroupSet[xor] = true
839845
}
846+
for _, and := range flag.And {
847+
andGroupSet[and] = true
848+
}
840849
}
841850
if !flag.Required || flag.Set {
842851
continue
843852
}
844-
if len(flag.Xor) > 0 {
853+
if len(flag.Xor) > 0 || len(flag.And) > 0 {
845854
for _, xor := range flag.Xor {
846855
if xorGroupSet[xor] {
847856
continue
848857
}
849858
xorGroup[xor] = append(xorGroup[xor], flag.Summary())
850859
}
860+
for _, and := range flag.And {
861+
andGroup[and] = append(andGroup[and], flag.Summary())
862+
}
851863
} else {
852864
missing = append(missing, flag.Summary())
853865
}
@@ -857,6 +869,11 @@ func checkMissingFlags(flags []*Flag) error {
857869
missing = append(missing, strings.Join(flags, " or "))
858870
}
859871
}
872+
for _, flags := range andGroup {
873+
if len(flags) > 1 {
874+
missing = append(missing, strings.Join(flags, " and "))
875+
}
876+
}
860877

861878
if len(missing) == 0 {
862879
return nil
@@ -867,6 +884,18 @@ func checkMissingFlags(flags []*Flag) error {
867884
return fmt.Errorf("missing flags: %s", strings.Join(missing, ", "))
868885
}
869886

887+
func getRequiredAndGroupMap(flags []*Flag) map[string]bool {
888+
andGroupRequired := map[string]bool{}
889+
for _, flag := range flags {
890+
for _, and := range flag.And {
891+
if flag.Required {
892+
andGroupRequired[and] = true
893+
}
894+
}
895+
}
896+
return andGroupRequired
897+
}
898+
870899
func checkMissingChildren(node *Node) error {
871900
missing := []string{}
872901

@@ -977,6 +1006,20 @@ func checkPassthroughArg(target reflect.Value) bool {
9771006
}
9781007
}
9791008

1009+
func checkXorDuplicatedAndAndMissing(paths []*Path) error {
1010+
errs := []string{}
1011+
if err := checkXorDuplicates(paths); err != nil {
1012+
errs = append(errs, err.Error())
1013+
}
1014+
if err := checkAndMissing(paths); err != nil {
1015+
errs = append(errs, err.Error())
1016+
}
1017+
if len(errs) > 0 {
1018+
return fmt.Errorf(strings.Join(errs, ", "))
1019+
}
1020+
return nil
1021+
}
1022+
9801023
func checkXorDuplicates(paths []*Path) error {
9811024
for _, path := range paths {
9821025
seen := map[string]*Flag{}
@@ -995,6 +1038,38 @@ func checkXorDuplicates(paths []*Path) error {
9951038
return nil
9961039
}
9971040

1041+
func checkAndMissing(paths []*Path) error {
1042+
for _, path := range paths {
1043+
missingMsgs := []string{}
1044+
andGroups := map[string][]*Flag{}
1045+
for _, flag := range path.Flags {
1046+
for _, and := range flag.And {
1047+
andGroups[and] = append(andGroups[and], flag)
1048+
}
1049+
}
1050+
for _, flags := range andGroups {
1051+
oneSet := false
1052+
notSet := []*Flag{}
1053+
flagNames := []string{}
1054+
for _, flag := range flags {
1055+
flagNames = append(flagNames, flag.Name)
1056+
if flag.Set {
1057+
oneSet = true
1058+
} else {
1059+
notSet = append(notSet, flag)
1060+
}
1061+
}
1062+
if len(notSet) > 0 && oneSet {
1063+
missingMsgs = append(missingMsgs, fmt.Sprintf("--%s must be used together", strings.Join(flagNames, " and --")))
1064+
}
1065+
}
1066+
if len(missingMsgs) > 0 {
1067+
return fmt.Errorf("%s", strings.Join(missingMsgs, ", "))
1068+
}
1069+
}
1070+
return nil
1071+
}
1072+
9981073
func findPotentialCandidates(needle string, haystack []string, format string, args ...interface{}) error {
9991074
if len(haystack) == 0 {
10001075
return fmt.Errorf(format, args...)

Diff for: go.sum

-10
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,5 @@
1-
github.com/alecthomas/assert/v2 v2.4.1 h1:mwPZod/d35nlaCppr6sFP0rbCL05WH9fIo7lvsf47zo=
2-
github.com/alecthomas/assert/v2 v2.4.1/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM=
3-
github.com/alecthomas/assert/v2 v2.5.0 h1:OJKYg53BQx06/bMRBSPDCO49CbCDNiUQXwdoNrt6x5w=
4-
github.com/alecthomas/assert/v2 v2.5.0/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM=
5-
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
6-
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
7-
github.com/alecthomas/assert/v2 v2.8.1 h1:YCxnYR6jjpfnEK5AK5SysALKdUEBPGH4Y7As6tBnDw0=
8-
github.com/alecthomas/assert/v2 v2.8.1/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
91
github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY=
102
github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
11-
github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8=
12-
github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
133
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
144
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
155
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=

Diff for: kong_test.go

+109
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"errors"
66
"fmt"
7+
"sort"
78
"strings"
89
"testing"
910

@@ -919,6 +920,21 @@ func TestXor(t *testing.T) {
919920
assert.NoError(t, err)
920921
}
921922

923+
func TestAnd(t *testing.T) {
924+
var cli struct {
925+
Hello bool `and:"another"`
926+
One bool `and:"group"`
927+
Two string `and:"group"`
928+
}
929+
p := mustNew(t, &cli)
930+
_, err := p.Parse([]string{"--hello", "--one"})
931+
assert.EqualError(t, err, "--one and --two must be used together")
932+
933+
p = mustNew(t, &cli)
934+
_, err = p.Parse([]string{"--one", "--two=hi", "--hello"})
935+
assert.NoError(t, err)
936+
}
937+
922938
func TestXorChild(t *testing.T) {
923939
var cli struct {
924940
One bool `xor:"group"`
@@ -936,6 +952,23 @@ func TestXorChild(t *testing.T) {
936952
assert.Error(t, err, "--two and --three can't be used together")
937953
}
938954

955+
func TestAndChild(t *testing.T) {
956+
var cli struct {
957+
One bool `and:"group"`
958+
Cmd struct {
959+
Two string `and:"group"`
960+
Three string `and:"group"`
961+
} `cmd`
962+
}
963+
p := mustNew(t, &cli)
964+
_, err := p.Parse([]string{"--one", "cmd", "--two=hi", "--three=hello"})
965+
assert.NoError(t, err)
966+
967+
p = mustNew(t, &cli)
968+
_, err = p.Parse([]string{"--two=hi", "cmd"})
969+
assert.Error(t, err, "--two and --three must be used together")
970+
}
971+
939972
func TestMultiXor(t *testing.T) {
940973
var cli struct {
941974
Hello bool `xor:"one,two"`
@@ -952,6 +985,47 @@ func TestMultiXor(t *testing.T) {
952985
assert.EqualError(t, err, "--hello and --two can't be used together")
953986
}
954987

988+
func TestMultiAnd(t *testing.T) {
989+
var cli struct {
990+
Hello bool `and:"one,two"`
991+
One bool `and:"one"`
992+
Two string `and:"two"`
993+
}
994+
995+
p := mustNew(t, &cli)
996+
_, err := p.Parse([]string{"--hello"})
997+
// Split and combine error so messages always will be in the same order
998+
// when testing
999+
missingMsgs := strings.Split(err.Error(), ", ")
1000+
sort.Strings(missingMsgs)
1001+
err = fmt.Errorf("%s", strings.Join(missingMsgs, ", "))
1002+
assert.EqualError(t, err, "--hello and --one must be used together, --hello and --two must be used together")
1003+
1004+
p = mustNew(t, &cli)
1005+
_, err = p.Parse([]string{"--two=foo"})
1006+
assert.EqualError(t, err, "--hello and --two must be used together")
1007+
}
1008+
1009+
func TestXorAnd(t *testing.T) {
1010+
var cli struct {
1011+
Hello bool `xor:"one" and:"two"`
1012+
One bool `xor:"one"`
1013+
Two string `and:"two"`
1014+
}
1015+
1016+
p := mustNew(t, &cli)
1017+
_, err := p.Parse([]string{"--hello"})
1018+
assert.EqualError(t, err, "--hello and --two must be used together")
1019+
1020+
p = mustNew(t, &cli)
1021+
_, err = p.Parse([]string{"--one"})
1022+
assert.NoError(t, err)
1023+
1024+
p = mustNew(t, &cli)
1025+
_, err = p.Parse([]string{"--hello", "--one"})
1026+
assert.EqualError(t, err, "--hello and --one can't be used together, --hello and --two must be used together")
1027+
}
1028+
9551029
func TestXorRequired(t *testing.T) {
9561030
var cli struct {
9571031
One bool `xor:"one,two" required:""`
@@ -972,6 +1046,26 @@ func TestXorRequired(t *testing.T) {
9721046
assert.EqualError(t, err, "missing flags: --four, --one or --three, --one or --two")
9731047
}
9741048

1049+
func TestAndRequired(t *testing.T) {
1050+
var cli struct {
1051+
One bool `and:"one,two" required:""`
1052+
Two bool `and:"one" required:""`
1053+
Three bool `and:"two"`
1054+
Four bool `required:""`
1055+
}
1056+
p := mustNew(t, &cli)
1057+
_, err := p.Parse([]string{"--one", "--two", "--three"})
1058+
assert.EqualError(t, err, "missing flags: --four")
1059+
1060+
p = mustNew(t, &cli)
1061+
_, err = p.Parse([]string{"--four"})
1062+
assert.EqualError(t, err, "missing flags: --one and --three, --one and --two")
1063+
1064+
p = mustNew(t, &cli)
1065+
_, err = p.Parse([]string{})
1066+
assert.EqualError(t, err, "missing flags: --four, --one and --three, --one and --two")
1067+
}
1068+
9751069
func TestXorRequiredMany(t *testing.T) {
9761070
var cli struct {
9771071
One bool `xor:"one" required:""`
@@ -991,6 +1085,21 @@ func TestXorRequiredMany(t *testing.T) {
9911085
assert.EqualError(t, err, "missing flags: --one or --two or --three")
9921086
}
9931087

1088+
func TestAndRequiredMany(t *testing.T) {
1089+
var cli struct {
1090+
One bool `and:"one" required:""`
1091+
Two bool `and:"one" required:""`
1092+
Three bool `and:"one" required:""`
1093+
}
1094+
p := mustNew(t, &cli)
1095+
_, err := p.Parse([]string{})
1096+
assert.EqualError(t, err, "missing flags: --one and --two and --three")
1097+
1098+
p = mustNew(t, &cli)
1099+
_, err = p.Parse([]string{"--three"})
1100+
assert.EqualError(t, err, "missing flags: --one and --two")
1101+
}
1102+
9941103
func TestEnumSequence(t *testing.T) {
9951104
var cli struct {
9961105
State []string `enum:"a,b,c" default:"a"`

Diff for: model.go

+1
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ type Flag struct {
405405
*Value
406406
Group *Group // Logical grouping when displaying. May also be used by configuration loaders to group options logically.
407407
Xor []string
408+
And []string
408409
PlaceHolder string
409410
Envs []string
410411
Aliases []string

Diff for: tag.go

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type Tag struct {
3232
Enum string
3333
Group string
3434
Xor []string
35+
And []string
3536
Vars Vars
3637
Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix.
3738
EnvPrefix string
@@ -249,6 +250,9 @@ func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo
249250
for _, xor := range t.GetAll("xor") {
250251
t.Xor = append(t.Xor, strings.FieldsFunc(xor, tagSplitFn)...)
251252
}
253+
for _, and := range t.GetAll("and") {
254+
t.And = append(t.And, strings.FieldsFunc(and, tagSplitFn)...)
255+
}
252256
t.Prefix = t.Get("prefix")
253257
t.EnvPrefix = t.Get("envprefix")
254258
t.Embed = t.Has("embed")

0 commit comments

Comments
 (0)