6408010083
Previously, there was a confusing mix of functionality shared between
the two wherein you would need to use the Kong type for printing errors,
etc. but it did not have access to the context in order to print
context-sensitive usage information. This has been fixed.
Additionally, there are now fuzzy correction suggestions for flags and
commands
Also added a server example which shows how Kong can be used for parsing
in interactive shells. Run with:
$ go run ./_examples/server/*.go
Then interact with:
$ ssh -p 6740 127.0.0.1
515 lines
11 KiB
Go
515 lines
11 KiB
Go
package kong_test
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/alecthomas/kong"
|
|
)
|
|
|
|
func mustNew(t *testing.T, cli interface{}, 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...)
|
|
require.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"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "user create <id> <first> <last>", ctx.Command())
|
|
t.Run("Missing", func(t *testing.T) {
|
|
_, err := p.Parse([]string{"user", "create", "10"})
|
|
require.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"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, 10, cli.User.ID.ID)
|
|
require.Equal(t, "user <id> delete", ctx.Command())
|
|
t.Run("Missing", func(t *testing.T) {
|
|
_, err = p.Parse([]string{"user"})
|
|
require.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{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "", cli.Flag)
|
|
require.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"})
|
|
require.NoError(t, err)
|
|
require.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`})
|
|
require.NoError(t, err)
|
|
require.Equal(t, []string{"a,b", "c"}, cli.Slice)
|
|
}
|
|
|
|
func TestArgSlice(t *testing.T) {
|
|
// nolint: govet
|
|
var cli struct {
|
|
Slice []int `arg`
|
|
Flag bool
|
|
}
|
|
parser := mustNew(t, &cli)
|
|
_, err := parser.Parse([]string{"1", "2", "3", "--flag"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, []int{1, 2, 3}, cli.Slice)
|
|
require.Equal(t, true, cli.Flag)
|
|
}
|
|
|
|
func TestArgSliceWithSeparator(t *testing.T) {
|
|
// nolint: govet
|
|
var cli struct {
|
|
Slice []string `arg`
|
|
Flag bool
|
|
}
|
|
parser := mustNew(t, &cli)
|
|
_, err := parser.Parse([]string{"a,b", "c", "--flag"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, []string{"a,b", "c"}, cli.Slice)
|
|
require.Equal(t, true, cli.Flag)
|
|
}
|
|
|
|
func TestUnsupportedFieldErrors(t *testing.T) {
|
|
var cli struct {
|
|
Keys struct{}
|
|
}
|
|
_, err := kong.New(&cli)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestMatchingArgField(t *testing.T) {
|
|
var cli struct {
|
|
ID struct {
|
|
NotID int `kong:"arg"`
|
|
} `kong:"arg"`
|
|
}
|
|
|
|
_, err := kong.New(&cli)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCantMixPositionalAndBranches(t *testing.T) {
|
|
var cli struct {
|
|
Arg string `kong:"arg"`
|
|
Command struct {
|
|
} `kong:"cmd"`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
require.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"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "moo", cli.Flag1)
|
|
require.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{})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestOptionalArg(t *testing.T) {
|
|
var cli struct {
|
|
Arg string `kong:"arg,optional"`
|
|
}
|
|
|
|
parser := mustNew(t, &cli)
|
|
_, err := parser.Parse([]string{})
|
|
require.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{})
|
|
require.NoError(t, err)
|
|
require.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{})
|
|
require.NoError(t, err)
|
|
require.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{})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInvalidRequiredAfterOptional(t *testing.T) {
|
|
var cli struct {
|
|
ID int `kong:"arg,optional"`
|
|
Name string `kong:"arg"`
|
|
}
|
|
|
|
_, err := kong.New(&cli)
|
|
require.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"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "gak", cli.Name.Name)
|
|
require.Equal(t, true, cli.Name.Enabled)
|
|
})
|
|
|
|
t.Run("WithoutFlag", func(t *testing.T) {
|
|
_, err := parser.Parse([]string{"gak"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "gak", cli.Name.Name)
|
|
})
|
|
|
|
t.Run("WithNothing", func(t *testing.T) {
|
|
_, err := parser.Parse([]string{})
|
|
require.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"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "gak", cli.Name)
|
|
require.Equal(t, 5, cli.ID)
|
|
})
|
|
|
|
t.Run("ExtraOptional", func(t *testing.T) {
|
|
_, err := parser.Parse([]string{"gak"})
|
|
require.NoError(t, err)
|
|
require.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)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCommandMissingTagIsInvalid(t *testing.T) {
|
|
var cli struct {
|
|
One struct{}
|
|
}
|
|
_, err := kong.New(&cli)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestDuplicateFlag(t *testing.T) {
|
|
var cli struct {
|
|
Flag bool
|
|
Cmd struct {
|
|
Flag bool
|
|
} `kong:"cmd"`
|
|
}
|
|
_, err := kong.New(&cli)
|
|
require.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)
|
|
require.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"})
|
|
require.NoError(t, err)
|
|
require.Error(t, ctx.Error)
|
|
require.Equal(t, "one", ctx.Command())
|
|
}
|
|
|
|
func TestHooks(t *testing.T) {
|
|
var cli struct {
|
|
One struct {
|
|
Two string `kong:"arg,optional"`
|
|
Three string
|
|
} `kong:"cmd"`
|
|
}
|
|
type values struct {
|
|
one bool
|
|
two string
|
|
three string
|
|
}
|
|
hooked := values{}
|
|
var tests = []struct {
|
|
name string
|
|
input string
|
|
values values
|
|
}{
|
|
{"Command", "one", values{true, "", ""}},
|
|
{"Arg", "one two", values{true, "two", ""}},
|
|
{"Flag", "one --three=three", values{true, "", "three"}},
|
|
{"ArgAndFlag", "one two --three=three", values{true, "two", "three"}},
|
|
}
|
|
setOne := func(ctx *kong.Context, path *kong.Path) error { hooked.one = true; return nil }
|
|
setTwo := func(ctx *kong.Context, path *kong.Path) error { hooked.two = ctx.Value(path).String(); return nil }
|
|
setThree := func(ctx *kong.Context, path *kong.Path) error { hooked.three = ctx.Value(path).String(); return nil }
|
|
p := mustNew(t, &cli,
|
|
kong.Hook(&cli.One, setOne),
|
|
kong.Hook(&cli.One.Two, setTwo),
|
|
kong.Hook(&cli.One.Three, setThree))
|
|
|
|
for _, test := range tests {
|
|
hooked = values{}
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, err := p.Parse(strings.Split(test.input, " "))
|
|
require.NoError(t, err)
|
|
require.Equal(t, test.values, hooked)
|
|
})
|
|
}
|
|
}
|
|
|
|
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"})
|
|
require.NoError(t, err)
|
|
require.True(t, cli.Bool)
|
|
require.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"})
|
|
require.NoError(t, err)
|
|
require.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)
|
|
require.NoError(t, err)
|
|
require.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"})
|
|
require.NoError(t, err)
|
|
require.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"})
|
|
require.NoError(t, err)
|
|
require.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"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "moo", cli.Embedded)
|
|
require.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"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, []string{"a,b", "b,c"}, cli.Flag)
|
|
}
|
|
|
|
func TestMultilineMessage(t *testing.T) {
|
|
w := &bytes.Buffer{}
|
|
var cli struct{}
|
|
p := mustNew(t, &cli, kong.Writers(w, w))
|
|
p.Printf("hello\nworld")
|
|
require.Equal(t, "test: hello\n world\n", 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 grammarWithRun struct {
|
|
One cmdWithRun `cmd:""`
|
|
Two cmdWithRun `cmd:""`
|
|
}
|
|
|
|
func TestRun(t *testing.T) {
|
|
cli := &grammarWithRun{}
|
|
p := mustNew(t, cli)
|
|
|
|
ctx, err := p.Parse([]string{"one", "two"})
|
|
require.NoError(t, err)
|
|
err = ctx.Run("hello")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "twohello", cli.One.Arg)
|
|
|
|
ctx, err = p.Parse([]string{"two", "three"})
|
|
require.NoError(t, err)
|
|
err = ctx.Run("ERROR")
|
|
require.Error(t, err)
|
|
}
|