35.4 Commands, Subcommands, Flags, and Args with cobra
Right, so you’ve graduated from the simple, idyllic life of flag and you’re ready to build a proper CLI tool, the kind that has commands, subcommands, and help text that doesn’t look like it was generated by a depressed robot. Welcome to cobra. It’s the library behind kubectl, docker, git, and pretty much every other CLI tool that makes you feel simultaneously powerful and slightly intimidated.
Think of cobra not as a library, but as a very opinionated framework for organizing your CLI. A cobra.Command is the core building block, and it can be a root command (myapp), a subcommand (myapp create), or even a sub-subcommand (myapp create user). Each command can have its own flags, its own arguments, and its own logic. It’s a beautiful hierarchy, and it forces you to structure your application logically.
Let’s build something real. Say we’re building a tool to politely bother a web server. We’ll call it poke.
The Root Command: Your Application’s Foundation
First, you define the root command. This is what gets called when someone just runs poke. It’s often used for a general-purpose message or to print the help.
package main
import (
"fmt"
"github.com/spf13/cobra"
"os"
)
var rootCmd = &cobra.Command{
Use: "poke",
Short: "Poke a website and see what happens",
Long: `A longer description that explains we're poking websites to check their status, latency, etc.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hey, you gotta tell me what to do! Try 'poke --help'.")
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func main() {
Execute()
}
Use defines the invocation syntax, Short is for the general help list, and Long is for when someone specifically asks for poke --help. The Run function is the action it executes. This is the simplest possible setup. Build and run it, and it already has -h and --help flags built in. Cobra isn’t messing around.
Adding a Subcommand (The Actual Functionality)
Now, let’s add a status subcommand. This is where the real work happens. You create a new cobra.Command and add it to the root command.
var statusCmd = &cobra.Command{
Use: "status",
Short: "Check the status of a website",
Long: `Check the status of a website by sending it a HTTP HEAD request.`,
Args: cobra.ExactArgs(1), // This is crucial!
Run: func(cmd *cobra.Command, args []string) {
url := args[0]
fmt.Printf("Checking status of %s...\n", url)
// You'd put real HTTP logic here
},
}
func init() {
rootCmd.AddCommand(statusCmd)
}
See that Args: cobra.ExactArgs(1)? This is Cobra’s built-in argument validation. It’s fantastic. You can require a specific number of arguments (ExactArgs), a minimum (MinimumNArgs), a maximum (MaximumNArgs), or even write a custom validator. This check happens before your Run function is ever called, saving you from writing a ton of boilerplate if len(args) < 1 statements. This is a best practice you should adopt religiously.
Defining Flags: Local vs. Persistent
Flags are where Cobra’s design gets interesting, and also where most people get tripped up. There are two types:
- Local Flags: These only apply to that specific command. For our
statuscommand, maybe we want a--timeoutflag. - Persistent Flags: These apply to the command and all of its subcommands. On the root command, a
--verboseflag is a classic example; you’d want it to be available forpoke --verbose status example.comandpoke --verbose create ....
Let’s add both.
func init() {
rootCmd.AddCommand(statusCmd)
// A persistent flag on the root command
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
// A local flag on the status command
statusCmd.Flags().IntP("timeout", "t", 5, "Timeout in seconds for the HTTP request")
}
The P variant (e.g., BoolP) lets you define a shorthand. So -v and --verbose will both work.
Now, the most important part: how to actually read these flags inside your Run function. You don’t use the global flag package. You use the cmd.Flags() methods on the command itself.
Run: func(cmd *cobra.Command, args []string) {
url := args[0]
timeout, _ := cmd.Flags().GetInt("timeout") // Get the local flag
verbose, _ := cmd.Flags().GetBool("verbose") // Get the persistent flag
if verbose {
fmt.Printf("Poking %s with a timeout of %d seconds...\n", url, timeout)
}
fmt.Printf("Checking status of %s...\n", url)
}
Notice the error is ignored with _. In a real application, you should handle that error, but because Cobra itself defines the flag, you know it’s safe to read. This is one of the few places where ignoring the error is acceptable.
The Rough Edges and Pitfalls
Cobra is brilliant, but it’s not perfect. Here’s what to watch out for:
- Configuration Chaos: Cobra works beautifully with
viperfor binding flags to environment variables and config files, but it’s a deep rabbit hole. The complexity can explode if you’re not careful. Start simple. - Over-Engineering: It’s tempting to model every single option as a subcommand. Don’t. If your tool is
poke --url example.com --method GET, that’s a command with flags. If it’spoke check status example.com, that’s a command with subcommands. Use subcommands for actions, not for settings. - Testing: Testing Cobra commands can be… fun. You often have to manipulate
os.Argsdirectly or use theCommand.Execute()method. It’s not impossible, but it requires more setup than testing a simple function. - The Help Command: Cobra adds a default
helpcommand. Sopoke help statusworks. This is great, but it sometimes conflicts if you try to create your own command namedhelp. Just be aware it’s there.
The designers made a choice that I both love and hate: flags are available after the subcommand. This means poke --verbose status example.com works, but poke status --verbose example.com also works. This is POSIX-compliant and flexible, but it can be confusing for users who expect flags to come first. There’s no way to change this behavior, so you just have to roll with it.
The end result? A CLI tool that behaves exactly like the pros. It’s worth the initial setup. Now go build something people will actually want to use.