Merge remote-tracking branch 'upstream/master'

This commit is contained in:
S.Solodyagin
2025-07-02 20:54:46 +03:00
44 changed files with 1927 additions and 574 deletions
+727 -50
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"fmt"
"sort"
"strings"
"testing"
@@ -13,7 +14,7 @@ import (
"git.company.lan/gopkg/kong"
)
func mustNew(t *testing.T, cli interface{}, options ...kong.Option) *kong.Kong {
func mustNew(t *testing.T, cli any, options ...kong.Option) *kong.Kong {
t.Helper()
options = append([]kong.Option{
kong.Name("test"),
@@ -47,6 +48,25 @@ func TestPositionalArguments(t *testing.T) {
})
}
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>
@@ -356,8 +376,9 @@ func TestTraceErrorPartiallySucceeds(t *testing.T) {
}
type commandWithNegatableFlag struct {
Flag bool `kong:"default='true',negatable"`
ran bool
Flag bool `kong:"default='true',negatable"`
Custom bool `kong:"default='true',negatable='standard'"`
ran bool
}
func (c *commandWithNegatableFlag) Run() error {
@@ -367,34 +388,64 @@ func (c *commandWithNegatableFlag) Run() error {
func TestNegatableFlag(t *testing.T) {
tests := []struct {
name string
args []string
expected bool
name string
args []string
expectedFlag bool
expectedCustom bool
}{
{
name: "no flag",
args: []string{"cmd"},
expected: true,
name: "no flag",
args: []string{"cmd"},
expectedFlag: true,
expectedCustom: true,
},
{
name: "boolean flag",
args: []string{"cmd", "--flag"},
expected: true,
name: "boolean flag",
args: []string{"cmd", "--flag"},
expectedFlag: true,
expectedCustom: true,
},
{
name: "inverted boolean flag",
args: []string{"cmd", "--flag=false"},
expected: false,
name: "custom boolean flag",
args: []string{"cmd", "--custom"},
expectedFlag: true,
expectedCustom: true,
},
{
name: "negated boolean flag",
args: []string{"cmd", "--no-flag"},
expected: false,
name: "inverted boolean flag",
args: []string{"cmd", "--flag=false"},
expectedFlag: false,
expectedCustom: true,
},
{
name: "inverted negated boolean flag",
args: []string{"cmd", "--no-flag=false"},
expected: 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 {
@@ -407,16 +458,47 @@ func TestNegatableFlag(t *testing.T) {
p := mustNew(t, &cli)
kctx, err := p.Parse(tt.args)
assert.NoError(t, err)
assert.Equal(t, tt.expected, cli.Cmd.Flag)
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.expected, cli.Cmd.Flag)
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 {
@@ -506,6 +588,65 @@ func TestHooks(t *testing.T) {
}
}
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"`
@@ -593,11 +734,24 @@ func TestSliceWithDisabledSeparator(t *testing.T) {
}
func TestMultilineMessage(t *testing.T) {
w := &bytes.Buffer{}
var cli struct{}
p := mustNew(t, &cli, kong.Writers(w, w))
p.Printf("hello\nworld")
assert.Equal(t, "test: hello\n world\n", w.String())
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 {
@@ -671,8 +825,8 @@ func TestPassesThroughOriginalCommandError(t *testing.T) {
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}"`
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)
@@ -692,7 +846,9 @@ func TestInterpolationIntoModel(t *testing.T) {
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)
}
@@ -780,6 +936,43 @@ func TestExcludedField(t *testing.T) {
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
@@ -850,15 +1043,6 @@ func TestParentBindings(t *testing.T) {
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:"-"`
@@ -879,14 +1063,12 @@ func TestDefaultEnumValidated(t *testing.T) {
}
func TestEnvarEnumValidated(t *testing.T) {
restore := tempEnv(map[string]string{
"FLAG": "invalid",
})
defer restore()
var cli struct {
Flag string `env:"FLAG" required:"" enum:"valid"`
}
p := mustNew(t, &cli)
p := newEnvParser(t, &cli, envMap{
"FLAG": "invalid",
})
_, err := p.Parse(nil)
assert.EqualError(t, err, "--flag must be one of \"valid\" but got \"invalid\"")
}
@@ -906,6 +1088,21 @@ func TestXor(t *testing.T) {
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"`
@@ -923,6 +1120,23 @@ func TestXorChild(t *testing.T) {
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"`
@@ -939,6 +1153,57 @@ func TestMultiXor(t *testing.T) {
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:""`
@@ -959,6 +1224,26 @@ func TestXorRequired(t *testing.T) {
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:""`
@@ -978,6 +1263,21 @@ func TestXorRequiredMany(t *testing.T) {
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"`
@@ -1040,10 +1340,9 @@ func TestIssue153(t *testing.T) {
Ls LsCmd `cmd help:"List paths."`
}
p, revert := newEnvParser(t, &cli, envMap{
p := newEnvParser(t, &cli, envMap{
"CMD_PATHS": "hello",
})
defer revert()
_, err := p.Parse([]string{"ls"})
assert.NoError(t, err)
assert.Equal(t, []string{"hello"}, cli.Ls.Paths)
@@ -1272,6 +1571,19 @@ func TestValidateArg(t *testing.T) {
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
@@ -1297,6 +1609,12 @@ func (d *dynamicCommand) Run() error {
return nil
}
type commandFunc func() error
func (cf commandFunc) Run() error {
return cf()
}
func TestDynamicCommands(t *testing.T) {
cli := struct {
One struct{} `cmd:"one"`
@@ -1304,9 +1622,12 @@ func TestDynamicCommands(t *testing.T) {
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"})
@@ -1317,8 +1638,15 @@ func TestDynamicCommands(t *testing.T) {
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"`)
assert.EqualError(t, err, `expected one of "one", "two", "four"`)
assert.NotContains(t, help.String(), "three", help.String())
}
@@ -1460,7 +1788,7 @@ func TestOptionReturnsErr(t *testing.T) {
func TestEnumValidation(t *testing.T) {
tests := []struct {
name string
cli interface{}
cli any
fail bool
}{
{
@@ -1526,6 +1854,89 @@ func TestEnumValidation(t *testing.T) {
}
}
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
@@ -1651,7 +2062,7 @@ func TestVersionFlagShouldStillWork(t *testing.T) {
func TestSliceDecoderHelpfulErrorMsg(t *testing.T) {
tests := []struct {
name string
cli interface{}
cli any
args []string
err string
}{
@@ -1701,7 +2112,7 @@ func TestSliceDecoderHelpfulErrorMsg(t *testing.T) {
func TestMapDecoderHelpfulErrorMsg(t *testing.T) {
tests := []struct {
name string
cli interface{}
cli any
args []string
expected string
}{
@@ -2027,3 +2438,269 @@ func TestEnumPtrOmittedNoDefault(t *testing.T) {
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)
})
}