diff --git a/lib/utils/cli.go b/lib/utils/cli.go index 75e4f7167a9..1b2e685cb7a 100644 --- a/lib/utils/cli.go +++ b/lib/utils/cli.go @@ -292,7 +292,55 @@ func InitCLIParser(appName, appHelp string) (app *kingpin.Application) { app.HelpFlag.NoEnvar() // set our own help template - return app.UsageTemplate(defaultUsageTemplate) + return app.UsageTemplate(createUsageTemplate()) +} + +// createUsageTemplate creates an usage template for kingpin applications. +func createUsageTemplate(opts ...func(*usageTemplateOptions)) string { + opt := &usageTemplateOptions{ + commandPrintfWidth: defaultCommandPrintfWidth, + } + + for _, optFunc := range opts { + optFunc(opt) + } + return fmt.Sprintf(defaultUsageTemplate, opt.commandPrintfWidth) +} + +// UpdateAppUsageTemplate updates usage template for kingpin applications by +// pre-parsing the arguments then applying any changes to the usage template if +// necessary. +func UpdateAppUsageTemplate(app *kingpin.Application, args []string) { + // If ParseContext fails, kingpin will not show usage so there is no need + // to update anything here. See app.Parse for more details. + context, err := app.ParseContext(args) + if err != nil { + return + } + + app.UsageTemplate(createUsageTemplate( + withCommandPrintfWidth(app, context), + )) +} + +// withCommandPrintfWidth returns an usage template option that +// updates command printf width if longer than default. +func withCommandPrintfWidth(app *kingpin.Application, context *kingpin.ParseContext) func(*usageTemplateOptions) { + return func(opt *usageTemplateOptions) { + var commands []*kingpin.CmdModel + if context.SelectedCommand != nil { + commands = context.SelectedCommand.Model().FlattenedCommands() + } else { + commands = app.Model().FlattenedCommands() + } + + for _, command := range commands { + if !command.Hidden && len(command.FullCommand) > opt.commandPrintfWidth { + opt.commandPrintfWidth = len(command.FullCommand) + } + } + + } } // SplitIdentifiers splits list of identifiers by commas/spaces/newlines. Helpful when @@ -380,8 +428,19 @@ func needsQuoting(text string) bool { return false } -// Usage template with compactly formatted commands. -var defaultUsageTemplate = `{{define "FormatCommand"}}\ +// usageTemplateOptions defines options to format the usage template. +type usageTemplateOptions struct { + // commandPrintfWidth is the width of the command name with padding, for + // {{.FullCommand | printf "%%-%ds"}} + commandPrintfWidth int +} + +// defaultCommandPrintfWidth is the default command printf width. +const defaultCommandPrintfWidth = 12 + +// defaultUsageTemplate is a fmt format that defines the usage template with +// compactly formatted commands. Should be only used in createUsageTemplate. +const defaultUsageTemplate = `{{define "FormatCommand"}}\ {{if .FlagSummary}} {{.FlagSummary}}{{end}}\ {{range .Args}} {{if not .Required}}[{{end}}<{{.Name}}>{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}\ {{end}}\ @@ -389,7 +448,7 @@ var defaultUsageTemplate = `{{define "FormatCommand"}}\ {{define "FormatCommands"}}\ {{range .FlattenedCommands}}\ {{if not .Hidden}}\ - {{.FullCommand | printf "%-12s" }}{{if .Default}} (Default){{end}} {{ .Help }} + {{.FullCommand | printf "%%-%ds"}}{{if .Default}} (Default){{end}} {{ .Help }} {{end}}\ {{end}}\ {{end}}\ diff --git a/tool/tbot/main.go b/tool/tbot/main.go index c38b0e525c8..484a73f97ca 100644 --- a/tool/tbot/main.go +++ b/tool/tbot/main.go @@ -71,6 +71,7 @@ func Run(args []string) error { app := utils.InitCLIParser("tbot", "tbot: Teleport Machine ID").Interspersed(false) app.Flag("debug", "Verbose logging to stdout").Short('d').BoolVar(&cf.Debug) app.Flag("config", "Path to a configuration file. Defaults to `/etc/tbot.yaml` if unspecified.").Short('c').StringVar(&cf.ConfigPath) + app.HelpFlag.Short('h') versionCmd := app.Command("version", "Print the version") @@ -97,6 +98,7 @@ func Run(args []string) error { watchCmd := app.Command("watch", "Watch a destination directory for changes.").Hidden() + utils.UpdateAppUsageTemplate(app, args) command, err := app.Parse(args) if err != nil { app.Usage(args) diff --git a/tool/tctl/common/tctl.go b/tool/tctl/common/tctl.go index ea7c8855eeb..12010a012cd 100644 --- a/tool/tctl/common/tctl.go +++ b/tool/tctl/common/tctl.go @@ -137,6 +137,7 @@ func Run(commands []CLICommand) { app.HelpFlag.Short('h') // parse CLI commands+flags: + utils.UpdateAppUsageTemplate(app, os.Args[1:]) selectedCmd, err := app.Parse(os.Args[1:]) if err != nil { app.Usage(os.Args[1:]) diff --git a/tool/teleport/common/teleport.go b/tool/teleport/common/teleport.go index 6128d732587..8bd6bc0a2b0 100644 --- a/tool/teleport/common/teleport.go +++ b/tool/teleport/common/teleport.go @@ -294,6 +294,7 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con dump.Flag("key-file", "Path to a TLS key file for the proxy.").ExistingFileVar(&dumpFlags.KeyFile) // parse CLI commands+flags: + utils.UpdateAppUsageTemplate(app, options.Args) command, err := app.Parse(options.Args) if err != nil { app.Usage(options.Args) diff --git a/tool/tsh/tsh.go b/tool/tsh/tsh.go index d24e890d6fd..0e5285d080b 100644 --- a/tool/tsh/tsh.go +++ b/tool/tsh/tsh.go @@ -638,6 +638,7 @@ func Run(args []string, opts ...cliOption) error { } // parse CLI commands+flags: + utils.UpdateAppUsageTemplate(app, args) command, err := app.Parse(args) if err != nil { app.Usage(args)