4be6ae6168
* hooks: Recursively search embedded fields for methods
Follow up to #493 and 840220c
Kong currently supports hooks on embedded fields of a parsed node,
but only at the first level of embedding:
```
type mainCmd struct {
FooOptions
}
type FooOptions struct {
BarOptions
}
func (f *FooOptions) BeforeApply() error {
// this will be called
}
type BarOptions struct {
}
func (b *BarOptions) BeforeApply() error {
// this will not be called
}
```
This change adds support for hooks to be defined
on embedded fields of embedded fields so that the above
example would work as expected.
Per #493, the definition of "embedded" field is adjusted to mean:
- Any anonymous (Go-embedded) field that is exported
- Any non-anonymous field that is tagged with `embed:""`
*Testing*:
Includes a test case for embedding an anonymous field in an `embed:""`
and an `embed:""` field in an anonymous field.
* Use recursion to build up the list of receivers
The 'receivers' parameter helps avoid constant memory allocation
as the backing storage for the slice is reused across recursive calls.
2563 lines
58 KiB
Go
2563 lines
58 KiB
Go
package kong_test
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/alecthomas/assert/v2"
|
|
"github.com/alecthomas/repr"
|
|
|
|
"github.com/alecthomas/kong"
|
|
)
|
|
|
|
func mustNew(t *testing.T, cli any, options ...kong.Option) *kong.Kong {
|
|
t.Helper()
|
|
options = append([]kong.Option{
|
|
kong.Name("test"),
|
|
kong.Exit(func(int) {
|
|
t.Helper()
|
|
t.Fatalf("unexpected exit()")
|
|
}),
|
|
}, options...)
|
|
parser, err := kong.New(cli, options...)
|
|
assert.NoError(t, err)
|
|
return parser
|
|
}
|
|
|
|
func TestPositionalArguments(t *testing.T) {
|
|
var cli struct {
|
|
User struct {
|
|
Create struct {
|
|
ID int `kong:"arg"`
|
|
First string `kong:"arg"`
|
|
Last string `kong:"arg"`
|
|
} `kong:"cmd"`
|
|
} `kong:"cmd"`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
ctx, err := p.Parse([]string{"user", "create", "10", "Alec", "Thomas"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "user create <id> <first> <last>", ctx.Command())
|
|
t.Run("Missing", func(t *testing.T) {
|
|
_, err := p.Parse([]string{"user", "create", "10"})
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestBranchingArgument(t *testing.T) {
|
|
/*
|
|
app user create <id> <first> <last>
|
|
app user <id> delete
|
|
app user <id> rename <to>
|
|
|
|
*/
|
|
var cli struct {
|
|
User struct {
|
|
Create struct {
|
|
ID string `kong:"arg"`
|
|
First string `kong:"arg"`
|
|
Last string `kong:"arg"`
|
|
} `kong:"cmd"`
|
|
|
|
// Branching argument.
|
|
ID struct {
|
|
ID int `kong:"arg"`
|
|
Flag int
|
|
Delete struct{} `kong:"cmd"`
|
|
Rename struct {
|
|
To string
|
|
} `kong:"cmd"`
|
|
} `kong:"arg"`
|
|
} `kong:"cmd,help='User management.'"`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
ctx, err := p.Parse([]string{"user", "10", "delete"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 10, cli.User.ID.ID)
|
|
assert.Equal(t, "user <id> delete", ctx.Command())
|
|
t.Run("Missing", func(t *testing.T) {
|
|
_, err = p.Parse([]string{"user"})
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestResetWithDefaults(t *testing.T) {
|
|
var cli struct {
|
|
Flag string
|
|
FlagWithDefault string `kong:"default='default'"`
|
|
}
|
|
cli.Flag = "BLAH"
|
|
cli.FlagWithDefault = "BLAH"
|
|
parser := mustNew(t, &cli)
|
|
_, err := parser.Parse([]string{})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "", cli.Flag)
|
|
assert.Equal(t, "default", cli.FlagWithDefault)
|
|
}
|
|
|
|
func TestFlagSlice(t *testing.T) {
|
|
var cli struct {
|
|
Slice []int
|
|
}
|
|
parser := mustNew(t, &cli)
|
|
_, err := parser.Parse([]string{"--slice=1,2,3"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []int{1, 2, 3}, cli.Slice)
|
|
}
|
|
|
|
func TestFlagSliceWithSeparator(t *testing.T) {
|
|
var cli struct {
|
|
Slice []string
|
|
}
|
|
parser := mustNew(t, &cli)
|
|
_, err := parser.Parse([]string{`--slice=a\,b,c`})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"a,b", "c"}, cli.Slice)
|
|
}
|
|
|
|
func TestArgSlice(t *testing.T) {
|
|
var cli struct {
|
|
Slice []int `arg`
|
|
Flag bool
|
|
}
|
|
parser := mustNew(t, &cli)
|
|
_, err := parser.Parse([]string{"1", "2", "3", "--flag"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []int{1, 2, 3}, cli.Slice)
|
|
assert.Equal(t, true, cli.Flag)
|
|
}
|
|
|
|
func TestArgSliceWithSeparator(t *testing.T) {
|
|
var cli struct {
|
|
Slice []string `arg`
|
|
Flag bool
|
|
}
|
|
parser := mustNew(t, &cli)
|
|
_, err := parser.Parse([]string{"a,b", "c", "--flag"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"a,b", "c"}, cli.Slice)
|
|
assert.Equal(t, true, cli.Flag)
|
|
}
|
|
|
|
func TestUnsupportedFieldErrors(t *testing.T) {
|
|
var cli struct {
|
|
Keys struct{}
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestMatchingArgField(t *testing.T) {
|
|
var cli struct {
|
|
ID struct {
|
|
NotID int `kong:"arg"`
|
|
} `kong:"arg"`
|
|
}
|
|
|
|
_, err := kong.New(&cli)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestCantMixPositionalAndBranches(t *testing.T) {
|
|
var cli struct {
|
|
Arg string `kong:"arg"`
|
|
Command struct {
|
|
} `kong:"cmd"`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestPropagatedFlags(t *testing.T) {
|
|
var cli struct {
|
|
Flag1 string
|
|
Command1 struct {
|
|
Flag2 bool
|
|
Command2 struct{} `kong:"cmd"`
|
|
} `kong:"cmd"`
|
|
}
|
|
|
|
parser := mustNew(t, &cli)
|
|
_, err := parser.Parse([]string{"command-1", "command-2", "--flag-2", "--flag-1=moo"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "moo", cli.Flag1)
|
|
assert.Equal(t, true, cli.Command1.Flag2)
|
|
}
|
|
|
|
func TestRequiredFlag(t *testing.T) {
|
|
var cli struct {
|
|
Flag string `kong:"required"`
|
|
}
|
|
|
|
parser := mustNew(t, &cli)
|
|
_, err := parser.Parse([]string{})
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestOptionalArg(t *testing.T) {
|
|
var cli struct {
|
|
Arg string `kong:"arg,optional"`
|
|
}
|
|
|
|
parser := mustNew(t, &cli)
|
|
_, err := parser.Parse([]string{})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestOptionalArgWithDefault(t *testing.T) {
|
|
var cli struct {
|
|
Arg string `kong:"arg,optional,default='moo'"`
|
|
}
|
|
|
|
parser := mustNew(t, &cli)
|
|
_, err := parser.Parse([]string{})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "moo", cli.Arg)
|
|
}
|
|
|
|
func TestArgWithDefaultIsOptional(t *testing.T) {
|
|
var cli struct {
|
|
Arg string `kong:"arg,default='moo'"`
|
|
}
|
|
|
|
parser := mustNew(t, &cli)
|
|
_, err := parser.Parse([]string{})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "moo", cli.Arg)
|
|
}
|
|
|
|
func TestRequiredArg(t *testing.T) {
|
|
var cli struct {
|
|
Arg string `kong:"arg"`
|
|
}
|
|
|
|
parser := mustNew(t, &cli)
|
|
_, err := parser.Parse([]string{})
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestInvalidRequiredAfterOptional(t *testing.T) {
|
|
var cli struct {
|
|
ID int `kong:"arg,optional"`
|
|
Name string `kong:"arg"`
|
|
}
|
|
|
|
_, err := kong.New(&cli)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestOptionalStructArg(t *testing.T) {
|
|
var cli struct {
|
|
Name struct {
|
|
Name string `kong:"arg,optional"`
|
|
Enabled bool
|
|
} `kong:"arg,optional"`
|
|
}
|
|
|
|
parser := mustNew(t, &cli)
|
|
|
|
t.Run("WithFlag", func(t *testing.T) {
|
|
_, err := parser.Parse([]string{"gak", "--enabled"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "gak", cli.Name.Name)
|
|
assert.Equal(t, true, cli.Name.Enabled)
|
|
})
|
|
|
|
t.Run("WithoutFlag", func(t *testing.T) {
|
|
_, err := parser.Parse([]string{"gak"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "gak", cli.Name.Name)
|
|
})
|
|
|
|
t.Run("WithNothing", func(t *testing.T) {
|
|
_, err := parser.Parse([]string{})
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestMixedRequiredArgs(t *testing.T) {
|
|
var cli struct {
|
|
Name string `kong:"arg"`
|
|
ID int `kong:"arg,optional"`
|
|
}
|
|
|
|
parser := mustNew(t, &cli)
|
|
|
|
t.Run("SingleRequired", func(t *testing.T) {
|
|
_, err := parser.Parse([]string{"gak", "5"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "gak", cli.Name)
|
|
assert.Equal(t, 5, cli.ID)
|
|
})
|
|
|
|
t.Run("ExtraOptional", func(t *testing.T) {
|
|
_, err := parser.Parse([]string{"gak"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "gak", cli.Name)
|
|
})
|
|
}
|
|
|
|
func TestInvalidDefaultErrors(t *testing.T) {
|
|
var cli struct {
|
|
Flag int `kong:"default='foo'"`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse(nil)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestCommandMissingTagIsInvalid(t *testing.T) {
|
|
var cli struct {
|
|
One struct{}
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestDuplicateFlag(t *testing.T) {
|
|
var cli struct {
|
|
Flag bool
|
|
Cmd struct {
|
|
Flag bool
|
|
} `kong:"cmd"`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestDuplicateFlagOnPeerCommandIsOkay(t *testing.T) {
|
|
var cli struct {
|
|
Cmd1 struct {
|
|
Flag bool
|
|
} `kong:"cmd"`
|
|
Cmd2 struct {
|
|
Flag bool
|
|
} `kong:"cmd"`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestTraceErrorPartiallySucceeds(t *testing.T) {
|
|
var cli struct {
|
|
One struct {
|
|
Two struct {
|
|
} `kong:"cmd"`
|
|
} `kong:"cmd"`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
ctx, err := kong.Trace(p, []string{"one", "bad"})
|
|
assert.NoError(t, err)
|
|
assert.Error(t, ctx.Error)
|
|
assert.Equal(t, "one", ctx.Command())
|
|
}
|
|
|
|
type commandWithNegatableFlag struct {
|
|
Flag bool `kong:"default='true',negatable"`
|
|
Custom bool `kong:"default='true',negatable='standard'"`
|
|
ran bool
|
|
}
|
|
|
|
func (c *commandWithNegatableFlag) Run() error {
|
|
c.ran = true
|
|
return nil
|
|
}
|
|
|
|
func TestNegatableFlag(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
expectedFlag bool
|
|
expectedCustom bool
|
|
}{
|
|
{
|
|
name: "no flag",
|
|
args: []string{"cmd"},
|
|
expectedFlag: true,
|
|
expectedCustom: true,
|
|
},
|
|
{
|
|
name: "boolean flag",
|
|
args: []string{"cmd", "--flag"},
|
|
expectedFlag: true,
|
|
expectedCustom: true,
|
|
},
|
|
{
|
|
name: "custom boolean flag",
|
|
args: []string{"cmd", "--custom"},
|
|
expectedFlag: true,
|
|
expectedCustom: true,
|
|
},
|
|
{
|
|
name: "inverted boolean flag",
|
|
args: []string{"cmd", "--flag=false"},
|
|
expectedFlag: false,
|
|
expectedCustom: true,
|
|
},
|
|
{
|
|
name: "custom inverted boolean flag",
|
|
args: []string{"cmd", "--custom=false"},
|
|
expectedFlag: true,
|
|
expectedCustom: false,
|
|
},
|
|
{
|
|
name: "negated boolean flag",
|
|
args: []string{"cmd", "--no-flag"},
|
|
expectedFlag: false,
|
|
expectedCustom: true,
|
|
},
|
|
{
|
|
name: "custom negated boolean flag",
|
|
args: []string{"cmd", "--standard"},
|
|
expectedFlag: true,
|
|
expectedCustom: false,
|
|
},
|
|
{
|
|
name: "inverted negated boolean flag",
|
|
args: []string{"cmd", "--no-flag=false"},
|
|
expectedFlag: true,
|
|
expectedCustom: true,
|
|
},
|
|
{
|
|
name: "inverted custom negated boolean flag",
|
|
args: []string{"cmd", "--standard=false"},
|
|
expectedFlag: true,
|
|
expectedCustom: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var cli struct {
|
|
Cmd commandWithNegatableFlag `kong:"cmd"`
|
|
}
|
|
|
|
p := mustNew(t, &cli)
|
|
kctx, err := p.Parse(tt.args)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.expectedFlag, cli.Cmd.Flag)
|
|
assert.Equal(t, tt.expectedCustom, cli.Cmd.Custom)
|
|
|
|
err = kctx.Run()
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.expectedFlag, cli.Cmd.Flag)
|
|
assert.Equal(t, tt.expectedCustom, cli.Cmd.Custom)
|
|
assert.True(t, cli.Cmd.ran)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDuplicateNegatableLong(t *testing.T) {
|
|
cli2 := struct {
|
|
NoFlag bool
|
|
Flag bool `negatable:""` // negation duplicates NoFlag
|
|
}{}
|
|
_, err := kong.New(&cli2)
|
|
assert.EqualError(t, err, "<anonymous struct>.Flag: duplicate negation flag --no-flag")
|
|
|
|
cli3 := struct {
|
|
One bool
|
|
Two bool `negatable:"one"` // negation duplicates Flag2
|
|
}{}
|
|
_, err = kong.New(&cli3)
|
|
assert.EqualError(t, err, "<anonymous struct>.Two: duplicate negation flag --one")
|
|
}
|
|
|
|
func TestDuplicateNegatableFlagsInSubcommands(t *testing.T) {
|
|
cli2 := struct {
|
|
Sub struct {
|
|
Negated bool `negatable:"nope-"`
|
|
} `cmd:""`
|
|
Sub2 struct {
|
|
Negated bool `negatable:"nope-"`
|
|
} `cmd:""`
|
|
}{}
|
|
_, err := kong.New(&cli2)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestExistingNoFlag(t *testing.T) {
|
|
var cli struct {
|
|
Cmd struct {
|
|
Flag bool `kong:"default='true'"`
|
|
NoFlag string
|
|
} `kong:"cmd"`
|
|
}
|
|
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"cmd", "--no-flag=none"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, true, cli.Cmd.Flag)
|
|
assert.Equal(t, "none", cli.Cmd.NoFlag)
|
|
}
|
|
|
|
func TestInvalidNegatedNonBool(t *testing.T) {
|
|
var cli struct {
|
|
Cmd struct {
|
|
Flag string `kong:"negatable"`
|
|
} `kong:"cmd"`
|
|
}
|
|
|
|
_, err := kong.New(&cli)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
type hookContext struct {
|
|
cmd bool
|
|
values []string
|
|
}
|
|
|
|
type hookValue string
|
|
|
|
func (h *hookValue) BeforeApply(ctx *hookContext) error {
|
|
ctx.values = append(ctx.values, "before:"+string(*h))
|
|
return nil
|
|
}
|
|
|
|
func (h *hookValue) AfterApply(ctx *hookContext) error {
|
|
ctx.values = append(ctx.values, "after:"+string(*h))
|
|
return nil
|
|
}
|
|
|
|
type hookCmd struct {
|
|
Two hookValue `kong:"arg,optional"`
|
|
Three hookValue
|
|
}
|
|
|
|
func (h *hookCmd) BeforeApply(ctx *hookContext) error {
|
|
ctx.cmd = true
|
|
return nil
|
|
}
|
|
|
|
func (h *hookCmd) AfterApply(ctx *hookContext) error {
|
|
ctx.cmd = true
|
|
return nil
|
|
}
|
|
|
|
func TestHooks(t *testing.T) {
|
|
var tests = []struct {
|
|
name string
|
|
input string
|
|
values hookContext
|
|
}{
|
|
{"Command", "one", hookContext{true, nil}},
|
|
{"Arg", "one two", hookContext{true, []string{"before:", "after:two"}}},
|
|
{"Flag", "one --three=THREE", hookContext{true, []string{"before:", "after:THREE"}}},
|
|
{"ArgAndFlag", "one two --three=THREE", hookContext{true, []string{"before:", "before:", "after:two", "after:THREE"}}},
|
|
}
|
|
|
|
var cli struct {
|
|
One hookCmd `cmd:""`
|
|
}
|
|
|
|
ctx := &hookContext{}
|
|
p := mustNew(t, &cli, kong.Bind(ctx))
|
|
|
|
for _, test := range tests {
|
|
test := test
|
|
*ctx = hookContext{}
|
|
cli.One = hookCmd{}
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, err := p.Parse(strings.Split(test.input, " "))
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, &test.values, ctx)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestShort(t *testing.T) {
|
|
var cli struct {
|
|
Bool bool `short:"b"`
|
|
String string `short:"s"`
|
|
}
|
|
app := mustNew(t, &cli)
|
|
_, err := app.Parse([]string{"-b", "-shello"})
|
|
assert.NoError(t, err)
|
|
assert.True(t, cli.Bool)
|
|
assert.Equal(t, "hello", cli.String)
|
|
}
|
|
|
|
func TestAlias(t *testing.T) {
|
|
var cli struct {
|
|
String string `aliases:"str"`
|
|
}
|
|
app := mustNew(t, &cli)
|
|
_, err := app.Parse([]string{"--str", "hello"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "hello", cli.String)
|
|
}
|
|
|
|
func TestDuplicateFlagChoosesLast(t *testing.T) {
|
|
var cli struct {
|
|
Flag int
|
|
}
|
|
|
|
_, err := mustNew(t, &cli).Parse([]string{"--flag=1", "--flag=2"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 2, cli.Flag)
|
|
}
|
|
|
|
func TestDuplicateSliceAccumulates(t *testing.T) {
|
|
var cli struct {
|
|
Flag []int
|
|
}
|
|
|
|
args := []string{"--flag=1,2", "--flag=3,4"}
|
|
_, err := mustNew(t, &cli).Parse(args)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []int{1, 2, 3, 4}, cli.Flag)
|
|
}
|
|
|
|
func TestMapFlag(t *testing.T) {
|
|
var cli struct {
|
|
Set map[string]int
|
|
}
|
|
_, err := mustNew(t, &cli).Parse([]string{"--set", "a=10", "--set", "b=20"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, map[string]int{"a": 10, "b": 20}, cli.Set)
|
|
}
|
|
|
|
func TestMapFlagWithSliceValue(t *testing.T) {
|
|
var cli struct {
|
|
Set map[string][]int
|
|
}
|
|
_, err := mustNew(t, &cli).Parse([]string{"--set", "a=1,2", "--set", "b=3"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, map[string][]int{"a": {1, 2}, "b": {3}}, cli.Set)
|
|
}
|
|
|
|
type embeddedFlags struct {
|
|
Embedded string
|
|
}
|
|
|
|
func TestEmbeddedStruct(t *testing.T) {
|
|
var cli struct {
|
|
embeddedFlags
|
|
NotEmbedded string
|
|
}
|
|
|
|
_, err := mustNew(t, &cli).Parse([]string{"--embedded=moo", "--not-embedded=foo"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "moo", cli.Embedded)
|
|
assert.Equal(t, "foo", cli.NotEmbedded)
|
|
}
|
|
|
|
func TestSliceWithDisabledSeparator(t *testing.T) {
|
|
var cli struct {
|
|
Flag []string `sep:"none"`
|
|
}
|
|
_, err := mustNew(t, &cli).Parse([]string{"--flag=a,b", "--flag=b,c"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"a,b", "b,c"}, cli.Flag)
|
|
}
|
|
|
|
func TestMultilineMessage(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
text string
|
|
want string
|
|
}{
|
|
{"Simple", "hello\nworld", "test: hello\n world\n"},
|
|
{"WithNewline", "hello\nworld\n", "test: hello\n world\n"},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
w := &bytes.Buffer{}
|
|
var cli struct{}
|
|
p := mustNew(t, &cli, kong.Writers(w, w))
|
|
p.Printf("%s", test.text)
|
|
assert.Equal(t, test.want, w.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
type cmdWithRun struct {
|
|
Arg string `arg:""`
|
|
}
|
|
|
|
func (c *cmdWithRun) Run(key string) error {
|
|
c.Arg += key
|
|
if key == "ERROR" {
|
|
return fmt.Errorf("ERROR")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type parentCmdWithRun struct {
|
|
Flag string
|
|
SubCommand struct {
|
|
Arg string `arg:""`
|
|
} `cmd:""`
|
|
}
|
|
|
|
func (p *parentCmdWithRun) Run(key string) error {
|
|
p.SubCommand.Arg += key
|
|
return nil
|
|
}
|
|
|
|
type grammarWithRun struct {
|
|
One cmdWithRun `cmd:""`
|
|
Two cmdWithRun `cmd:""`
|
|
Three parentCmdWithRun `cmd:""`
|
|
}
|
|
|
|
func TestRun(t *testing.T) {
|
|
cli := &grammarWithRun{}
|
|
p := mustNew(t, cli)
|
|
|
|
ctx, err := p.Parse([]string{"one", "two"})
|
|
assert.NoError(t, err)
|
|
err = ctx.Run("hello")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "twohello", cli.One.Arg)
|
|
|
|
ctx, err = p.Parse([]string{"two", "three"})
|
|
assert.NoError(t, err)
|
|
err = ctx.Run("ERROR")
|
|
assert.Error(t, err)
|
|
|
|
ctx, err = p.Parse([]string{"three", "sub-command", "arg"})
|
|
assert.NoError(t, err)
|
|
err = ctx.Run("ping")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "argping", cli.Three.SubCommand.Arg)
|
|
}
|
|
|
|
type failCmd struct{}
|
|
|
|
func (f failCmd) Run() error {
|
|
return errors.New("this command failed")
|
|
}
|
|
|
|
func TestPassesThroughOriginalCommandError(t *testing.T) {
|
|
var cli struct {
|
|
Fail failCmd `kong:"cmd"`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
ctx, _ := p.Parse([]string{"fail"})
|
|
err := ctx.Run()
|
|
assert.Error(t, err)
|
|
assert.Equal(t, err.Error(), "this command failed")
|
|
}
|
|
|
|
func TestInterpolationIntoModel(t *testing.T) {
|
|
var cli struct {
|
|
Flag string `default:"${default_value}" help:"Help, I need ${somebody}" enum:"${enum}"`
|
|
EnumRef string `enum:"a,b" required:"" help:"One of ${enum}"`
|
|
EnvRef string `env:"${env}" help:"God ${env}"`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.Error(t, err)
|
|
p, err := kong.New(&cli, kong.Vars{
|
|
"default_value": "Some default value.",
|
|
"somebody": "chickens!",
|
|
"enum": "a,b,c,d",
|
|
"env": "SAVE_THE_QUEEN",
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 4, len(p.Model.Flags))
|
|
flag := p.Model.Flags[1]
|
|
flag2 := p.Model.Flags[2]
|
|
flag3 := p.Model.Flags[3]
|
|
assert.Equal(t, "Some default value.", flag.Default)
|
|
assert.Equal(t, "Help, I need chickens!", flag.Help)
|
|
assert.Equal(t, map[string]bool{"a": true, "b": true, "c": true, "d": true}, flag.EnumMap())
|
|
assert.Equal(t, []string{"a", "b", "c", "d"}, flag.EnumSlice())
|
|
assert.Equal(t, "One of a,b", flag2.Help)
|
|
assert.Equal(t, []string{"SAVE_THE_QUEEN"}, flag3.Envs)
|
|
assert.Equal(t, "God SAVE_THE_QUEEN", flag3.Help)
|
|
}
|
|
|
|
func TestIssue244(t *testing.T) {
|
|
type Config struct {
|
|
Project string `short:"p" env:"CI_PROJECT_ID" help:"Environment variable: ${env}"`
|
|
}
|
|
w := &strings.Builder{}
|
|
k := mustNew(t, &Config{}, kong.Exit(func(int) {}), kong.Writers(w, w))
|
|
_, err := k.Parse([]string{"--help"})
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, w.String(), `Environment variable: CI_PROJECT_ID`)
|
|
}
|
|
|
|
func TestErrorMissingArgs(t *testing.T) {
|
|
var cli struct {
|
|
One string `arg:""`
|
|
Two string `arg:""`
|
|
}
|
|
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse(nil)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "expected \"<one> <two>\"", err.Error())
|
|
}
|
|
|
|
func TestBoolOverride(t *testing.T) {
|
|
var cli struct {
|
|
Flag bool `default:"true"`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"--flag=false"})
|
|
assert.NoError(t, err)
|
|
_, err = p.Parse([]string{"--flag", "false"})
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestAnonymousPrefix(t *testing.T) {
|
|
type Anonymous struct {
|
|
Flag string
|
|
}
|
|
var cli struct {
|
|
Anonymous `prefix:"anon-"`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"--anon-flag=moo"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "moo", cli.Flag)
|
|
}
|
|
|
|
type TestInterface interface {
|
|
SomeMethod()
|
|
}
|
|
|
|
type TestImpl struct {
|
|
Flag string
|
|
}
|
|
|
|
func (t *TestImpl) SomeMethod() {}
|
|
|
|
func TestEmbedInterface(t *testing.T) {
|
|
type CLI struct {
|
|
SomeFlag string
|
|
TestInterface
|
|
}
|
|
cli := &CLI{TestInterface: &TestImpl{}}
|
|
p := mustNew(t, cli)
|
|
_, err := p.Parse([]string{"--some-flag=foo", "--flag=yes"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "foo", cli.SomeFlag)
|
|
assert.Equal(t, "yes", cli.TestInterface.(*TestImpl).Flag) //nolint
|
|
}
|
|
|
|
func TestExcludedField(t *testing.T) {
|
|
var cli struct {
|
|
Flag string
|
|
Excluded string `kong:"-"`
|
|
}
|
|
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"--flag=foo"})
|
|
assert.NoError(t, err)
|
|
_, err = p.Parse([]string{"--excluded=foo"})
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestUnnamedFieldEmbeds(t *testing.T) {
|
|
type Embed struct {
|
|
Flag string
|
|
}
|
|
var cli struct {
|
|
One Embed `prefix:"one-" embed:""`
|
|
Two Embed `prefix:"two-" embed:""`
|
|
}
|
|
buf := &strings.Builder{}
|
|
p := mustNew(t, &cli, kong.Writers(buf, buf), kong.Exit(func(int) {}))
|
|
_, err := p.Parse([]string{"--help"})
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, buf.String(), `--one-flag=STRING`)
|
|
assert.Contains(t, buf.String(), `--two-flag=STRING`)
|
|
}
|
|
|
|
func TestHooksCalledForDefault(t *testing.T) {
|
|
var cli struct {
|
|
Flag hookValue `default:"default"`
|
|
}
|
|
|
|
ctx := &hookContext{}
|
|
_, err := mustNew(t, &cli, kong.Bind(ctx)).Parse(nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "default", string(cli.Flag))
|
|
assert.Equal(t, []string{"before:default", "after:default"}, ctx.values)
|
|
}
|
|
|
|
func TestEnum(t *testing.T) {
|
|
var cli struct {
|
|
Flag string `enum:"a,b,c" required:""`
|
|
}
|
|
_, err := mustNew(t, &cli).Parse([]string{"--flag", "d"})
|
|
assert.EqualError(t, err, "--flag must be one of \"a\",\"b\",\"c\" but got \"d\"")
|
|
}
|
|
|
|
func TestEnumMeaningfulOrder(t *testing.T) {
|
|
var cli struct {
|
|
Flag string `enum:"first,second,third,fourth,fifth" required:""`
|
|
}
|
|
_, err := mustNew(t, &cli).Parse([]string{"--flag", "sixth"})
|
|
assert.EqualError(t, err, "--flag must be one of \"first\",\"second\",\"third\",\"fourth\",\"fifth\" but got \"sixth\"")
|
|
}
|
|
|
|
type commandWithHook struct {
|
|
value string
|
|
}
|
|
|
|
func (c *commandWithHook) AfterApply(cli *cliWithHook) error {
|
|
c.value = cli.Flag
|
|
return nil
|
|
}
|
|
|
|
type cliWithHook struct {
|
|
Flag string
|
|
Command commandWithHook `cmd:""`
|
|
}
|
|
|
|
func (c *cliWithHook) AfterApply(ctx *kong.Context) error {
|
|
ctx.Bind(c)
|
|
return nil
|
|
}
|
|
|
|
func TestParentBindings(t *testing.T) {
|
|
cli := &cliWithHook{}
|
|
_, err := mustNew(t, cli).Parse([]string{"command", "--flag=foo"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "foo", cli.Command.value)
|
|
}
|
|
|
|
func TestNumericParamErrors(t *testing.T) {
|
|
var cli struct {
|
|
Name string
|
|
}
|
|
parser := mustNew(t, &cli)
|
|
_, err := parser.Parse([]string{"--name", "-10"})
|
|
assert.EqualError(t, err, `--name: expected string value but got "-10" (short flag); perhaps try --name="-10"?`)
|
|
}
|
|
|
|
func TestDefaultValueIsHyphen(t *testing.T) {
|
|
var cli struct {
|
|
Flag string `default:"-"`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse(nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "-", cli.Flag)
|
|
}
|
|
|
|
func TestDefaultEnumValidated(t *testing.T) {
|
|
var cli struct {
|
|
Flag string `default:"invalid" enum:"valid"`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse(nil)
|
|
assert.EqualError(t, err, "--flag must be one of \"valid\" but got \"invalid\"")
|
|
}
|
|
|
|
func TestEnvarEnumValidated(t *testing.T) {
|
|
var cli struct {
|
|
Flag string `env:"FLAG" required:"" enum:"valid"`
|
|
}
|
|
p := newEnvParser(t, &cli, envMap{
|
|
"FLAG": "invalid",
|
|
})
|
|
_, err := p.Parse(nil)
|
|
assert.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"})
|
|
assert.EqualError(t, err, "--one and --two can't be used together")
|
|
|
|
p = mustNew(t, &cli)
|
|
_, err = p.Parse([]string{"--one", "--hello"})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestAnd(t *testing.T) {
|
|
var cli struct {
|
|
Hello bool `and:"another"`
|
|
One bool `and:"group"`
|
|
Two string `and:"group"`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"--hello", "--one"})
|
|
assert.EqualError(t, err, "--one and --two must be used together")
|
|
|
|
p = mustNew(t, &cli)
|
|
_, err = p.Parse([]string{"--one", "--two=hi", "--hello"})
|
|
assert.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"})
|
|
assert.NoError(t, err)
|
|
|
|
p = mustNew(t, &cli)
|
|
_, err = p.Parse([]string{"--two=hi", "cmd", "--three"})
|
|
assert.Error(t, err, "--two and --three can't be used together")
|
|
}
|
|
|
|
func TestAndChild(t *testing.T) {
|
|
var cli struct {
|
|
One bool `and:"group"`
|
|
Cmd struct {
|
|
Two string `and:"group"`
|
|
Three string `and:"group"`
|
|
} `cmd`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"--one", "cmd", "--two=hi", "--three=hello"})
|
|
assert.NoError(t, err)
|
|
|
|
p = mustNew(t, &cli)
|
|
_, err = p.Parse([]string{"--two=hi", "cmd"})
|
|
assert.Error(t, err, "--two and --three must 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"})
|
|
assert.EqualError(t, err, "--hello and --one can't be used together")
|
|
|
|
p = mustNew(t, &cli)
|
|
_, err = p.Parse([]string{"--hello", "--two=foo"})
|
|
assert.EqualError(t, err, "--hello and --two can't be used together")
|
|
}
|
|
|
|
func TestMultiAnd(t *testing.T) {
|
|
var cli struct {
|
|
Hello bool `and:"one,two"`
|
|
One bool `and:"one"`
|
|
Two string `and:"two"`
|
|
}
|
|
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"--hello"})
|
|
// Split and combine error so messages always will be in the same order
|
|
// when testing
|
|
missingMsgs := strings.Split(err.Error(), ", ")
|
|
sort.Strings(missingMsgs)
|
|
err = fmt.Errorf("%s", strings.Join(missingMsgs, ", "))
|
|
assert.EqualError(t, err, "--hello and --one must be used together, --hello and --two must be used together")
|
|
|
|
p = mustNew(t, &cli)
|
|
_, err = p.Parse([]string{"--two=foo"})
|
|
assert.EqualError(t, err, "--hello and --two must be used together")
|
|
}
|
|
|
|
func TestXorAnd(t *testing.T) {
|
|
var cli struct {
|
|
Hello bool `xor:"one" and:"two"`
|
|
One bool `xor:"one"`
|
|
Two string `and:"two"`
|
|
}
|
|
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"--hello"})
|
|
assert.EqualError(t, err, "--hello and --two must be used together")
|
|
|
|
p = mustNew(t, &cli)
|
|
_, err = p.Parse([]string{"--one"})
|
|
assert.NoError(t, err)
|
|
|
|
p = mustNew(t, &cli)
|
|
_, err = p.Parse([]string{"--hello", "--one"})
|
|
assert.EqualError(t, err, "--hello and --one can't be used together, --hello and --two must be used together")
|
|
}
|
|
|
|
func TestOverLappingXorAnd(t *testing.T) {
|
|
var cli struct {
|
|
Hello bool `xor:"one" and:"two"`
|
|
One bool `xor:"one" and:"two"`
|
|
Two string `xor:"one" and:"two"`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.EqualError(t, err, "invalid xor and combination, one and two overlap with more than one: [hello one two]")
|
|
}
|
|
|
|
func TestXorRequired(t *testing.T) {
|
|
var cli struct {
|
|
One bool `xor:"one,two" required:""`
|
|
Two bool `xor:"one" required:""`
|
|
Three bool `xor:"two" required:""`
|
|
Four bool `required:""`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"--one"})
|
|
assert.EqualError(t, err, "missing flags: --four")
|
|
|
|
p = mustNew(t, &cli)
|
|
_, err = p.Parse([]string{"--two"})
|
|
assert.EqualError(t, err, "missing flags: --four, --one or --three")
|
|
|
|
p = mustNew(t, &cli)
|
|
_, err = p.Parse([]string{})
|
|
assert.EqualError(t, err, "missing flags: --four, --one or --three, --one or --two")
|
|
}
|
|
|
|
func TestAndRequired(t *testing.T) {
|
|
var cli struct {
|
|
One bool `and:"one,two" required:""`
|
|
Two bool `and:"one" required:""`
|
|
Three bool `and:"two"`
|
|
Four bool `required:""`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"--one", "--two", "--three"})
|
|
assert.EqualError(t, err, "missing flags: --four")
|
|
|
|
p = mustNew(t, &cli)
|
|
_, err = p.Parse([]string{"--four"})
|
|
assert.EqualError(t, err, "missing flags: --one and --three, --one and --two")
|
|
|
|
p = mustNew(t, &cli)
|
|
_, err = p.Parse([]string{})
|
|
assert.EqualError(t, err, "missing flags: --four, --one and --three, --one and --two")
|
|
}
|
|
|
|
func TestXorRequiredMany(t *testing.T) {
|
|
var cli struct {
|
|
One bool `xor:"one" required:""`
|
|
Two bool `xor:"one" required:""`
|
|
Three bool `xor:"one" required:""`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"--one"})
|
|
assert.NoError(t, err)
|
|
|
|
p = mustNew(t, &cli)
|
|
_, err = p.Parse([]string{"--three"})
|
|
assert.NoError(t, err)
|
|
|
|
p = mustNew(t, &cli)
|
|
_, err = p.Parse([]string{})
|
|
assert.EqualError(t, err, "missing flags: --one or --two or --three")
|
|
}
|
|
|
|
func TestAndRequiredMany(t *testing.T) {
|
|
var cli struct {
|
|
One bool `and:"one" required:""`
|
|
Two bool `and:"one" required:""`
|
|
Three bool `and:"one" required:""`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{})
|
|
assert.EqualError(t, err, "missing flags: --one and --two and --three")
|
|
|
|
p = mustNew(t, &cli)
|
|
_, err = p.Parse([]string{"--three"})
|
|
assert.EqualError(t, err, "missing flags: --one and --two")
|
|
}
|
|
|
|
func TestEnumSequence(t *testing.T) {
|
|
var cli struct {
|
|
State []string `enum:"a,b,c" default:"a"`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse(nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"a"}, cli.State)
|
|
}
|
|
|
|
func TestIssue40EnumAcrossCommands(t *testing.T) {
|
|
var cli struct {
|
|
One struct {
|
|
OneArg string `arg:"" required:""`
|
|
} `cmd:""`
|
|
Two struct {
|
|
TwoArg string `arg:"" enum:"a,b,c" required:"" env:"FOO"`
|
|
} `cmd:""`
|
|
Three struct {
|
|
ThreeArg string `arg:"" optional:"" default:"a" enum:"a,b,c"`
|
|
} `cmd:""`
|
|
}
|
|
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"one", "two"})
|
|
assert.NoError(t, err)
|
|
_, err = p.Parse([]string{"two", "d"})
|
|
assert.Error(t, err)
|
|
_, err = p.Parse([]string{"three", "d"})
|
|
assert.Error(t, err)
|
|
_, err = p.Parse([]string{"three", "c"})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestIssue179(t *testing.T) {
|
|
type A struct {
|
|
Enum string `required:"" enum:"1,2"`
|
|
}
|
|
|
|
type B struct{}
|
|
|
|
var root struct {
|
|
A A `cmd`
|
|
B B `cmd`
|
|
}
|
|
|
|
p := mustNew(t, &root)
|
|
_, err := p.Parse([]string{"b"})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestIssue153(t *testing.T) {
|
|
type LsCmd struct {
|
|
Paths []string `arg required name:"path" help:"Paths to list." env:"CMD_PATHS"`
|
|
}
|
|
|
|
var cli struct {
|
|
Debug bool `help:"Enable debug mode."`
|
|
|
|
Ls LsCmd `cmd help:"List paths."`
|
|
}
|
|
|
|
p := newEnvParser(t, &cli, envMap{
|
|
"CMD_PATHS": "hello",
|
|
})
|
|
_, err := p.Parse([]string{"ls"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"hello"}, cli.Ls.Paths)
|
|
}
|
|
|
|
func TestEnumArg(t *testing.T) {
|
|
var cli struct {
|
|
Nested struct {
|
|
One string `arg:"" enum:"a,b,c" required:""`
|
|
Two string `arg:""`
|
|
} `cmd:""`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"nested", "a", "b"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "a", cli.Nested.One)
|
|
assert.Equal(t, "b", cli.Nested.Two)
|
|
}
|
|
|
|
func TestDefaultCommand(t *testing.T) {
|
|
var cli struct {
|
|
One struct{} `cmd:"" default:"1"`
|
|
Two struct{} `cmd:""`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
ctx, err := p.Parse([]string{})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "one", ctx.Command())
|
|
}
|
|
|
|
func TestMultipleDefaultCommands(t *testing.T) {
|
|
var cli struct {
|
|
One struct{} `cmd:"" default:"1"`
|
|
Two struct{} `cmd:"" default:"1"`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.EqualError(t, err, "<anonymous struct>.Two: can't have more than one default command under <command>")
|
|
}
|
|
|
|
func TestDefaultCommandWithSubCommand(t *testing.T) {
|
|
var cli struct {
|
|
One struct {
|
|
Two struct{} `cmd:""`
|
|
} `cmd:"" default:"1"`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.EqualError(t, err, "<anonymous struct>.One: default command one <command> must not have subcommands or arguments")
|
|
}
|
|
|
|
func TestDefaultCommandWithAllowedSubCommand(t *testing.T) {
|
|
var cli struct {
|
|
One struct {
|
|
Two struct{} `cmd:""`
|
|
} `cmd:"" default:"withargs"`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
ctx, err := p.Parse([]string{"two"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "one two", ctx.Command())
|
|
}
|
|
|
|
func TestDefaultCommandWithArgument(t *testing.T) {
|
|
var cli struct {
|
|
One struct {
|
|
Arg string `arg:""`
|
|
} `cmd:"" default:"1"`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.EqualError(t, err, "<anonymous struct>.One: default command one <arg> must not have subcommands or arguments")
|
|
}
|
|
|
|
func TestDefaultCommandWithAllowedArgument(t *testing.T) {
|
|
var cli struct {
|
|
One struct {
|
|
Arg string `arg:""`
|
|
Flag string
|
|
} `cmd:"" default:"withargs"`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"arg", "--flag=value"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "arg", cli.One.Arg)
|
|
assert.Equal(t, "value", cli.One.Flag)
|
|
}
|
|
|
|
func TestDefaultCommandWithBranchingArgument(t *testing.T) {
|
|
var cli struct {
|
|
One struct {
|
|
Two struct {
|
|
Two string `arg:""`
|
|
} `arg:""`
|
|
} `cmd:"" default:"1"`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.EqualError(t, err, "<anonymous struct>.One: default command one <command> must not have subcommands or arguments")
|
|
}
|
|
|
|
func TestDefaultCommandWithAllowedBranchingArgument(t *testing.T) {
|
|
var cli struct {
|
|
One struct {
|
|
Two struct {
|
|
Two string `arg:""`
|
|
Flag string
|
|
} `arg:""`
|
|
} `cmd:"" default:"withargs"`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"arg", "--flag=value"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "arg", cli.One.Two.Two)
|
|
assert.Equal(t, "value", cli.One.Two.Flag)
|
|
}
|
|
|
|
func TestDefaultCommandPrecedence(t *testing.T) {
|
|
var cli struct {
|
|
Two struct {
|
|
Arg string `arg:""`
|
|
Flag bool
|
|
} `cmd:"" default:"withargs"`
|
|
One struct{} `cmd:""`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
|
|
// A named command should take precedence over a default command with arg
|
|
ctx, err := p.Parse([]string{"one"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "one", ctx.Command())
|
|
|
|
// An explicitly named command with arg should parse, even if labeled default:"witharg"
|
|
ctx, err = p.Parse([]string{"two", "arg"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "two <arg>", ctx.Command())
|
|
|
|
// An arg to a default command that does not match another command should select the default
|
|
ctx, err = p.Parse([]string{"arg"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "two <arg>", ctx.Command())
|
|
|
|
// A flag on a default command should not be valid on a sibling command
|
|
_, err = p.Parse([]string{"one", "--flag"})
|
|
assert.EqualError(t, err, "unknown flag --flag")
|
|
}
|
|
|
|
func TestLoneHpyhen(t *testing.T) {
|
|
var cli struct {
|
|
Flag string
|
|
Arg string `arg:"" optional:""`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
|
|
_, err := p.Parse([]string{"-"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "-", cli.Arg)
|
|
|
|
_, err = p.Parse([]string{"--flag", "-"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "-", cli.Flag)
|
|
}
|
|
|
|
func TestPlugins(t *testing.T) {
|
|
var pluginOne struct {
|
|
One string
|
|
}
|
|
var pluginTwo struct {
|
|
Two string
|
|
}
|
|
var cli struct {
|
|
Base string
|
|
kong.Plugins
|
|
}
|
|
cli.Plugins = kong.Plugins{&pluginOne, &pluginTwo}
|
|
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"--base=base", "--one=one", "--two=two"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "base", cli.Base)
|
|
assert.Equal(t, "one", pluginOne.One)
|
|
assert.Equal(t, "two", pluginTwo.Two)
|
|
}
|
|
|
|
type validateCmd struct{}
|
|
|
|
func (v *validateCmd) Validate() error { return errors.New("cmd error") }
|
|
|
|
type validateCli struct {
|
|
Cmd validateCmd `cmd:""`
|
|
}
|
|
|
|
func (v *validateCli) Validate() error { return errors.New("app error") }
|
|
|
|
type validateFlag string
|
|
|
|
func (v *validateFlag) Validate() error { return errors.New("flag error") }
|
|
|
|
func TestValidateApp(t *testing.T) {
|
|
cli := validateCli{}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{})
|
|
assert.EqualError(t, err, "app error")
|
|
}
|
|
|
|
func TestValidateCmd(t *testing.T) {
|
|
cli := struct {
|
|
Cmd validateCmd `cmd:""`
|
|
}{}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"cmd"})
|
|
assert.EqualError(t, err, "cmd: cmd error")
|
|
}
|
|
|
|
func TestValidateFlag(t *testing.T) {
|
|
cli := struct {
|
|
Flag validateFlag
|
|
}{}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"--flag=one"})
|
|
assert.EqualError(t, err, "--flag: flag error")
|
|
}
|
|
|
|
func TestValidateArg(t *testing.T) {
|
|
cli := struct {
|
|
Arg validateFlag `arg:""`
|
|
}{}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"one"})
|
|
assert.EqualError(t, err, "<arg>: flag error")
|
|
}
|
|
|
|
type extendedValidateFlag string
|
|
|
|
func (v *extendedValidateFlag) Validate(kctx *kong.Context) error { return errors.New("flag error") }
|
|
|
|
func TestExtendedValidateFlag(t *testing.T) {
|
|
cli := struct {
|
|
Flag extendedValidateFlag
|
|
}{}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"--flag=one"})
|
|
assert.EqualError(t, err, "--flag: flag error")
|
|
}
|
|
|
|
func TestPointers(t *testing.T) {
|
|
cli := struct {
|
|
Mapped *mappedValue
|
|
JSON *jsonUnmarshalerValue
|
|
}{}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"--mapped=mapped", "--json=\"foo\""})
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, cli.Mapped)
|
|
assert.Equal(t, "mapped", cli.Mapped.decoded)
|
|
assert.NotZero(t, cli.JSON)
|
|
assert.Equal(t, "FOO", string(*cli.JSON))
|
|
}
|
|
|
|
type dynamicCommand struct {
|
|
Flag string
|
|
|
|
ran bool
|
|
}
|
|
|
|
func (d *dynamicCommand) Run() error {
|
|
d.ran = true
|
|
return nil
|
|
}
|
|
|
|
type commandFunc func() error
|
|
|
|
func (cf commandFunc) Run() error {
|
|
return cf()
|
|
}
|
|
|
|
func TestDynamicCommands(t *testing.T) {
|
|
cli := struct {
|
|
One struct{} `cmd:"one"`
|
|
}{}
|
|
help := &strings.Builder{}
|
|
two := &dynamicCommand{}
|
|
three := &dynamicCommand{}
|
|
fourRan := false
|
|
four := commandFunc(func() error { fourRan = true; return nil })
|
|
p := mustNew(t, &cli,
|
|
kong.DynamicCommand("two", "", "", &two),
|
|
kong.DynamicCommand("three", "", "", three, "hidden"),
|
|
kong.DynamicCommand("four", "", "", &four),
|
|
kong.Writers(help, help),
|
|
kong.Exit(func(int) {}))
|
|
kctx, err := p.Parse([]string{"two", "--flag=flag"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "flag", two.Flag)
|
|
assert.False(t, two.ran)
|
|
err = kctx.Run()
|
|
assert.NoError(t, err)
|
|
assert.True(t, two.ran)
|
|
|
|
kctx, err = p.Parse([]string{"four"})
|
|
assert.NoError(t, err)
|
|
assert.False(t, fourRan)
|
|
err = kctx.Run()
|
|
assert.NoError(t, err)
|
|
assert.True(t, fourRan)
|
|
|
|
_, err = p.Parse([]string{"--help"})
|
|
assert.EqualError(t, err, `expected one of "one", "two", "four"`)
|
|
assert.NotContains(t, help.String(), "three", help.String())
|
|
}
|
|
|
|
func TestDuplicateShortflags(t *testing.T) {
|
|
cli := struct {
|
|
Flag1 bool `short:"t"`
|
|
Flag2 bool `short:"t"`
|
|
}{}
|
|
_, err := kong.New(&cli)
|
|
assert.EqualError(t, err, "<anonymous struct>.Flag2: duplicate short flag -t")
|
|
}
|
|
|
|
func TestDuplicateAliases(t *testing.T) {
|
|
cli1 := struct {
|
|
Flag1 string `aliases:"flag"`
|
|
Flag2 string `aliases:"flag"`
|
|
}{}
|
|
_, err := kong.New(&cli1)
|
|
assert.EqualError(t, err, "<anonymous struct>.Flag2: duplicate flag --flag")
|
|
}
|
|
|
|
func TestSubCommandAliases(t *testing.T) {
|
|
type SubC struct {
|
|
Flag1 string `aliases:"flag"`
|
|
}
|
|
|
|
cli1 := struct {
|
|
Sub1 SubC `cmd:"sub1"`
|
|
Sub2 SubC `cmd:"sub2"`
|
|
}{}
|
|
|
|
_, err := kong.New(&cli1)
|
|
assert.NoError(t, err, "dupe aliases shouldn't error if they're in separate sub commands")
|
|
}
|
|
|
|
func TestDuplicateAliasLong(t *testing.T) {
|
|
cli2 := struct {
|
|
Flag string ``
|
|
Flag2 string `aliases:"flag"` // duplicates Flag
|
|
}{}
|
|
_, err := kong.New(&cli2)
|
|
assert.EqualError(t, err, "<anonymous struct>.Flag2: duplicate flag --flag")
|
|
}
|
|
|
|
func TestDuplicateNestedShortFlags(t *testing.T) {
|
|
cli := struct {
|
|
Flag1 bool `short:"t"`
|
|
Cmd struct {
|
|
Flag2 bool `short:"t"`
|
|
} `cmd:""`
|
|
}{}
|
|
_, err := kong.New(&cli)
|
|
assert.EqualError(t, err, "<anonymous struct>.Flag2: duplicate short flag -t")
|
|
}
|
|
|
|
func TestHydratePointerCommandsAndEmbeds(t *testing.T) {
|
|
type cmd struct {
|
|
Flag bool
|
|
}
|
|
|
|
type embed struct {
|
|
Embed bool
|
|
}
|
|
|
|
var cli struct {
|
|
Cmd *cmd `cmd:""`
|
|
Embed *embed `embed:""`
|
|
}
|
|
|
|
k := mustNew(t, &cli)
|
|
_, err := k.Parse([]string{"--embed", "cmd", "--flag"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, &cmd{Flag: true}, cli.Cmd)
|
|
assert.Equal(t, &embed{Embed: true}, cli.Embed)
|
|
}
|
|
|
|
//nolint:revive
|
|
type testIgnoreFields struct {
|
|
Foo struct {
|
|
Bar bool
|
|
Sub struct {
|
|
SubFlag1 bool `kong:"name=subflag1"`
|
|
XXX_SubFlag2 bool `kong:"name=subflag2"` //nolint:stylecheck
|
|
} `kong:"cmd"`
|
|
} `kong:"cmd"`
|
|
XXX_Baz struct { //nolint:stylecheck
|
|
Boo bool
|
|
} `kong:"cmd,name=baz"`
|
|
}
|
|
|
|
func TestIgnoreRegex(t *testing.T) {
|
|
cli := testIgnoreFields{}
|
|
|
|
k, err := kong.New(&cli, kong.IgnoreFields(`.*\.XXX_.+`))
|
|
assert.NoError(t, err)
|
|
|
|
_, err = k.Parse([]string{"foo", "sub"})
|
|
assert.NoError(t, err)
|
|
|
|
_, err = k.Parse([]string{"foo", "sub", "--subflag1"})
|
|
assert.NoError(t, err)
|
|
|
|
_, err = k.Parse([]string{"foo", "sub", "--subflag2"})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "unknown flag --subflag2")
|
|
|
|
_, err = k.Parse([]string{"baz"})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "unexpected argument baz")
|
|
}
|
|
|
|
// Verify that passing a nil regex will work
|
|
func TestIgnoreRegexEmpty(t *testing.T) {
|
|
cli := testIgnoreFields{}
|
|
|
|
_, err := kong.New(&cli, kong.IgnoreFields(""))
|
|
assert.Error(t, err)
|
|
assert.Contains(t, "regex input cannot be empty", err.Error())
|
|
}
|
|
|
|
type optionWithErr struct{}
|
|
|
|
func (o *optionWithErr) Apply(k *kong.Kong) error {
|
|
return errors.New("option returned err")
|
|
}
|
|
|
|
func TestOptionReturnsErr(t *testing.T) {
|
|
cli := struct {
|
|
Test bool
|
|
}{}
|
|
|
|
optWithError := &optionWithErr{}
|
|
|
|
_, err := kong.New(cli, optWithError)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "option returned err", err.Error())
|
|
}
|
|
|
|
func TestEnumValidation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cli any
|
|
fail bool
|
|
}{
|
|
{
|
|
"Arg",
|
|
&struct {
|
|
Enum string `arg:"" enum:"one,two"`
|
|
}{},
|
|
false,
|
|
},
|
|
{
|
|
"RequiredArg",
|
|
&struct {
|
|
Enum string `required:"" arg:"" enum:"one,two"`
|
|
}{},
|
|
false,
|
|
},
|
|
{
|
|
"OptionalArg",
|
|
&struct {
|
|
Enum string `optional:"" arg:"" enum:"one,two"`
|
|
}{},
|
|
true,
|
|
},
|
|
{
|
|
"RepeatedArgs",
|
|
&struct {
|
|
Enum []string `arg:"" enum:"one,two"`
|
|
}{},
|
|
false,
|
|
},
|
|
{
|
|
"RequiredRepeatedArgs",
|
|
&struct {
|
|
Enum []string `required:"" arg:"" enum:"one,two"`
|
|
}{},
|
|
false,
|
|
},
|
|
{
|
|
"OptionalRepeatedArgs",
|
|
&struct {
|
|
Enum []string `optional:"" arg:"" enum:"one,two"`
|
|
}{},
|
|
false,
|
|
},
|
|
{
|
|
"EnumWithEmptyDefault",
|
|
&struct {
|
|
Flag string `enum:"one,two," default:""`
|
|
}{},
|
|
false,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, err := kong.New(test.cli)
|
|
if test.fail {
|
|
assert.Error(t, err, repr.String(test.cli))
|
|
} else {
|
|
assert.NoError(t, err, repr.String(test.cli))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPassthroughArgs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
flag string
|
|
cmdArgs []string
|
|
}{
|
|
{
|
|
"NoArgs",
|
|
[]string{},
|
|
"",
|
|
[]string(nil),
|
|
},
|
|
{
|
|
"RecognizedFlagAndArgs",
|
|
[]string{"--flag", "foobar", "something"},
|
|
"foobar",
|
|
[]string{"something"},
|
|
},
|
|
{
|
|
"DashDashBetweenArgs",
|
|
[]string{"foo", "--", "bar"},
|
|
"",
|
|
[]string{"foo", "--", "bar"},
|
|
},
|
|
{
|
|
"DashDash",
|
|
[]string{"--", "--flag", "foobar"},
|
|
"",
|
|
[]string{"--", "--flag", "foobar"},
|
|
},
|
|
{
|
|
"UnrecognizedFlagAndArgs",
|
|
[]string{"--unrecognized-flag", "something"},
|
|
"",
|
|
[]string{"--unrecognized-flag", "something"},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
var cli struct {
|
|
Flag string
|
|
Args []string `arg:"" optional:"" passthrough:""`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse(test.args)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, test.flag, cli.Flag)
|
|
assert.Equal(t, test.cmdArgs, cli.Args)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPassthroughPartial(t *testing.T) {
|
|
var cli struct {
|
|
Flag string
|
|
Args []string `arg:"" optional:"" passthrough:"partial"`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"--flag", "foobar", "something"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "foobar", cli.Flag)
|
|
assert.Equal(t, []string{"something"}, cli.Args)
|
|
_, err = p.Parse([]string{"--invalid", "foobar", "something"})
|
|
assert.EqualError(t, err, "unknown flag --invalid")
|
|
}
|
|
|
|
func TestPassthroughAll(t *testing.T) {
|
|
var cli struct {
|
|
Flag string
|
|
Args []string `arg:"" optional:"" passthrough:"all"`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse([]string{"--flag", "foobar", "something"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "foobar", cli.Flag)
|
|
assert.Equal(t, []string{"something"}, cli.Args)
|
|
_, err = p.Parse([]string{"--invalid", "foobar", "something"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"--invalid", "foobar", "something"}, cli.Args)
|
|
}
|
|
|
|
func TestPassthroughCmd(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
flag string
|
|
cmdArgs []string
|
|
}{
|
|
{
|
|
"Simple",
|
|
[]string{"--flag", "foobar", "command", "something"},
|
|
"foobar",
|
|
[]string{"something"},
|
|
},
|
|
{
|
|
"DashDash",
|
|
[]string{"--flag", "foobar", "command", "--", "something"},
|
|
"foobar",
|
|
[]string{"--", "something"},
|
|
},
|
|
{
|
|
"Flag",
|
|
[]string{"command", "--flag", "foobar"},
|
|
"",
|
|
[]string{"--flag", "foobar"},
|
|
},
|
|
{
|
|
"FlagAndFlag",
|
|
[]string{"--flag", "foobar", "command", "--flag", "foobar"},
|
|
"foobar",
|
|
[]string{"--flag", "foobar"},
|
|
},
|
|
{
|
|
"NoArgs",
|
|
[]string{"--flag", "foobar", "command"},
|
|
"foobar",
|
|
[]string(nil),
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
var cli struct {
|
|
Flag string
|
|
Command struct {
|
|
Args []string `arg:"" optional:""`
|
|
} `cmd:"" passthrough:""`
|
|
}
|
|
p := mustNew(t, &cli)
|
|
_, err := p.Parse(test.args)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, test.flag, cli.Flag)
|
|
assert.Equal(t, test.cmdArgs, cli.Command.Args)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPassthroughCmdOnlyArgs(t *testing.T) {
|
|
var cli struct {
|
|
Command struct {
|
|
Flag string
|
|
Args []string `arg:"" optional:""`
|
|
} `cmd:"" passthrough:""`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.EqualError(t, err, "<anonymous struct>.Command: passthrough command command [<args> ...] [flags] must not have subcommands or flags")
|
|
}
|
|
|
|
func TestPassthroughCmdOnlyStringArgs(t *testing.T) {
|
|
var cli struct {
|
|
Command struct {
|
|
Args []int `arg:"" optional:""`
|
|
} `cmd:"" passthrough:""`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.EqualError(t, err, "<anonymous struct>.Command: passthrough command command [<args> ...] must contain exactly one positional argument of []string type")
|
|
}
|
|
|
|
func TestHelpShouldStillWork(t *testing.T) {
|
|
type CLI struct {
|
|
Dir string `type:"existingdir" default:"missing-dir"`
|
|
File string `type:"existingfile" default:"testdata/missing.txt"`
|
|
}
|
|
var cli CLI
|
|
w := &strings.Builder{}
|
|
k := mustNew(t, &cli, kong.Writers(w, w))
|
|
rc := -1 // init nonzero to help assert help hook was called
|
|
k.Exit = func(i int) {
|
|
rc = i
|
|
}
|
|
_, err := k.Parse([]string{"--help"})
|
|
t.Log(w.String())
|
|
// checking return code validates the help hook was called
|
|
assert.Zero(t, rc)
|
|
// allow for error propagation from other validation (only for the
|
|
// sake of this test, due to the exit function not actually exiting the
|
|
// program; errors will not propagate in the real world).
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestVersionFlagShouldStillWork(t *testing.T) {
|
|
type CLI struct {
|
|
Dir string `type:"existingdir" default:"missing-dir"`
|
|
File string `type:"existingfile" default:"testdata/missing.txt"`
|
|
Version kong.VersionFlag
|
|
}
|
|
var cli CLI
|
|
w := &strings.Builder{}
|
|
k := mustNew(t, &cli, kong.Writers(w, w))
|
|
rc := -1 // init nonzero to help assert help hook was called
|
|
k.Exit = func(i int) {
|
|
rc = i
|
|
}
|
|
_, err := k.Parse([]string{"--version"})
|
|
t.Log(w.String())
|
|
// checking return code validates the help hook was called
|
|
assert.Zero(t, rc)
|
|
// allow for error propagation from other validation (only for the
|
|
// sake of this test, due to the exit function not actually exiting the
|
|
// program; errors will not propagate in the real world).
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestSliceDecoderHelpfulErrorMsg(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cli any
|
|
args []string
|
|
err string
|
|
}{
|
|
{
|
|
"DefaultRune",
|
|
&struct {
|
|
Stuff []string
|
|
}{},
|
|
[]string{"--stuff"},
|
|
`--stuff: missing value, expecting "<arg>,..."`,
|
|
},
|
|
{
|
|
"SpecifiedRune",
|
|
&struct {
|
|
Stuff []string `sep:","`
|
|
}{},
|
|
[]string{"--stuff"},
|
|
`--stuff: missing value, expecting "<arg>,..."`,
|
|
},
|
|
{
|
|
"SpaceRune",
|
|
&struct {
|
|
Stuff []string `sep:" "`
|
|
}{},
|
|
[]string{"--stuff"},
|
|
`--stuff: missing value, expecting "<arg> ..."`,
|
|
},
|
|
{
|
|
"OtherRune",
|
|
&struct {
|
|
Stuff []string `sep:"_"`
|
|
}{},
|
|
[]string{"--stuff"},
|
|
`--stuff: missing value, expecting "<arg>_..."`,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
p := mustNew(t, test.cli)
|
|
_, err := p.Parse(test.args)
|
|
assert.EqualError(t, err, test.err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMapDecoderHelpfulErrorMsg(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cli any
|
|
args []string
|
|
expected string
|
|
}{
|
|
{
|
|
"DefaultRune",
|
|
&struct {
|
|
Stuff map[string]int
|
|
}{},
|
|
[]string{"--stuff"},
|
|
`--stuff: missing value, expecting "<key>=<value>;..."`,
|
|
},
|
|
{
|
|
"SpecifiedRune",
|
|
&struct {
|
|
Stuff map[string]int `mapsep:";"`
|
|
}{},
|
|
[]string{"--stuff"},
|
|
`--stuff: missing value, expecting "<key>=<value>;..."`,
|
|
},
|
|
{
|
|
"SpaceRune",
|
|
&struct {
|
|
Stuff map[string]int `mapsep:" "`
|
|
}{},
|
|
[]string{"--stuff"},
|
|
`--stuff: missing value, expecting "<key>=<value> ..."`,
|
|
},
|
|
{
|
|
"OtherRune",
|
|
&struct {
|
|
Stuff map[string]int `mapsep:","`
|
|
}{},
|
|
[]string{"--stuff"},
|
|
`--stuff: missing value, expecting "<key>=<value>,..."`,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
p := mustNew(t, test.cli)
|
|
_, err := p.Parse(test.args)
|
|
assert.EqualError(t, err, test.expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDuplicateName(t *testing.T) {
|
|
var cli struct {
|
|
DupA struct{} `cmd:"" name:"duplicate"`
|
|
DupB struct{} `cmd:"" name:"duplicate"`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestDuplicateChildName(t *testing.T) {
|
|
var cli struct {
|
|
A struct {
|
|
DupA struct{} `cmd:"" name:"duplicate"`
|
|
DupB struct{} `cmd:"" name:"duplicate"`
|
|
} `cmd:""`
|
|
B struct{} `cmd:""`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestChildNameCanBeDuplicated(t *testing.T) {
|
|
var cli struct {
|
|
A struct {
|
|
A struct{} `cmd:"" name:"duplicateA"`
|
|
B struct{} `cmd:"" name:"duplicateB"`
|
|
} `cmd:"" name:"duplicateA"`
|
|
B struct{} `cmd:"" name:"duplicateB"`
|
|
}
|
|
mustNew(t, &cli)
|
|
}
|
|
|
|
func TestCumulativeArgumentLast(t *testing.T) {
|
|
var cli struct {
|
|
Arg1 string `arg:""`
|
|
Arg2 []string `arg:""`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestCumulativeArgumentNotLast(t *testing.T) {
|
|
var cli struct {
|
|
Arg2 []string `arg:""`
|
|
Arg1 string `arg:""`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestStringPointer(t *testing.T) {
|
|
var cli struct {
|
|
Foo *string
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, k)
|
|
ctx, err := k.Parse([]string{"--foo", "wtf"})
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, ctx)
|
|
assert.NotZero(t, cli.Foo)
|
|
assert.Equal(t, "wtf", *cli.Foo)
|
|
}
|
|
|
|
func TestStringPointerNoValue(t *testing.T) {
|
|
var cli struct {
|
|
Foo *string
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, k)
|
|
ctx, err := k.Parse([]string{})
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, ctx)
|
|
assert.Zero(t, cli.Foo)
|
|
}
|
|
|
|
func TestStringPointerDefault(t *testing.T) {
|
|
var cli struct {
|
|
Foo *string `default:"stuff"`
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, k)
|
|
ctx, err := k.Parse([]string{})
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, ctx)
|
|
assert.NotZero(t, cli.Foo)
|
|
assert.Equal(t, "stuff", *cli.Foo)
|
|
}
|
|
|
|
func TestStringPointerAliasNoValue(t *testing.T) {
|
|
type Foo string
|
|
var cli struct {
|
|
F *Foo
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, k)
|
|
ctx, err := k.Parse([]string{})
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, ctx)
|
|
assert.Zero(t, cli.F)
|
|
}
|
|
|
|
func TestStringPointerAlias(t *testing.T) {
|
|
type Foo string
|
|
var cli struct {
|
|
F *Foo
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, k)
|
|
ctx, err := k.Parse([]string{"--f=value"})
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, ctx)
|
|
assert.NotZero(t, cli.F)
|
|
assert.Equal(t, Foo("value"), *cli.F)
|
|
}
|
|
|
|
func TestStringPointerEmptyValue(t *testing.T) {
|
|
var cli struct {
|
|
F *string
|
|
G *string
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, k)
|
|
ctx, err := k.Parse([]string{"--f", "", "--g="})
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, ctx)
|
|
assert.NotZero(t, cli.F)
|
|
assert.NotZero(t, cli.G)
|
|
assert.Equal(t, "", *cli.F)
|
|
assert.Equal(t, "", *cli.G)
|
|
}
|
|
|
|
func TestIntPtr(t *testing.T) {
|
|
var cli struct {
|
|
F *int
|
|
G *int
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, k)
|
|
ctx, err := k.Parse([]string{"--f=6"})
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, ctx)
|
|
assert.NotZero(t, cli.F)
|
|
assert.Zero(t, cli.G)
|
|
assert.Equal(t, 6, *cli.F)
|
|
}
|
|
|
|
func TestBoolPtr(t *testing.T) {
|
|
var cli struct {
|
|
X *bool
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, k)
|
|
ctx, err := k.Parse([]string{"--x"})
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, ctx)
|
|
assert.NotZero(t, cli.X)
|
|
assert.Equal(t, true, *cli.X)
|
|
}
|
|
|
|
func TestBoolPtrFalse(t *testing.T) {
|
|
var cli struct {
|
|
X *bool
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, k)
|
|
ctx, err := k.Parse([]string{"--x=false"})
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, ctx)
|
|
assert.NotZero(t, cli.X)
|
|
assert.Equal(t, false, *cli.X)
|
|
}
|
|
|
|
func TestBoolPtrNegated(t *testing.T) {
|
|
var cli struct {
|
|
X *bool `negatable:""`
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, k)
|
|
ctx, err := k.Parse([]string{"--no-x"})
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, ctx)
|
|
assert.NotZero(t, cli.X)
|
|
assert.Equal(t, false, *cli.X)
|
|
}
|
|
|
|
func TestNilNegatableBoolPtr(t *testing.T) {
|
|
var cli struct {
|
|
X *bool `negatable:""`
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, k)
|
|
ctx, err := k.Parse([]string{})
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, ctx)
|
|
assert.Zero(t, cli.X)
|
|
}
|
|
|
|
func TestBoolPtrNil(t *testing.T) {
|
|
var cli struct {
|
|
X *bool
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, k)
|
|
ctx, err := k.Parse([]string{})
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, ctx)
|
|
assert.Zero(t, cli.X)
|
|
}
|
|
|
|
func TestUnsupportedPtr(t *testing.T) {
|
|
type Foo struct {
|
|
x int //nolint
|
|
y int //nolint
|
|
}
|
|
|
|
var cli struct {
|
|
F *Foo
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, k)
|
|
ctx, err := k.Parse([]string{"--f=whatever"})
|
|
assert.Zero(t, ctx)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "--f: cannot find mapper for kong_test.Foo", err.Error())
|
|
}
|
|
|
|
func TestEnumPtr(t *testing.T) {
|
|
var cli struct {
|
|
X *string `enum:"A,B,C" default:"C"`
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, k)
|
|
ctx, err := k.Parse([]string{"--x=A"})
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, ctx)
|
|
assert.NotZero(t, cli.X)
|
|
assert.Equal(t, "A", *cli.X)
|
|
}
|
|
|
|
func TestEnumPtrOmitted(t *testing.T) {
|
|
var cli struct {
|
|
X *string `enum:"A,B,C" default:"C"`
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, k)
|
|
ctx, err := k.Parse([]string{})
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, ctx)
|
|
assert.NotZero(t, cli.X)
|
|
assert.Equal(t, "C", *cli.X)
|
|
}
|
|
|
|
func TestEnumPtrOmittedNoDefault(t *testing.T) {
|
|
var cli struct {
|
|
X *string `enum:"A,B,C"`
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, k)
|
|
ctx, err := k.Parse([]string{})
|
|
assert.NoError(t, err)
|
|
assert.NotZero(t, ctx)
|
|
assert.Zero(t, cli.X)
|
|
}
|
|
|
|
func TestIntEnum(t *testing.T) {
|
|
var cli struct {
|
|
Enum int `enum:"1,2,3" default:"1"`
|
|
}
|
|
k, err := kong.New(&cli)
|
|
assert.NoError(t, err)
|
|
_, err = k.Parse([]string{"--enum=123"})
|
|
assert.EqualError(t, err, `--enum must be one of "1","2","3" but got "123"`)
|
|
}
|
|
|
|
func TestRecursiveVariableExpansion(t *testing.T) {
|
|
var cli struct {
|
|
Config string `type:"path" default:"${config_file}" help:"Default: ${default}"`
|
|
}
|
|
k := mustNew(t, &cli, kong.Vars{"config_file": "/etc/config"}, kong.Exit(func(int) {}))
|
|
w := &strings.Builder{}
|
|
k.Stderr = w
|
|
k.Stdout = w
|
|
_, err := k.Parse([]string{"--help"})
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, w.String(), "Default: /etc/config")
|
|
}
|
|
|
|
type afterRunCLI struct {
|
|
runCalled bool `kong:"-"`
|
|
afterRunCalled bool `kong:"-"`
|
|
}
|
|
|
|
func (c *afterRunCLI) Run() error {
|
|
c.runCalled = true
|
|
return nil
|
|
}
|
|
|
|
func (c *afterRunCLI) AfterRun() error {
|
|
c.afterRunCalled = true
|
|
return nil
|
|
}
|
|
|
|
func TestAfterRun(t *testing.T) {
|
|
var cli afterRunCLI
|
|
k := mustNew(t, &cli)
|
|
kctx, err := k.Parse([]string{})
|
|
assert.NoError(t, err)
|
|
err = kctx.Run()
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, afterRunCLI{runCalled: true, afterRunCalled: true}, cli)
|
|
}
|
|
|
|
type ProvidedString string
|
|
|
|
type providerCLI struct {
|
|
Sub providerSubCommand `cmd:""`
|
|
}
|
|
|
|
type providerSubCommand struct{}
|
|
|
|
func (p *providerCLI) ProvideFoo() (ProvidedString, error) {
|
|
return ProvidedString("foo"), nil
|
|
}
|
|
|
|
func (p *providerSubCommand) Run(t *testing.T, ps ProvidedString) error {
|
|
assert.Equal(t, ProvidedString("foo"), ps)
|
|
return nil
|
|
}
|
|
|
|
func TestProviderMethods(t *testing.T) {
|
|
k := mustNew(t, &providerCLI{})
|
|
kctx, err := k.Parse([]string{"sub"})
|
|
assert.NoError(t, err)
|
|
err = kctx.Run(t)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
type EmbeddedCallback struct {
|
|
Nested NestedCallback `embed:""`
|
|
|
|
Embedded bool
|
|
}
|
|
|
|
func (e *EmbeddedCallback) AfterApply() error {
|
|
e.Embedded = true
|
|
return nil
|
|
}
|
|
|
|
type taggedEmbeddedCallback struct {
|
|
NestedCallback
|
|
|
|
Tagged bool
|
|
}
|
|
|
|
func (e *taggedEmbeddedCallback) AfterApply() error {
|
|
e.Tagged = true
|
|
return nil
|
|
}
|
|
|
|
type NestedCallback struct {
|
|
nested bool
|
|
}
|
|
|
|
func (n *NestedCallback) AfterApply() error {
|
|
n.nested = true
|
|
return nil
|
|
}
|
|
|
|
type EmbeddedRoot struct {
|
|
EmbeddedCallback
|
|
Tagged taggedEmbeddedCallback `embed:""`
|
|
Root bool
|
|
}
|
|
|
|
func (e *EmbeddedRoot) AfterApply() error {
|
|
e.Root = true
|
|
return nil
|
|
}
|
|
|
|
func TestEmbeddedCallbacks(t *testing.T) {
|
|
actual := &EmbeddedRoot{}
|
|
k := mustNew(t, actual)
|
|
_, err := k.Parse(nil)
|
|
assert.NoError(t, err)
|
|
expected := &EmbeddedRoot{
|
|
EmbeddedCallback: EmbeddedCallback{
|
|
Embedded: true,
|
|
Nested: NestedCallback{
|
|
nested: true,
|
|
},
|
|
},
|
|
Tagged: taggedEmbeddedCallback{
|
|
Tagged: true,
|
|
NestedCallback: NestedCallback{
|
|
nested: true,
|
|
},
|
|
},
|
|
Root: true,
|
|
}
|
|
assert.Equal(t, expected, actual)
|
|
}
|
|
|
|
type applyCalledOnce struct {
|
|
Dev bool
|
|
}
|
|
|
|
func (c *applyCalledOnce) AfterApply() error {
|
|
c.Dev = false
|
|
return nil
|
|
}
|
|
|
|
func (c applyCalledOnce) Run() error {
|
|
if c.Dev {
|
|
return fmt.Errorf("--dev should not be set")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestApplyCalledOnce(t *testing.T) {
|
|
cli := &applyCalledOnce{}
|
|
kctx, err := mustNew(t, cli).Parse([]string{"--dev"})
|
|
assert.NoError(t, err)
|
|
err = kctx.Run()
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestCustomTypeNoEllipsis(t *testing.T) {
|
|
type CLI struct {
|
|
Flag []byte `type:"existingfile"`
|
|
}
|
|
var cli CLI
|
|
p := mustNew(t, &cli, kong.Exit(func(int) {}))
|
|
w := &strings.Builder{}
|
|
p.Stderr = w
|
|
p.Stdout = w
|
|
_, err := p.Parse([]string{"--help"})
|
|
assert.NoError(t, err)
|
|
help := w.String()
|
|
assert.NotContains(t, help, "...")
|
|
}
|
|
|
|
func TestPrefixXorIssue343(t *testing.T) {
|
|
type DBConfig struct {
|
|
Password string `help:"Password" xor:"password" optional:""`
|
|
PasswordFile string `help:"File which content will be used for a password" xor:"password" optional:""`
|
|
PasswordCommand string `help:"Command to run to retrieve password" xor:"password" optional:""`
|
|
}
|
|
|
|
type SourceTargetConfig struct {
|
|
Source DBConfig `help:"Database config of source to be copied from" prefix:"source-" xorprefix:"source-" embed:""`
|
|
Target DBConfig `help:"Database config of source to be copied from" prefix:"target-" xorprefix:"target-" embed:""`
|
|
}
|
|
|
|
cli := SourceTargetConfig{}
|
|
kctx := mustNew(t, &cli)
|
|
_, err := kctx.Parse([]string{"--source-password=foo", "--target-password=bar"})
|
|
assert.NoError(t, err)
|
|
_, err = kctx.Parse([]string{"--source-password-file=foo", "--source-password=bar"})
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestIssue483EmptyRootNodeNoRun(t *testing.T) {
|
|
var emptyCLI struct{}
|
|
parser, err := kong.New(&emptyCLI)
|
|
assert.NoError(t, err)
|
|
|
|
kctx, err := parser.Parse([]string{})
|
|
assert.NoError(t, err)
|
|
|
|
err = kctx.Run()
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "no command selected")
|
|
}
|
|
|
|
type providerWithoutErrorCLI struct {
|
|
}
|
|
|
|
func (p *providerWithoutErrorCLI) Run(name string) error {
|
|
if name == "Bob" {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("name %s is not Bob", name)
|
|
}
|
|
|
|
func TestProviderWithoutError(t *testing.T) {
|
|
k := mustNew(t, &providerWithoutErrorCLI{})
|
|
kctx, err := k.Parse(nil)
|
|
assert.NoError(t, err)
|
|
err = kctx.BindToProvider(func() string { return "Bob" })
|
|
assert.NoError(t, err)
|
|
err = kctx.Run()
|
|
assert.NoError(t, err)
|
|
}
|