Files
kong/kong_test.go
2025-07-02 20:54:46 +03:00

2707 lines
62 KiB
Go

package kong_test
import (
"bytes"
"errors"
"fmt"
"sort"
"strings"
"testing"
"github.com/alecthomas/assert/v2"
"github.com/alecthomas/repr"
"git.company.lan/gopkg/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 TestRemainderReturnsUnparsedArgs(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)
args := []string{"user", "create", "10", "Alec", "Thomas"}
ctx, err := p.Parse(args)
assert.NoError(t, err)
for i, x := range ctx.Path {
assert.Equal(t, strings.Join(args[i:], " "), strings.Join(x.Remainder(), " "))
}
}
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 TestGlobalHooks(t *testing.T) {
var cli struct {
One struct {
Two string `kong:"arg,optional"`
Three string
} `cmd:""`
}
called := []string{}
log := func(name string) any {
return func(value *kong.Path) error {
switch {
case value.App != nil:
called = append(called, fmt.Sprintf("%s (app)", name))
case value.Positional != nil:
called = append(called, fmt.Sprintf("%s (arg) %s", name, value.Positional.Name))
case value.Flag != nil:
called = append(called, fmt.Sprintf("%s (flag) %s", name, value.Flag.Name))
case value.Argument != nil:
called = append(called, fmt.Sprintf("%s (arg) %s", name, value.Argument.Name))
case value.Command != nil:
called = append(called, fmt.Sprintf("%s (cmd) %s", name, value.Command.Name))
}
return nil
}
}
p := mustNew(t, &cli,
kong.WithBeforeReset(log("BeforeReset")),
kong.WithBeforeResolve(log("BeforeResolve")),
kong.WithBeforeApply(log("BeforeApply")),
kong.WithAfterApply(log("AfterApply")),
)
_, err := p.Parse([]string{"one", "two", "--three=THREE"})
assert.NoError(t, err)
assert.Equal(t, []string{
"BeforeReset (app)",
"BeforeReset (cmd) one",
"BeforeReset (arg) two",
"BeforeReset (flag) three",
"BeforeResolve (app)",
"BeforeResolve (cmd) one",
"BeforeResolve (arg) two",
"BeforeResolve (flag) three",
"BeforeApply (app)",
"BeforeApply (cmd) one",
"BeforeApply (arg) two",
"BeforeApply (flag) three",
"AfterApply (app)",
"AfterApply (cmd) one",
"AfterApply (arg) two",
"AfterApply (flag) three",
}, called)
}
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}" placeholder:"${enum}"`
EnumRef string `enum:"a,b" required:"" help:"One of ${enum}" placeholder:"${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, "a,b,c,d", flag.PlaceHolder)
assert.Equal(t, "One of a,b", flag2.Help)
assert.Equal(t, "a,b", flag2.PlaceHolder)
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 TestExcludeEmbeddedField(t *testing.T) {
type Embedded struct {
Flag string
Excluded string
}
type Embedded2 struct {
Flag2 string
Excluded string
}
var cli struct {
Embedded
Excluded string `kong:"-"`
Embedded2
}
var cli2 struct {
Embedded Embedded `kong:"embed"`
Excluded string `kong:"-"`
Embedded2 Embedded2 `kong:"embed"`
}
p := mustNew(t, &cli)
_, err := p.Parse([]string{"--flag=foo"})
assert.NoError(t, err)
_, err = p.Parse([]string{"--flag-2=foo"})
assert.NoError(t, err)
_, err = p.Parse([]string{"--excluded=foo"})
assert.Error(t, err)
p = mustNew(t, &cli2)
_, err = p.Parse([]string{"--flag=foo"})
assert.NoError(t, err)
_, err = p.Parse([]string{"--flag-2=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 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)
}
func TestParseHyphenParameter(t *testing.T) {
type shortFlag struct {
Flag string `short:"f"`
Other string `short:"o"`
Numeric int `short:"n"`
}
t.Run("ShortFlag", func(t *testing.T) {
actual := &shortFlag{}
_, err := mustNew(t, actual, kong.WithHyphenPrefixedParameters(true)).Parse([]string{"-f", "-foo"})
assert.NoError(t, err)
assert.Equal(t, &shortFlag{Flag: "-foo"}, actual)
})
t.Run("LongFlag", func(t *testing.T) {
actual := &shortFlag{}
_, err := mustNew(t, actual, kong.WithHyphenPrefixedParameters(true)).Parse([]string{"--flag", "-foo"})
assert.NoError(t, err)
assert.Equal(t, &shortFlag{Flag: "-foo"}, actual)
})
t.Run("ParamMatchesFlag", func(t *testing.T) {
actual := &shortFlag{}
_, err := mustNew(t, actual, kong.WithHyphenPrefixedParameters(true)).Parse([]string{"--flag", "-oo"})
assert.NoError(t, err)
assert.Equal(t, &shortFlag{Flag: "-oo"}, actual)
})
t.Run("NegativeNumber", func(t *testing.T) {
actual := &shortFlag{}
_, err := mustNew(t, actual, kong.WithHyphenPrefixedParameters(true)).Parse([]string{"--numeric", "-10"})
assert.NoError(t, err)
assert.Equal(t, &shortFlag{Numeric: -10}, actual)
})
}