Cobra is the de facto standard for building command-line applications in Go. It powers kubectl, hugo, gh, and many other popular tools. Here's how to structure your own CLI.
Project Structure
A well-organized Cobra project separates commands from business logic:
myapp/
├── cmd/
│ ├── root.go # Root command, global flags
│ ├── serve.go # myapp serve
│ └── migrate.go # myapp migrate
├── internal/
│ ├── server/ # Business logic
│ └── database/
├── main.go # Entry point
└── go.mod
The Root Command
Start with a root command that sets up global configuration:
// cmd/root.go
package cmd
var cfgFile string
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "A brief description of your application",
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
}
Adding Subcommands
Each subcommand lives in its own file and registers itself:
// cmd/serve.go
package cmd
var port int
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the HTTP server",
RunE: func(cmd *cobra.Command, args []string) error {
return server.Start(port)
},
}
func init() {
rootCmd.AddCommand(serveCmd)
serveCmd.Flags().IntVarP(&port, "port", "p", 8080, "port to listen on")
}
Flag Types
Cobra supports various flag patterns:
- Persistent flags: Available to command and all subcommands
- Local flags: Only available to the specific command
- Required flags: Must be provided by user
- Flag groups: Mutually exclusive or dependent flags
// Required flag
cmd.Flags().StringVarP(&name, "name", "n", "", "user name (required)")
cmd.MarkFlagRequired("name")
// Mutually exclusive
cmd.MarkFlagsMutuallyExclusive("json", "yaml")
Testing Commands
Cobra commands are testable by capturing output:
func TestServeCommand(t *testing.T) {
buf := new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.SetArgs([]string{"serve", "--port", "9000"})
err := rootCmd.Execute()
assert.NoError(t, err)
}
Conclusion
Cobra handles the boilerplate of CLI parsing so you can focus on functionality. Keep commands thin, delegate to internal packages, and your CLI will be maintainable as it grows.