In this blog, we are diving deep into the world of developing advanced command-line applications using the popular CLI framework, Cobra.

Generating A CLI Application with Cobra in Golang


In this blog, we are diving deep into the world of developing advanced command-line applications using the popular CLI framework, Cobra. Before we delve into Cobra, we’ll briefly explore alternative tools, so you understand all the options available to you and can pick the one that fits your use-case.

Let’s jump in!

Comparing CLI Frameworks

urfave/cli vs. Cobra

Before we commit to using Cobra, it’s essential to compare it against to other popular CLI frameworks. One of the most popular is urfave/cli. This comparison will help determine which framework aligns best with the needs of your project.

Cobra is renowned for its powerful features that enables you to build complex command-line applications. It supports nested commands, automatic help generation, and boasts a robust ecosystem, including seamless integration with Viper for handling configurations. Despite its complexity, Cobra is well-documented and supported, with over 35,000 stars on GitHub.

urfave/cli on the other hand, emphasizes simplicity and composability. It offers a straightforward approach to command-line application development with a focus on flag and argument parsing. It also has a substantial community base with over 20,000 stars on GitHub.

Choosing between these frameworks depends on several factors. Cobra is well-suited for complex applications requiring sophisticated configurations, whereas urfave/cli may be more appropriate for simpler projects. Familiarity with the framework among your development team can also be a significant advantage, so take that into consideration!

Getting Started with Cobra

To illustrate the capabilities of Cobra, we’ll outline a health check CLI tool using cobra-cli to speed up development. The goal of this application is to take a list of URLs and check to see if they are “up”, which in most cases means returning a 200 http status code.

Initializing the Project

First, ensure you have the Cobra CLI tool installed. Confirm the installation by running cobra-cli in your terminal. If the general help text and usage are printed, it’s installed correctly. If this is your first time using cobra, you can install it by running

go install github.com/spf13/cobra-cli@latest

Make a new folder called healthcheck.

mkdir healthcheck && cd healthcheck

Initialize your project with Go mod init healthcheck and then set up your Cobra application:

cobra-cli init

This command generates a cmd/root.go file (as well as others), which we’ll customize by adding short and long descriptions to our root command.

Creating a Logger

One of Cobra’s benefits is its support for comprehensive logging. We’ll create a new logger using the popular slog package.

  1. Create a logger folder and file:

    // logger/logger.go
    
  2. Define the handlers:

    package logger
    
    import (
        "context"
        "log/slog"
        "os"
    )
    
    type MultiWriterHandler struct {
        StdoutHandler slog.Handler
        FileHandler   slog.Handler
    }
    
    func (m *MultiWriterHandler) Enabled(ctx context.Context, level slog.Level) bool {
        return true
    }
    
    func (m *MultiWriterHandler) Handle(ctx context.Context, r slog.Record) error {
        if err := m.StdoutHandler.Handle(ctx, r); err != nil {
            return err
        }
        if err := m.FileHandler.Handle(ctx, r); err != nil {
            return err
        }
        return nil
    }
    
    func (m *MultiWriterHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
        return &MultiWriterHandler{
            StdoutHandler: m.StdoutHandler.WithAttrs(attrs),
            FileHandler:   m.FileHandler.WithAttrs(attrs),
        }
    }
    
    func (m *MultiWriterHandler) WithGroup(name string) slog.Handler {
        return &MultiWriterHandler{
            StdoutHandler: m.StdoutHandler.WithGroup(name),
            FileHandler:   m.FileHandler.WithGroup(name),
        }
    }
    
    func NewMultiWriterHandler(logFile string) *MultiWriterHandler {
        file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
        if err != nil {
            slog.Error("Filed to open log file", "error", err, "path", logFile)
            return nil
        }
    
        return &MultiWriterHandler{
            StdoutHandler: slog.NewTextHandler(os.Stdout, nil),
            FileHandler:   slog.NewTextHandler(file, nil),
        }
    }
    
    func NewLogger(logFile string) *slog.Logger {
        handler := NewMultiWriterHandler(logFile)
        return slog.New(handler)
    }
    

Setting Up the Root Command

Define global variables for logging in root.go and set up a persistent pre-run command to initialize the logger. Once you are done, your root.go file should look like this:

package cmd

import (
   logger2 "healthcheck/logger"
   "log/slog"
   "os"

   "github.com/spf13/cobra"
)

var (
   logFile string
   l       *slog.Logger
)

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
   Use:   "healthcheck",
   Short: "A brief description of your application",
   Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
   PersistentPostRun: func(cmd *cobra.Command, args []string) {
      l = logger2.NewLogger(logFile)
   },
}

// Execute adds all child commands to the root command and sets flags appropriately.
func Execute() {
   err := rootCmd.Execute()
   if err != nil {
      os.Exit(1)
   }
}

func init() {
   rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
   rootCmd.PersistentFlags().StringVar(&logFile, "logfile", "healthcheck.log", "File to log output to")
}

Adding Commands

This is where we can now add a healthcheck command. This can be an exercise for you, dear reader. If you are struggling, we cover off how to build this plus many more CLI projects in our course.

// cmd/check.go
var checkCmd = &cobra.Command{
   Use:   "check [urls]",
   Short: "Check the health of URLs",
   RunE: func(cmd *cobra.Command, args []string) error {
       // Implementation for checking URLs
   },
}

func init() {
   rootCmd.AddCommand(checkCmd)
}

Conclusion

In this blog, we’ve covered the essential steps to structure a CLI application using Cobra. From project initialization to setting up commands, Cobra proves to be a powerful and flexible framework for building robust CLI tools.

From here, in our course we add the following features:

  • Adding further command hooks
  • Using the context to handle global cancellation.
  • Creating a version command.

We hope you found this helpful, and you now learnt everything you need to know to create a Cobra CLI in Go!