Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,7 @@ Both can coexist with standard Tag parsing.
| `required:""` | If present, flag/arg is required. |
| `optional:""` | If present, flag/arg is optional. |
| `hidden:""` | If present, command or flag is hidden. |
| `deprecated:"X"` | If present, command is deprecated. The value is shown as a warning when the command is used. |
| `negatable:""` | If present on a `bool` field, supports prefixing a flag with `--no-` to invert the default value |
| `negatable:"X"` | If present on a `bool` field, supports `--X` to invert the default value |
| `format:"X"` | Format for parsing input, if supported. |
Expand Down
4 changes: 4 additions & 0 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,10 @@ func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.S
child.Parent = node
child.Help = tag.Help
child.Hidden = tag.Hidden
child.Deprecated = tag.HasDeprecated
if tag.HasDeprecated {
child.DeprecatedMsg = tag.Deprecated
}
child.Group = buildGroupForKey(k, tag.Group)
child.Aliases = tag.Aliases

Expand Down
20 changes: 16 additions & 4 deletions help.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ func writeCompactCommandList(cmds []*Node, iw *helpWriter) {
if cmd.Hidden {
continue
}
rows = append(rows, [2]string{cmd.Path(), cmd.Help})
rows = append(rows, [2]string{cmd.Path(), cmd.Help + deprecationNotice(cmd)})
}
writeTwoColumns(iw, rows)
}
Expand Down Expand Up @@ -360,10 +360,22 @@ func collectCommandGroups(nodes []*Node) []helpCommandGroup {
return out
}

func deprecationNotice(node *Node) string {
if !node.Deprecated {
return ""
}
if node.DeprecatedMsg != "" {
return " (deprecated: " + node.DeprecatedMsg + ")"
}
return " (deprecated)"
}

func printCommandSummary(w *helpWriter, cmd *Command) {

w.Print(cmd.Summary())
if cmd.Help != "" {
w.Indent().Wrap(cmd.Help)
help := cmd.Help + deprecationNotice(cmd)
if help != "" {
w.Indent().Wrap(help)
}
}

Expand Down Expand Up @@ -528,7 +540,7 @@ func (h *HelpOptions) CommandTree(node *Node, prefix string) (rows [][2]string)
case ArgumentNode:
nodeName += prefix + "<" + node.Name + ">"
}
rows = append(rows, [2]string{nodeName, node.Help})
rows = append(rows, [2]string{nodeName, node.Help + deprecationNotice(node)})
if h.Indenter == nil {
prefix = SpaceIndenter(prefix)
} else {
Expand Down
98 changes: 98 additions & 0 deletions help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -863,3 +863,101 @@ test: error: missing flags: --flag=STRING
assert.Equal(t, expected, w.String())
assert.Equal(t, 80, exitCode)
}

func TestHelpDeprecatedCommand(t *testing.T) {
var cli struct {
NewCmd struct{} `cmd help:"The new command."`
OldCmd struct{} `cmd help:"The old command." deprecated:"use new-cmd instead"`
}
w := bytes.NewBuffer(nil)
exited := false
app := mustNew(t, &cli,
kong.Name("test"),
kong.Writers(w, w),
kong.Exit(func(int) {
exited = true
panic(true)
}),
)
panicsTrue(t, func() {
_, err := app.Parse([]string{"--help"})
assert.NoError(t, err)
})
assert.True(t, exited)
t.Log(w.String())
assert.Contains(t, w.String(), "The old command. (deprecated: use new-cmd instead)")
}

func TestHelpDeprecatedCommandNoMessage(t *testing.T) {
var cli struct {
NewCmd struct{} `cmd help:"The new command."`
OldCmd struct{} `cmd help:"The old command." deprecated:""`
}
w := bytes.NewBuffer(nil)
exited := false
app := mustNew(t, &cli,
kong.Name("test"),
kong.Writers(w, w),
kong.Exit(func(int) {
exited = true
panic(true)
}),
)
panicsTrue(t, func() {
_, err := app.Parse([]string{"--help"})
assert.NoError(t, err)
})
assert.True(t, exited)
t.Log(w.String())
assert.Contains(t, w.String(), "The old command. (deprecated)")
}

func TestHelpDeprecatedCommandCompact(t *testing.T) {
var cli struct {
NewCmd struct{} `cmd help:"The new command."`
OldCmd struct{} `cmd help:"The old command." deprecated:"use new-cmd"`
}
w := bytes.NewBuffer(nil)
exited := false
app := mustNew(t, &cli,
kong.Name("test"),
kong.Writers(w, w),
kong.ConfigureHelp(kong.HelpOptions{Compact: true}),
kong.Exit(func(int) {
exited = true
panic(true)
}),
)
panicsTrue(t, func() {
_, err := app.Parse([]string{"--help"})
assert.NoError(t, err)
})
assert.True(t, exited)
t.Log(w.String())
assert.Contains(t, w.String(), "(deprecated: use new-cmd)")
}

func TestHelpDeprecatedCommandTree(t *testing.T) {
var cli struct {
NewCmd struct{} `cmd help:"The new command."`
OldCmd struct{} `cmd help:"The old command." deprecated:"use new-cmd"`
}
w := bytes.NewBuffer(nil)
exited := false
app := mustNew(t, &cli,
kong.Name("test"),
kong.Writers(w, w),
kong.ConfigureHelp(kong.HelpOptions{Tree: true}),
kong.Exit(func(int) {
exited = true
panic(true)
}),
)
panicsTrue(t, func() {
_, err := app.Parse([]string{"--help"})
assert.NoError(t, err)
})
assert.True(t, exited)
t.Log(w.String())
assert.Contains(t, w.String(), "(deprecated: use new-cmd)")
}
9 changes: 9 additions & 0 deletions kong.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,15 @@ func (k *Kong) Parse(args []string) (ctx *Context, err error) {
if err = k.applyHook(ctx, "AfterApply"); err != nil {
return nil, &ParseError{error: err, Context: ctx}
}
for _, trace := range ctx.Path {
if trace.Command != nil && trace.Command.Deprecated {
if trace.Command.DeprecatedMsg != "" {
fmt.Fprintf(k.Stderr, "%s: command %q: %s\n", k.Model.Name, trace.Command.Name, trace.Command.DeprecatedMsg)
} else {
fmt.Fprintf(k.Stderr, "%s: command %q is deprecated\n", k.Model.Name, trace.Command.Name)
}
}
}
return ctx, nil
}

Expand Down
55 changes: 51 additions & 4 deletions kong_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,8 @@ func TestMatchingArgField(t *testing.T) {

func TestCantMixPositionalAndBranches(t *testing.T) {
var cli struct {
Arg string `kong:"arg"`
Command struct {
Arg string `kong:"arg"`
Command struct{
} `kong:"cmd"`
}
_, err := kong.New(&cli)
Expand Down Expand Up @@ -364,7 +364,7 @@ func TestDuplicateFlagOnPeerCommandIsOkay(t *testing.T) {
func TestTraceErrorPartiallySucceeds(t *testing.T) {
var cli struct {
One struct {
Two struct {
Two struct{
} `kong:"cmd"`
} `kong:"cmd"`
}
Expand Down Expand Up @@ -2653,7 +2653,7 @@ func TestIssue483EmptyRootNodeNoRun(t *testing.T) {
assert.Contains(t, err.Error(), "no command selected")
}

type providerWithoutErrorCLI struct {
type providerWithoutErrorCLI struct{
}

func (p *providerWithoutErrorCLI) Run(name string) error {
Expand Down Expand Up @@ -2708,3 +2708,50 @@ func TestParseHyphenParameter(t *testing.T) {
assert.Equal(t, &shortFlag{Numeric: -10}, actual)
})
}

func TestDeprecatedCommandWarning(t *testing.T) {
var cli struct {
OldCmd struct{} `cmd deprecated:"use new-cmd instead"`
}
stderr := &bytes.Buffer{}
p := mustNew(t, &cli, kong.Writers(bytes.NewBuffer(nil), stderr))
_, err := p.Parse([]string{"old-cmd"})
assert.NoError(t, err)
assert.Contains(t, stderr.String(), `command "old-cmd": use new-cmd instead`)
}

func TestDeprecatedCommandWarningNoMessage(t *testing.T) {
var cli struct {
OldCmd struct{} `cmd deprecated:""`
}
stderr := &bytes.Buffer{}
p := mustNew(t, &cli, kong.Writers(bytes.NewBuffer(nil), stderr))
_, err := p.Parse([]string{"old-cmd"})
assert.NoError(t, err)
assert.Contains(t, stderr.String(), `command "old-cmd" is deprecated`)
assert.NotContains(t, stderr.String(), "deprecated:")
}

func TestNonDeprecatedCommandNoWarning(t *testing.T) {
var cli struct {
Cmd struct{} `cmd`
}
stderr := &bytes.Buffer{}
p := mustNew(t, &cli, kong.Writers(bytes.NewBuffer(nil), stderr))
_, err := p.Parse([]string{"cmd"})
assert.NoError(t, err)
assert.Equal(t, "", stderr.String())
}

func TestDeprecatedAndHiddenCommand(t *testing.T) {
var cli struct {
OldCmd struct{} `cmd hidden deprecated:"use new-cmd instead"`
}
stderr := &bytes.Buffer{}
p := mustNew(t, &cli, kong.Writers(bytes.NewBuffer(nil), stderr))
_, err := p.Parse([]string{"old-cmd"})
assert.NoError(t, err)
assert.Contains(t, stderr.String(), `command "old-cmd": use new-cmd instead`)
// Command should be hidden
assert.True(t, p.Model.Children[0].Hidden)
}
34 changes: 18 additions & 16 deletions model.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,24 @@ const (

// Node is a branch in the CLI. ie. a command or positional argument.
type Node struct {
Type NodeType
Parent *Node
Name string
Help string // Short help displayed in summaries.
Detail string // Detailed help displayed when describing command/arg alone.
Group *Group
Hidden bool
Flags []*Flag
Positional []*Positional
Children []*Node
DefaultCmd *Node
Target reflect.Value // Pointer to the value in the grammar that this Node is associated with.
Tag *Tag
Aliases []string
Passthrough bool // Set to true to stop flag parsing when encountered.
Active bool // Denotes the node is part of an active branch in the CLI.
Type NodeType
Parent *Node
Name string
Help string // Short help displayed in summaries.
Detail string // Detailed help displayed when describing command/arg alone.
Group *Group
Hidden bool
Deprecated bool
DeprecatedMsg string
Flags []*Flag
Positional []*Positional
Children []*Node
DefaultCmd *Node
Target reflect.Value // Pointer to the value in the grammar that this Node is associated with.
Tag *Tag
Aliases []string
Passthrough bool // Set to true to stop flag parsing when encountered.
Active bool // Denotes the node is part of an active branch in the CLI.

Argument *Value // Populated when Type is ArgumentNode.
}
Expand Down
8 changes: 7 additions & 1 deletion tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ type Tag struct {
Envs []string
Short rune
Hidden bool
Deprecated string
HasDeprecated bool
Sep rune
MapSep rune
Enum string
Expand Down Expand Up @@ -75,7 +77,7 @@ type tagChars struct {
}

var kongChars = tagChars{sep: ',', quote: '\'', assign: '=', needsUnquote: false}
var bareChars = tagChars{sep: ' ', quote: '"', assign: ':', needsUnquote: true}
var bareChars = tagChars{sep: ' ', quote: '"', assign: ':', needsUnquote: true}

//nolint:gocyclo
func parseTagItems(tagString string, chr tagChars) (map[string][]string, error) {
Expand Down Expand Up @@ -273,6 +275,10 @@ func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo
return fmt.Errorf("invalid short flag name %q: %s", t.Get("short"), err)
}
t.Hidden = t.Has("hidden")
t.HasDeprecated = t.Has("deprecated")
if t.HasDeprecated {
t.Deprecated = t.Get("deprecated")
}
t.Format = t.Get("format")
t.Sep, _ = t.GetSep("sep", ',')
t.MapSep, _ = t.GetSep("mapsep", ';')
Expand Down
20 changes: 20 additions & 0 deletions tag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,26 @@ func TestTagAliasesSub(t *testing.T) {
assert.Equal(t, "arg", cli.Cmd.SubCmd.Arg)
}

func TestDeprecatedTag(t *testing.T) {
var cli struct {
OldCmd struct{} `cmd deprecated:"use new-cmd instead"`
}
p := mustNew(t, &cli)
node := p.Model.Children[0]
assert.True(t, node.Deprecated)
assert.Equal(t, "use new-cmd instead", node.DeprecatedMsg)
}

func TestDeprecatedTagEmpty(t *testing.T) {
var cli struct {
OldCmd struct{} `cmd deprecated:""`
}
p := mustNew(t, &cli)
node := p.Model.Children[0]
assert.True(t, node.Deprecated)
assert.Equal(t, "", node.DeprecatedMsg)
}

func TestInvalidRuneErrors(t *testing.T) {
cli := struct {
Flag bool `short:"invalid"`
Expand Down