Cobra Advanced: Persistent Flags, Hooks, Completion
Cobra Advanced: Persistent Flags, Hooks, Completion
Overview
After basics: persistent flags (inherited by children), PreRun/PostRun hooks, required flags, groups, and shell completion. These are the features that justify Cobra over hand-rolled FlagSets.
Persistent flags
var (
cfgFile string
verbose bool
)
func init() {
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose")
// children see --config and -v without redefining
}mytool --verbose get key
mytool get key --verbose # also works for persistent flags
Local flags stay on one command:
getCmd.Flags().Bool("raw", false, "raw")Required flags
getCmd.Flags().String("ns", "", "namespace")
_ = getCmd.MarkFlagRequired("ns")PreRun / PostRun chain
Order for a subcommand:
PersistentPreRun → PreRun → Run → PostRun → PersistentPostRun
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
return loadConfig(cfgFile)
}
getCmd.PreRunE = func(cmd *cobra.Command, args []string) error {
if verbose {
fmt.Fprintln(os.Stderr, "get", args)
}
return nil
}Use PreRun for auth checks, config validation, and client construction.
Bind flags to structs cleanly
type GetOpts struct {
Raw bool
NS string
}
func newGetCmd(store Store) *cobra.Command {
var opts GetOpts
cmd := &cobra.Command{
Use: "get KEY",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return store.Get(cmd.Context(), args[0], opts)
},
}
cmd.Flags().BoolVar(&opts.Raw, "raw", false, "raw")
cmd.Flags().StringVar(&opts.NS, "ns", "default", "namespace")
return cmd
}Factory functions make testing easier than package-level var.
Context on commands
// Cobra sets cmd.Context() from ExecuteContext
func Execute() error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
return rootCmd.ExecuteContext(ctx)
}
// in RunE:
req, _ := http.NewRequestWithContext(cmd.Context(), ...)Command groups (Cobra 1.6+)
rootCmd.AddGroup(&cobra.Group{ID: "core", Title: "Core Commands"})
getCmd.GroupID = "core"
rootCmd.AddCommand(getCmd)Help output clusters commands by group.
Custom usage / version templates
rootCmd.SetVersionTemplate(`{{printf "mytool %s\n" .Version}}`)
rootCmd.Version = buildinfo.Version
// enables --version on root when Version is non-emptyShell completion
// cmd/completion.go
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
default:
return fmt.Errorf("unsupported shell %q", args[0])
}
},
}Dynamic args:
getCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
keys := store.KeysWithPrefix(toComplete)
return keys, cobra.ShellCompDirectiveNoFileComp
}Traverse children for help
rootCmd.SetHelpCommand(helpCmd) // optional customizeExample: parent remote with children
remoteCmd := &cobra.Command{Use: "remote", Short: "Manage remotes"}
remoteAddCmd := &cobra.Command{
Use: "add NAME URL",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
return remotes.Add(args[0], args[1])
},
}
remoteCmd.AddCommand(remoteAddCmd)
rootCmd.AddCommand(remoteCmd)
// mytool remote add origin https://...Testing Cobra commands
func TestHello(t *testing.T) {
cmd := newHelloCmd()
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{"-n", "2", "Ada"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// assert b.String()
}Prefer constructing commands via factories rather than executing package-global rootCmd (mutable).
Rules of thumb
| Do | Don’t |
|---|---|
| Persistent flags for globals | Copy the same flags on 20 commands |
ExecuteContext + signals |
Ignore cancel in long RunE |
| Factories for commands | Global mutable option vars only |
| Completion for public CLIs | Skip docs for install paths |
Try next
- Add
--configpersistent flag and load JSON in PersistentPreRunE. - Mark a flag required; confirm error UX.
- Generate zsh completion and install to your fpath.