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

866 lines
23 KiB
Go

package kong_test
import (
"bytes"
"fmt"
"strings"
"testing"
"git.company.lan/gopkg/kong"
"github.com/alecthomas/assert/v2"
)
func panicsTrue(t *testing.T, f func()) {
defer func() {
if value := recover(); value != nil {
if boolval, ok := value.(bool); !ok || !boolval {
t.Fatalf("expected panic with true but got %v", value)
}
}
}()
f()
t.Fatal("expected panic did not occur")
}
type threeArg struct {
RequiredThree bool `required`
Three string `arg`
}
func (threeArg) Help() string {
return `Detailed help provided through the HelpProvider interface.`
}
func TestHelpOptionalArgs(t *testing.T) {
var cli struct {
One string `arg:"" optional:"" help:"One optional arg."`
Two string `arg:"" optional:"" help:"Two optional arg."`
}
w := bytes.NewBuffer(nil)
exited := false
app := mustNew(t, &cli,
kong.Name("test-app"),
kong.Writers(w, w),
kong.Exit(func(int) {
exited = true
panic(true) // Panic to fake "exit".
}),
)
panicsTrue(t, func() {
_, err := app.Parse([]string{"--help"})
assert.NoError(t, err)
})
assert.True(t, exited)
expected := `Usage: test-app [<one> [<two>]]
Arguments:
[<one>] One optional arg.
[<two>] Two optional arg.
Flags:
-h, --help Show context-sensitive help.
`
assert.Equal(t, expected, w.String())
}
func TestHelp(t *testing.T) {
var cli struct {
String string `help:"A string flag."`
Bool bool `help:"A bool flag with very long help that wraps a lot and is verbose and is really verbose."`
Slice []string `help:"A slice of strings." placeholder:"STR"`
Map map[string]int `help:"A map of strings to ints."`
Required bool `required help:"A required flag."`
Sort bool `negatable short:"s" help:"Is sortable or not."`
Approve bool `negatable:"deny" help:"Approve or deny message."`
One struct {
Flag string `help:"Nested flag."`
} `cmd help:"A subcommand."`
Two struct {
Flag string `help:"Nested flag under two."`
RequiredTwo bool `required`
Three threeArg `arg help:"Sub-sub-arg."`
Four struct{} `cmd help:"Sub-sub-command."`
} `cmd help:"Another subcommand."`
}
w := bytes.NewBuffer(nil)
exited := false
app := mustNew(t, &cli,
kong.Name("test-app"),
kong.Description("A test app."),
kong.Writers(w, w),
kong.Exit(func(int) {
exited = true
panic(true) // Panic to fake "exit".
}),
)
t.Run("Full", func(t *testing.T) {
panicsTrue(t, func() {
_, err := app.Parse([]string{"--help"})
assert.NoError(t, err)
})
assert.True(t, exited)
expected := `Usage: test-app --required <command> [flags]
A test app.
Flags:
-h, --help Show context-sensitive help.
--string=STRING A string flag.
--bool A bool flag with very long help that wraps a lot
and is verbose and is really verbose.
--slice=STR,... A slice of strings.
--map=KEY=VALUE;... A map of strings to ints.
--required A required flag.
-s, --[no-]sort Is sortable or not.
--approve/deny Approve or deny message.
Commands:
one --required [flags]
A subcommand.
two <three> --required --required-two --required-three [flags]
Sub-sub-arg.
two four --required --required-two [flags]
Sub-sub-command.
Run "test-app <command> --help" for more information on a command.
`
t.Log(w.String())
t.Log(expected)
assert.Equal(t, expected, w.String())
})
t.Run("Selected", func(t *testing.T) {
exited = false
w.Truncate(0)
panicsTrue(t, func() {
_, err := app.Parse([]string{"two", "hello", "--help"})
assert.NoError(t, err)
})
assert.True(t, exited)
expected := `Usage: test-app two <three> --required --required-two --required-three [flags]
Sub-sub-arg.
Detailed help provided through the HelpProvider interface.
Flags:
-h, --help Show context-sensitive help.
--string=STRING A string flag.
--bool A bool flag with very long help that wraps a lot
and is verbose and is really verbose.
--slice=STR,... A slice of strings.
--map=KEY=VALUE;... A map of strings to ints.
--required A required flag.
-s, --[no-]sort Is sortable or not.
--approve/deny Approve or deny message.
--flag=STRING Nested flag under two.
--required-two
--required-three
`
t.Log(expected)
t.Log(w.String())
assert.Equal(t, expected, w.String())
})
}
func TestFlagsLast(t *testing.T) {
var cli struct {
String string `help:"A string flag."`
Bool bool `help:"A bool flag with very long help that wraps a lot and is verbose and is really verbose."`
Slice []string `help:"A slice of strings." placeholder:"STR"`
Map map[string]int `help:"A map of strings to ints."`
Required bool `required help:"A required flag."`
One struct {
Flag string `help:"Nested flag."`
} `cmd help:"A subcommand."`
Two struct {
Flag string `help:"Nested flag under two."`
RequiredTwo bool `required`
Three threeArg `arg help:"Sub-sub-arg."`
Four struct{} `cmd help:"Sub-sub-command."`
} `cmd help:"Another subcommand."`
}
w := bytes.NewBuffer(nil)
exited := false
app := mustNew(t, &cli,
kong.Name("test-app"),
kong.Description("A test app."),
kong.HelpOptions{
FlagsLast: true,
},
kong.Writers(w, w),
kong.Exit(func(int) {
exited = true
panic(true) // Panic to fake "exit".
}),
)
t.Run("Full", func(t *testing.T) {
panicsTrue(t, func() {
_, err := app.Parse([]string{"--help"})
assert.NoError(t, err)
})
assert.True(t, exited)
expected := `Usage: test-app --required <command> [flags]
A test app.
Commands:
one --required [flags]
A subcommand.
two <three> --required --required-two --required-three [flags]
Sub-sub-arg.
two four --required --required-two [flags]
Sub-sub-command.
Flags:
-h, --help Show context-sensitive help.
--string=STRING A string flag.
--bool A bool flag with very long help that wraps a lot
and is verbose and is really verbose.
--slice=STR,... A slice of strings.
--map=KEY=VALUE;... A map of strings to ints.
--required A required flag.
Run "test-app <command> --help" for more information on a command.
`
t.Log(w.String())
t.Log(expected)
assert.Equal(t, expected, w.String())
})
t.Run("Selected", func(t *testing.T) {
exited = false
w.Truncate(0)
panicsTrue(t, func() {
_, err := app.Parse([]string{"two", "hello", "--help"})
assert.NoError(t, err)
})
assert.True(t, exited)
expected := `Usage: test-app two <three> --required --required-two --required-three [flags]
Sub-sub-arg.
Detailed help provided through the HelpProvider interface.
Flags:
-h, --help Show context-sensitive help.
--string=STRING A string flag.
--bool A bool flag with very long help that wraps a lot
and is verbose and is really verbose.
--slice=STR,... A slice of strings.
--map=KEY=VALUE;... A map of strings to ints.
--required A required flag.
--flag=STRING Nested flag under two.
--required-two
--required-three
`
t.Log(expected)
t.Log(w.String())
assert.Equal(t, expected, w.String())
})
}
func TestHelpTree(t *testing.T) {
var cli struct {
One struct {
Thing struct {
Arg string `arg help:"argument"`
} `cmd help:"subcommand thing"`
Other struct {
Other string `arg help:"other arg"`
} `arg help:"subcommand other"`
} `cmd help:"subcommand one" group:"Group A" aliases:"un,uno"` // Groups are ignored in trees
Two struct {
Three threeArg `arg help:"Sub-sub-arg."`
Four struct{} `cmd help:"Sub-sub-command." aliases:"for,fore"`
} `cmd help:"Another subcommand."`
}
w := bytes.NewBuffer(nil)
exited := false
app := mustNew(t, &cli,
kong.Name("test-app"),
kong.Description("A test app."),
kong.Writers(w, w),
kong.ConfigureHelp(kong.HelpOptions{
Tree: true,
Indenter: kong.LineIndenter,
}),
kong.Exit(func(int) {
exited = true
panic(true) // Panic to fake "exit".
}),
)
t.Run("Full", func(t *testing.T) {
panicsTrue(t, func() {
_, err := app.Parse([]string{"--help"})
assert.NoError(t, err)
})
assert.True(t, exited)
expected := `Usage: test-app <command>
A test app.
Flags:
-h, --help Show context-sensitive help.
Commands:
one (un,uno) subcommand one
- thing subcommand thing
- <arg> argument
- <other> subcommand other
two Another subcommand.
- <three> Sub-sub-arg.
- four (for,fore) Sub-sub-command.
Run "test-app <command> --help" for more information on a command.
`
if expected != w.String() {
t.Errorf("help command returned:\n%v\n\nwant:\n%v", w.String(), expected)
}
assert.Equal(t, expected, w.String())
})
t.Run("Selected", func(t *testing.T) {
exited = false
w.Truncate(0)
panicsTrue(t, func() {
_, err := app.Parse([]string{"one", "--help"})
assert.NoError(t, err)
})
assert.True(t, exited)
expected := `Usage: test-app one (un,uno) <command>
subcommand one
Flags:
-h, --help Show context-sensitive help.
Commands:
thing subcommand thing
- <arg> argument
<other> subcommand other
`
if expected != w.String() {
t.Errorf("help command returned:\n%v\n\nwant:\n%v", w.String(), expected)
}
assert.Equal(t, expected, w.String())
})
}
func TestHelpCompactNoExpand(t *testing.T) {
var cli struct {
One struct {
Thing struct {
Arg string `arg help:"argument"`
} `cmd help:"subcommand thing"`
Other struct {
Other string `arg help:"other arg"`
} `arg help:"subcommand other"`
} `cmd help:"subcommand one" group:"Group A" aliases:"un,uno"` // Groups are ignored in trees
Two struct {
Three threeArg `arg help:"Sub-sub-arg."`
Four struct{} `cmd help:"Sub-sub-command." aliases:"for,fore"`
} `cmd help:"Another subcommand."`
}
w := bytes.NewBuffer(nil)
exited := false
app := mustNew(t, &cli,
kong.Name("test-app"),
kong.Description("A test app."),
kong.Writers(w, w),
kong.ConfigureHelp(kong.HelpOptions{
Compact: true,
NoExpandSubcommands: true,
}),
kong.Exit(func(int) {
exited = true
panic(true) // Panic to fake "exit".
}),
)
t.Run("Full", func(t *testing.T) {
panicsTrue(t, func() {
_, err := app.Parse([]string{"--help"})
assert.NoError(t, err)
})
assert.True(t, exited)
expected := `Usage: test-app <command>
A test app.
Flags:
-h, --help Show context-sensitive help.
Commands:
two Another subcommand.
Group A
one (un,uno) subcommand one
Run "test-app <command> --help" for more information on a command.
`
if expected != w.String() {
t.Errorf("help command returned:\n%v\n\nwant:\n%v", w.String(), expected)
}
assert.Equal(t, expected, w.String())
})
t.Run("Selected", func(t *testing.T) {
exited = false
w.Truncate(0)
panicsTrue(t, func() {
_, err := app.Parse([]string{"one", "--help"})
assert.NoError(t, err)
})
assert.True(t, exited)
expected := `Usage: test-app one (un,uno) <command>
subcommand one
Flags:
-h, --help Show context-sensitive help.
Group A
one (un,uno) thing subcommand thing
one (un,uno) <other> subcommand other
`
if expected != w.String() {
t.Errorf("help command returned:\n%v\n\nwant:\n%v", w.String(), expected)
}
assert.Equal(t, expected, w.String())
})
}
func TestEnvarAutoHelp(t *testing.T) {
var cli struct {
Flag string `env:"FLAG" help:"A flag."`
}
w := &strings.Builder{}
p := mustNew(t, &cli, kong.Writers(w, w), kong.Exit(func(int) {}))
_, err := p.Parse([]string{"--help"})
assert.NoError(t, err)
assert.Contains(t, w.String(), "A flag ($FLAG).")
}
func TestMultipleEnvarAutoHelp(t *testing.T) {
var cli struct {
Flag string `env:"FLAG1,FLAG2" help:"A flag."`
}
w := &strings.Builder{}
p := mustNew(t, &cli, kong.Writers(w, w), kong.Exit(func(int) {}))
_, err := p.Parse([]string{"--help"})
assert.NoError(t, err)
assert.Contains(t, w.String(), "A flag ($FLAG1, $FLAG2).")
}
//nolint:dupl // false positive
func TestEnvarAutoHelpWithEnvPrefix(t *testing.T) {
type Anonymous struct {
Flag string `env:"FLAG" help:"A flag."`
Other string `help:"A different flag."`
}
var cli struct {
Anonymous `envprefix:"ANON_"`
}
w := &strings.Builder{}
p := mustNew(t, &cli, kong.Writers(w, w), kong.Exit(func(int) {}))
_, err := p.Parse([]string{"--help"})
assert.NoError(t, err)
assert.Contains(t, w.String(), "A flag ($ANON_FLAG).")
assert.Contains(t, w.String(), "A different flag.")
}
//nolint:dupl // false positive
func TestMultipleEnvarAutoHelpWithEnvPrefix(t *testing.T) {
type Anonymous struct {
Flag string `env:"FLAG1,FLAG2" help:"A flag."`
Other string `help:"A different flag."`
}
var cli struct {
Anonymous `envprefix:"ANON_"`
}
w := &strings.Builder{}
p := mustNew(t, &cli, kong.Writers(w, w), kong.Exit(func(int) {}))
_, err := p.Parse([]string{"--help"})
assert.NoError(t, err)
assert.Contains(t, w.String(), "A flag ($ANON_FLAG1, $ANON_FLAG2).")
assert.Contains(t, w.String(), "A different flag.")
}
//nolint:dupl // false positive
func TestCustomValueFormatter(t *testing.T) {
var cli struct {
Flag string `env:"FLAG" help:"A flag."`
}
w := &strings.Builder{}
p := mustNew(t, &cli,
kong.Writers(w, w),
kong.Exit(func(int) {}),
kong.ValueFormatter(func(value *kong.Value) string {
return value.Help
}),
)
_, err := p.Parse([]string{"--help"})
assert.NoError(t, err)
assert.Contains(t, w.String(), "A flag.")
}
//nolint:dupl // false positive
func TestMultipleCustomValueFormatter(t *testing.T) {
var cli struct {
Flag string `env:"FLAG1,FLAG2" help:"A flag."`
}
w := &strings.Builder{}
p := mustNew(t, &cli,
kong.Writers(w, w),
kong.Exit(func(int) {}),
kong.ValueFormatter(func(value *kong.Value) string {
return value.Help
}),
)
_, err := p.Parse([]string{"--help"})
assert.NoError(t, err)
assert.Contains(t, w.String(), "A flag.")
}
func TestAutoGroup(t *testing.T) {
var cli struct {
GroupedAString string `help:"A string flag grouped in A."`
FreeString string `help:"A non grouped string flag."`
GroupedBString string `help:"A string flag grouped in B."`
FreeBool bool `help:"A non grouped bool flag."`
GroupedABool bool `help:"A bool flag grouped in A."`
One struct {
Flag string `help:"Nested flag."`
// Group is inherited from the parent command
Thing struct {
Arg string `arg help:"argument"`
} `cmd help:"subcommand thing"`
Other struct {
Other string `arg help:"other arg"`
} `arg help:"subcommand other"`
// ... but a subcommand can override it
Stuff struct {
Stuff string `arg help:"argument"`
} `arg help:"subcommand stuff"`
} `cmd help:"A subcommand grouped in A."`
Two struct {
Grouped1String string `help:"A string flag grouped in 1."`
AFreeString string `help:"A non grouped string flag."`
Grouped2String string `help:"A string flag grouped in 2."`
AGroupedAString bool `help:"A string flag grouped in A."`
Grouped1Bool bool `help:"A bool flag grouped in 1."`
} `cmd help:"A non grouped subcommand."`
Four struct {
Flag string `help:"Nested flag."`
} `cmd help:"Another subcommand grouped in B."`
Three struct {
Flag string `help:"Nested flag."`
} `cmd help:"Another subcommand grouped in A."`
}
w := bytes.NewBuffer(nil)
app := mustNew(t, &cli,
kong.Writers(w, w),
kong.Exit(func(int) {}),
kong.AutoGroup(func(parent kong.Visitable, flag *kong.Flag) *kong.Group {
if node, ok := parent.(*kong.Node); ok {
return &kong.Group{
Key: node.Name,
Title: strings.Title(node.Name) + " flags:", //nolint:staticcheck // strings.Title in test is okay
}
}
return nil
}),
)
_, _ = app.Parse([]string{"--help", "two"})
assert.Equal(t, `Usage: test two [flags]
A non grouped subcommand.
Flags:
-h, --help Show context-sensitive help.
--grouped-a-string=STRING A string flag grouped in A.
--free-string=STRING A non grouped string flag.
--grouped-b-string=STRING A string flag grouped in B.
--free-bool A non grouped bool flag.
--grouped-a-bool A bool flag grouped in A.
Two flags:
--grouped-1-string=STRING A string flag grouped in 1.
--a-free-string=STRING A non grouped string flag.
--grouped-2-string=STRING A string flag grouped in 2.
--a-grouped-a-string A string flag grouped in A.
--grouped-1-bool A bool flag grouped in 1.
`, w.String())
}
func TestHelpGrouping(t *testing.T) {
var cli struct {
GroupedAString string `help:"A string flag grouped in A." group:"Group A"`
FreeString string `help:"A non grouped string flag."`
GroupedBString string `help:"A string flag grouped in B." group:"Group B"`
FreeBool bool `help:"A non grouped bool flag."`
GroupedABool bool `help:"A bool flag grouped in A." group:"Group A"`
One struct {
Flag string `help:"Nested flag."`
// Group is inherited from the parent command
Thing struct {
Arg string `arg help:"argument"`
} `cmd help:"subcommand thing"`
Other struct {
Other string `arg help:"other arg"`
} `arg help:"subcommand other"`
// ... but a subcommand can override it
Stuff struct {
Stuff string `arg help:"argument"`
} `arg help:"subcommand stuff" group:"Group B"`
} `cmd help:"A subcommand grouped in A." group:"Group A"`
Two struct {
Grouped1String string `help:"A string flag grouped in 1." group:"Group 1"`
AFreeString string `help:"A non grouped string flag."`
Grouped2String string `help:"A string flag grouped in 2." group:"Group 2"`
AGroupedAString bool `help:"A string flag grouped in A." group:"Group A"`
Grouped1Bool bool `help:"A bool flag grouped in 1." group:"Group 1"`
} `cmd help:"A non grouped subcommand."`
Four struct {
Flag string `help:"Nested flag."`
} `cmd help:"Another subcommand grouped in B." group:"Group B"`
Three struct {
Flag string `help:"Nested flag."`
} `cmd help:"Another subcommand grouped in A." group:"Group A"`
}
w := bytes.NewBuffer(nil)
exited := false
app := mustNew(t, &cli,
kong.Name("test-app"),
kong.Description("A test app."),
kong.Groups{
"Group A": "Group title taken from the kong.ExplicitGroups option\nA group header",
"Group 1": "Another group title, this time without header",
"Unknown key": "",
},
kong.Writers(w, w),
kong.Exit(func(int) {
exited = true
panic(true) // Panic to fake "exit".
}),
)
t.Run("Full", func(t *testing.T) {
panicsTrue(t, func() {
_, err := app.Parse([]string{"--help"})
assert.True(t, exited)
assert.NoError(t, err)
})
expected := `Usage: test-app <command> [flags]
A test app.
Flags:
-h, --help Show context-sensitive help.
--free-string=STRING A non grouped string flag.
--free-bool A non grouped bool flag.
Group title taken from the kong.ExplicitGroups option
A group header
--grouped-a-string=STRING A string flag grouped in A.
--grouped-a-bool A bool flag grouped in A.
Group B
--grouped-b-string=STRING A string flag grouped in B.
Commands:
two [flags]
A non grouped subcommand.
Group title taken from the kong.ExplicitGroups option
A group header
one thing <arg> [flags]
subcommand thing
one <other> [flags]
subcommand other
three [flags]
Another subcommand grouped in A.
Group B
one <stuff> [flags]
subcommand stuff
four [flags]
Another subcommand grouped in B.
Run "test-app <command> --help" for more information on a command.
`
t.Log(w.String())
t.Log(expected)
assert.Equal(t, expected, w.String())
})
t.Run("Selected", func(t *testing.T) {
exited = false
w.Truncate(0)
panicsTrue(t, func() {
_, err := app.Parse([]string{"two", "--help"})
assert.NoError(t, err)
assert.True(t, exited)
})
expected := `Usage: test-app two [flags]
A non grouped subcommand.
Flags:
-h, --help Show context-sensitive help.
--free-string=STRING A non grouped string flag.
--free-bool A non grouped bool flag.
--a-free-string=STRING A non grouped string flag.
Group title taken from the kong.ExplicitGroups option
A group header
--grouped-a-string=STRING A string flag grouped in A.
--grouped-a-bool A bool flag grouped in A.
--a-grouped-a-string A string flag grouped in A.
Group B
--grouped-b-string=STRING A string flag grouped in B.
Another group title, this time without header
--grouped-1-string=STRING A string flag grouped in 1.
--grouped-1-bool A bool flag grouped in 1.
Group 2
--grouped-2-string=STRING A string flag grouped in 2.
`
t.Log(expected)
t.Log(w.String())
assert.Equal(t, expected, w.String())
})
}
func TestUsageOnError(t *testing.T) {
var cli struct {
Flag string `help:"A required flag." required`
}
w := &strings.Builder{}
exitCode := -1
p := mustNew(t, &cli,
kong.Writers(w, w),
kong.Description("Some description."),
kong.Exit(func(code int) { exitCode = code }),
kong.UsageOnError(),
)
_, err := p.Parse([]string{})
p.FatalIfErrorf(err)
expected := `Usage: test --flag=STRING
Some description.
Flags:
-h, --help Show context-sensitive help.
--flag=STRING A required flag.
test: error: missing flags: --flag=STRING
`
assert.Equal(t, expected, w.String())
assert.Equal(t, 80, exitCode)
}
func TestShortUsageOnError(t *testing.T) {
var cli struct {
Flag string `help:"A required flag." required`
}
w := &strings.Builder{}
exitCode := -1
p := mustNew(t, &cli,
kong.Writers(w, w),
kong.Description("Some description."),
kong.Exit(func(code int) { exitCode = code }),
kong.ShortUsageOnError(),
)
_, err := p.Parse([]string{})
assert.Error(t, err)
p.FatalIfErrorf(err)
expected := `Usage: test --flag=STRING
Run "test --help" for more information.
test: error: missing flags: --flag=STRING
`
assert.Equal(t, expected, w.String())
assert.Equal(t, 80, exitCode)
}
func TestCustomShortUsageOnError(t *testing.T) {
var cli struct {
Flag string `help:"A required flag." required`
}
w := &strings.Builder{}
shortHelp := func(_ kong.HelpOptions, ctx *kong.Context) error {
fmt.Fprintln(ctx.Stdout, "🤷 wish I could help")
return nil
}
exitCode := -1
p := mustNew(t, &cli,
kong.Writers(w, w),
kong.Description("Some description."),
kong.Exit(func(code int) { exitCode = code }),
kong.ShortHelp(shortHelp),
kong.ShortUsageOnError(),
)
_, err := p.Parse([]string{})
assert.Error(t, err)
p.FatalIfErrorf(err)
expected := `🤷 wish I could help
test: error: missing flags: --flag=STRING
`
assert.Equal(t, expected, w.String())
assert.Equal(t, 80, exitCode)
}