Skip to content

Commit 9c1482c

Browse files
authored
feat: Redesign CLI help (#3870)
* feat: redesign CLI help * fix: test, go-lint * fix: unit test
1 parent cfdc230 commit 9c1482c

File tree

10 files changed

+199
-63
lines changed

10 files changed

+199
-63
lines changed

cli/app.go

+64-24
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ import (
5454
"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders"
5555
)
5656

57+
// Command category names.
58+
const (
59+
MainCommandsCategoryName = "Main commands"
60+
CatalogCommandsCategoryName = "Catalog commands"
61+
ConfigurationCommandsCategoryName = "Configuration commands"
62+
ShortcutsCommandsCategoryName = "OpenTofu shortcuts"
63+
)
64+
5765
func init() {
5866
cli.AppVersionTemplate = AppVersionTemplate
5967
cli.AppHelpTemplate = AppHelpTemplate
@@ -69,7 +77,7 @@ type App struct {
6977
func NewApp(opts *options.TerragruntOptions) *App {
7078
app := cli.NewApp()
7179
app.Name = "terragrunt"
72-
app.Usage = "Terragrunt is a flexible orchestration tool that allows Infrastructure as Code written in OpenTofu/Terraform to scale. For documentation, see https://terragrunt.gruntwork.io/."
80+
app.Usage = "Terragrunt is a flexible orchestration tool that allows Infrastructure as Code written in OpenTofu/Terraform to scale.\nFor documentation, see https://terragrunt.gruntwork.io/."
7381
app.Author = "Gruntwork <www.gruntwork.io>"
7482
app.Version = version.GetVersion()
7583
app.Writer = opts.Writer
@@ -142,7 +150,7 @@ func (app *App) RunContext(ctx context.Context, args []string) error {
142150
return nil
143151
}
144152

145-
// GlobalFlags returns global flags. For backward compatibility, the slice contains flags that have been moved to other commands and are hidden from the CLI help,
153+
// GlobalFlags returns global flags. For backward compatibility, the slice contains flags that have been moved to other commands and are hidden from the CLI help.
146154
func GlobalFlags(opts *options.TerragruntOptions) cli.Flags {
147155
globalFlags := global.NewFlags(opts, nil)
148156

@@ -158,17 +166,17 @@ func GlobalFlags(opts *options.TerragruntOptions) cli.Flags {
158166
graph.NewCommand(opts), // graph
159167
}
160168

161-
var knownFlags []string
169+
var seen []string
162170

163171
for _, cmd := range commands {
164172
for _, flag := range cmd.Flags {
165173
flagName := util.FirstElement(util.RemoveEmptyElements(flag.Names()))
166174

167-
if slices.Contains(knownFlags, flagName) {
175+
if slices.Contains(seen, flagName) {
168176
continue
169177
}
170178

171-
knownFlags = append(knownFlags, flagName)
179+
seen = append(seen, flagName)
172180
globalFlags = append(globalFlags, flags.NewMovedFlag(flag, cmd.Name, flags.StrictControlsByMovedGlobalFlags(opts.StrictControls, cmd.Name)))
173181
}
174182
}
@@ -202,30 +210,62 @@ func removeNoColorFlagDuplicates(args []string) []string {
202210

203211
// TerragruntCommands returns the set of Terragrunt commands.
204212
func TerragruntCommands(opts *options.TerragruntOptions) cli.Commands {
205-
cmds := cli.Commands{
206-
runCmd.NewCommand(opts), // run
207-
runall.NewCommand(opts), // runAction-all
208-
terragruntinfo.NewCommand(opts), // terragrunt-info
209-
validateinputs.NewCommand(opts), // validate-inputs
213+
mainCommands := cli.Commands{
214+
runall.NewCommand(opts), // run-all
215+
runCmd.NewCommand(opts), // run
216+
stack.NewCommand(opts), // stack
217+
graph.NewCommand(opts), // graph
218+
execCmd.NewCommand(opts), // exec
219+
}.SetCategory(
220+
&cli.Category{
221+
Name: MainCommandsCategoryName,
222+
Order: 10, //nolint: mnd
223+
},
224+
)
225+
226+
catalogCommands := cli.Commands{
227+
catalog.NewCommand(opts), // catalog
228+
scaffold.NewCommand(opts), // scaffold
229+
}.SetCategory(
230+
&cli.Category{
231+
Name: CatalogCommandsCategoryName,
232+
Order: 20, //nolint: mnd
233+
},
234+
)
235+
236+
configurationCommands := cli.Commands{
210237
graphdependencies.NewCommand(opts), // graph-dependencies
211-
hclfmt.NewCommand(opts), // hclfmt
212-
renderjson.NewCommand(opts), // render-json
213-
awsproviderpatch.NewCommand(opts), // aws-provider-patch
214238
outputmodulegroups.NewCommand(opts), // output-module-groups
215-
catalog.NewCommand(opts), // catalog
216-
scaffold.NewCommand(opts), // scaffold
217-
stack.NewCommand(opts), // stack
218-
graph.NewCommand(opts), // graph
239+
validateinputs.NewCommand(opts), // validate-inputs
219240
hclvalidate.NewCommand(opts), // hclvalidate
220-
execCmd.NewCommand(opts), // exec
221-
helpCmd.NewCommand(opts), // help
222-
versionCmd.NewCommand(opts), // version
241+
hclfmt.NewCommand(opts), // hclfmt
223242
info.NewCommand(opts), // info
224-
}
225-
cmds = append(cmds, commands.NewShortcutsCommands(opts)...)
226-
cmds = append(cmds, commands.NewDeprecatedCommands(opts)...)
243+
terragruntinfo.NewCommand(opts), // terragrunt-info
244+
renderjson.NewCommand(opts), // render-json
245+
helpCmd.NewCommand(opts), // help (hidden)
246+
versionCmd.NewCommand(opts), // version (hidden)
247+
awsproviderpatch.NewCommand(opts), // aws-provider-patch (hidden)
248+
}.SetCategory(
249+
&cli.Category{
250+
Name: ConfigurationCommandsCategoryName,
251+
Order: 30, //nolint: mnd
252+
},
253+
)
254+
255+
shortcutsCommands := commands.NewShortcutsCommands(opts).SetCategory(
256+
&cli.Category{
257+
Name: ShortcutsCommandsCategoryName,
258+
Order: 40, //nolint: mnd
259+
},
260+
)
261+
262+
allCommands := mainCommands.
263+
Merge(catalogCommands...).
264+
Merge(configurationCommands...).
265+
Merge(shortcutsCommands...).
266+
Merge(commands.NewDeprecatedCommands(opts)...)
227267

228-
return cmds
268+
return allCommands
229269
}
230270

231271
// WrapWithTelemetry wraps CLI command execution with setting of telemetry context and labels, if telemetry is disabled, just runAction the command.

cli/app_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,7 @@ func TestAutocomplete(t *testing.T) { //nolint:paralleltest
543543
}{
544544
{
545545
"",
546-
[]string{"aws-provider-patch", "graph-dependencies", "hclfmt", "output-module-groups", "render-json", "run-all", "terragrunt-info", "validate-inputs"},
546+
[]string{"graph-dependencies", "hclfmt", "output-module-groups", "render-json", "run-all", "terragrunt-info", "validate-inputs"},
547547
},
548548
{
549549
"--versio",
@@ -566,7 +566,7 @@ func TestAutocomplete(t *testing.T) { //nolint:paralleltest
566566
opts := options.NewTerragruntOptionsWithWriters(output, os.Stderr)
567567
app := cli.NewApp(opts)
568568

569-
app.Commands = app.Commands.Filter([]string{"aws-provider-patch", "graph-dependencies", "hclfmt", "output-module-groups", "render-json", "run-all", "terragrunt-info", "validate-inputs"})
569+
app.Commands = app.Commands.FilterByNames([]string{"graph-dependencies", "hclfmt", "output-module-groups", "render-json", "run-all", "terragrunt-info", "validate-inputs"})
570570

571571
err := app.Run([]string{"terragrunt"})
572572
require.NoError(t, err)

cli/commands/aws-provider-patch/command.go

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ func NewCommand(opts *options.TerragruntOptions) *cli.Command {
6464
return &cli.Command{
6565
Name: CommandName,
6666
Usage: "Overwrite settings on nested AWS providers to work around a Terraform bug (issue #13018).",
67+
Hidden: true,
6768
Flags: append(run.NewFlags(opts, nil), NewFlags(opts, nil)...).Sort(),
6869
Action: func(ctx *cli.Context) error { return Run(ctx, opts.OptionsFromContext(ctx)) },
6970
}

cli/help.go

+13-15
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
11
package cli
22

3-
const AppHelpTemplate = `NAME:
4-
{{$v := offset .App.HelpName 6}}{{wrap .App.HelpName 3}}{{if .App.Usage}} - {{wrap .App.Usage $v}}{{end}}
3+
// AppHelpTemplate is the main CLI help template.
4+
const AppHelpTemplate = `Usage: {{ if .App.UsageText }}{{ wrap .App.UsageText 3 }}{{ else }}{{ .App.HelpName }} [global options] <command> [options]{{ end }}{{ $description := .App.Usage }}{{ if .App.Description }}{{ $description = .App.Description }}{{ end }}{{ if $description }}
55
6-
USAGE:
7-
{{if .App.UsageText}}{{wrap .App.UsageText 3}}{{else}}{{.App.HelpName}} <command> [options]{{end}} {{if .App.Description}}
6+
{{ wrap $description 3 }}{{ end }}{{ $commands := .App.VisibleCommands }}{{ if $commands }}{{ $cv := offsetCommands $commands 5 }}
7+
{{ $categories := $commands.GetCategories.Sort }}{{ range $index, $category := $categories }}{{ $categoryCommands := $commands.FilterByCategory $category }}{{ if $index }}
8+
{{ end }}
9+
{{ $category.Name }}:{{ range $categoryCommands }}
10+
{{ $s := .HelpName }}{{ $s }}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}} {{ wrap .Usage $cv }}{{ end }}{{ end }}{{ end }}{{ if .App.VisibleFlags }}
811
9-
DESCRIPTION:
10-
{{wrap .App.Description 3}}{{end}}{{if .App.VisibleCommands}}
11-
12-
COMMANDS:{{ $cv := offsetCommands .App.VisibleCommands 5}}{{range .App.VisibleCommands}}
13-
{{$s := .HelpName}}{{$s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}} {{wrap .Usage $cv}}{{end}}{{end}}
14-
15-
GLOBAL OPTIONS:{{if .App.VisibleFlags}}
16-
{{range $index, $option := .App.VisibleFlags}}{{if $index}}
17-
{{end}}{{wrap $option.String 6}}{{end}}{{end}}{{if not .App.HideVersion}}
12+
Global Options:
13+
{{ range $index, $option := .App.VisibleFlags }}{{ if $index }}
14+
{{ end }}{{ wrap $option.String 6 }}{{ end }}{{ end }}{{ if not .App.HideVersion }}
1815
19-
VERSION: {{.App.Version}}{{if len .App.Authors}}{{end}}
16+
Version: {{ .App.Version }}{{ end }}{{ if len .App.Authors }}
2017
21-
AUTHOR: {{range .App.Authors}}{{.}}{{end}} {{end}}
18+
Author: {{ range .App.Authors }}{{ . }}{{ end }} {{ end }}
2219
`
2320

21+
// CommandHelpTemplate is the command CLI help template.
2422
const CommandHelpTemplate = `Usage: {{if .Command.UsageText}}{{wrap .Command.UsageText 3}}{{else}}{{range $parent := parentCommands . }}{{$parent.HelpName}} {{end}}{{.Command.HelpName}}{{if .Command.VisibleSubcommands}} <command>{{end}}{{if .Command.VisibleFlags}} [options]{{end}}{{end}}{{$description := .Command.Usage}}{{if .Command.Description}}{{$description = .Command.Description}}{{end}}{{if $description}}
2523
2624
{{wrap $description 3}}{{end}}{{if .Command.Examples}}

internal/cli/app.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ func (app *App) VisibleFlags() Flags {
151151
}
152152

153153
// VisibleCommands returns a slice of the Commands used for help.
154-
func (app *App) VisibleCommands() []*cli.Command {
154+
func (app *App) VisibleCommands() Commands {
155155
if app.Commands == nil {
156156
return nil
157157
}

internal/cli/category.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package cli
2+
3+
import "sort"
4+
5+
// Category represents a command category used to group commands when displaying them.
6+
type Category struct {
7+
// Name is the name of the category.
8+
Name string
9+
// Order is a number indicating the order in the category list.
10+
Order uint
11+
}
12+
13+
// String implements `fmt.Stringer` interface.
14+
func (category *Category) String() string {
15+
return category.Name
16+
}
17+
18+
// Categories is a slice of `Category`.
19+
type Categories []*Category
20+
21+
// Len implements `sort.Interface` interface.
22+
func (categories Categories) Len() int {
23+
return len(categories)
24+
}
25+
26+
// Less implements `sort.Interface` interface.
27+
func (categories Categories) Less(i, j int) bool {
28+
if categories[i].Order == categories[j].Order {
29+
return categories[i].Name < categories[j].Name
30+
}
31+
32+
return categories[i].Order < categories[j].Order
33+
}
34+
35+
// Swap implements `sort.Interface` interface.
36+
func (categories Categories) Swap(i, j int) {
37+
categories[i], categories[j] = categories[j], categories[i]
38+
}
39+
40+
// Sort returns `categories` in sorted order.
41+
func (categories Categories) Sort() Categories {
42+
sort.Sort(categories)
43+
44+
return categories
45+
}

internal/cli/command.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import (
44
"errors"
55
libflag "flag"
66
"strings"
7-
8-
"github.com/urfave/cli/v2"
97
)
108

119
const errFlagUndefined = "flag provided but not defined:"
@@ -23,6 +21,8 @@ type Command struct {
2321
Description string
2422
// Examples is list of examples of using the command in the help.
2523
Examples []string
24+
// Category is the category the command belongs to.
25+
Category *Category
2626
// Flags is list of flags to parse.
2727
Flags Flags
2828
// ErrorOnUndefinedFlag causes the application to exit and return an error on any undefined flag.
@@ -90,7 +90,7 @@ func (cmd *Command) VisibleFlags() Flags {
9090

9191
// VisibleSubcommands returns a slice of the Commands with Hidden=false.
9292
// Used by `urfave/cli` package to generate help.
93-
func (cmd *Command) VisibleSubcommands() []*cli.Command {
93+
func (cmd *Command) VisibleSubcommands() Commands {
9494
if cmd.Subcommands == nil {
9595
return nil
9696
}

internal/cli/command_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -297,15 +297,15 @@ func TestCommandVisibleSubcommand(t *testing.T) {
297297

298298
testCases := []struct {
299299
command cli.Command
300-
expected []*urfaveCli.Command
300+
expected cli.Commands
301301
}{
302302
{
303303
cli.Command{Name: "foo", Subcommands: cli.Commands{&cli.Command{Name: "bar"}, &cli.Command{Name: "baz", HelpName: "helpBaz"}}},
304-
[]*urfaveCli.Command{{Name: "bar", HelpName: "bar"}, {Name: "baz", HelpName: "helpBaz"}},
304+
cli.Commands{{Name: "bar", HelpName: "bar"}, {Name: "baz", HelpName: "helpBaz"}},
305305
},
306306
{
307307
cli.Command{Name: "foo", Subcommands: cli.Commands{&cli.Command{Name: "bar", Hidden: true}, &cli.Command{Name: "baz"}}},
308-
[]*urfaveCli.Command{{Name: "baz", HelpName: "baz"}},
308+
cli.Commands{{Name: "baz", HelpName: "baz"}},
309309
},
310310
}
311311

0 commit comments

Comments
 (0)