spf13 / cobra

A Commander for modern Go CLI interactions
https://cobra.dev
Apache License 2.0
38.41k stars 2.86k forks source link

Need help with SetUsageTemplate() and Flags #2157

Closed jftuga closed 5 months ago

jftuga commented 5 months ago

If I have the following:

rootCmd.PersistentFlags().StringVarP(&start, "start", "s", "", "start date, time")
rootCmd.PersistentFlags().StringVarP(&end, "end", "e", "", "end date, time")
rootCmd.PersistentFlags().BoolVarP(&noNewline, "nonewline", "n", false, "do not output a newline character")
rootCmd.PersistentFlags().BoolVarP(&readFromStdin, "stdin", "i", false, "read from STDIN instead of using -s and -e")

I want to create a custom Usage message with SetUsageTemplate() that would look like:

Globals:
    -n, --nonewline      do not output a newline character
    -i, --stdin          read from STDIN instead of using -s and -e

Flags:
    -s, --start string   start date, time
    -e, --end string     end date, time

How would I reference individual flags?

All I see is something like this:

const usageTemplate string = `Usage:{{if .Runnable}}
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
{{.CommandPath}} [command]{{end}}

Global:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}

Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}
`

I obviously need to replace this:

{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}

but I am not sure what to replace it with. How could I do this?

Thank you.

marckhouzam commented 5 months ago

You can look at what LocalFlags.FlagUsages does by looking at the Go functions LocalFlags() and then FlagUsages(). The second one comes from the pflag project

jftuga commented 5 months ago

With the assistance of ChatGPT, I have this working now. I wanted to document this in case some one else runs into the same problem. For my program I am actually using three groups of flags: Globals, Flag Group 1, and Flag Group 2. Forget for a moment, that I should just be cobra commands instead. 😃 FWIW, this is at the very least a good example of how to customize your flags.


// this constant was generated by ChatGPT and then manually refined
const usageTemplate string = `Usage:{{if .Runnable}}
 {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
 {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
Aliases:
 {{.NameAndAliases}}{{end}}{{if .HasExample}}
Examples:
 {{.Example}}{{end}}{{if .HasAvailableSubCommands}}
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
 {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}

Globals:
{{FlagUsagesCustom .LocalFlags "nonewline" "help" "version" | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableLocalFlags}}

Flag Group 1 (mutually exclusive with Flag Group 2):
{{FlagUsagesCustom .LocalFlags "start" "end" "stdin" | trimTrailingWhitespaces}}

Flag Group 2:
{{FlagUsagesCustom .LocalFlags "from" "add" "sub" | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}

Global Flags:
 {{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
 {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
`

This function is also needed. The tabs portion is a little clunky but seems to work for my use case:

// FlagUsagesCustom customized to filter and format flags with types
// this function was generated by ChatGPT and then manually refined
func FlagUsagesCustom(flags *pflag.FlagSet, names ...string) string {
    var buf bytes.Buffer
    flags.VisitAll(func(flag *pflag.Flag) {
        for _, name := range names {
            if flag.Name == name {
                shorthand := ""
                if flag.Shorthand != "" {
                    shorthand = fmt.Sprintf("-%s, ", flag.Shorthand)
                }
                name := flag.Name
                if flag.Value.Type() == "string" {
                    name += " string"
                }
                tabs := "\t"
                if len(name) <= 7 {
                    tabs = "\t\t"
                }
                fmt.Fprintf(&buf, "  %s--%s%s%s\n", shorthand, name, tabs, flag.Usage)
            }
        }
    })
    return buf.String()
}

Inside of the init() function, after all of the flags and exclusions have been defined, I have this code:

// Register the custom template function
cobra.AddTemplateFunc("FlagUsagesCustom", func(flags *pflag.FlagSet, names ...string) string {
    return FlagUsagesCustom(flags, names...)
})

// Set the custom usage template
rootCmd.SetUsageTemplate(usageTemplate)