When I first entered the world of Go, I had trouble thinking what are the best use-cases for the language. I knew that I could build backends for web applications or microservices, but I've always thought that it's perfect for building CLI tools. Some time ago I came across a library that makes building those in Go super easy. Let's build such a tool using Steve Francia's cobra.

taking a page from the best

According to githut.info there are over 22k Go repositories on Github. By that means, you can easily find a library that does what you need to do, but it might not be as good as you would like. The best way to measure how good certain repository is is to see who is using that and who made it. No, don't look at the Github stars only. With spr13/cobra, all three conditions are met. First of all, Steve Francia knows a thing or two about Go and creating great stuff, as he built another awesome tool called hugo which is a static site generator. If that doesn't convince you, take a look at who uses cobra to build their CLI tools: Docker, Kubernetes, CockroachDB, and more. If it's good enough for them, you can live with it as well.

Getting started

In order to get started, you just need to browse through project's readme. It has literally everything you need to know to build your first tool, which makes working with cobra so awesome. To make the example easier to follow, we'll use the recommended directory layout of main.go initializing root command, and cmd/ directory containing everything else.

It all starts with that root command, where we define its name and what does it do. We will, obviously, start with a hello world tool, duh. It will be named hello and that's what we defined as command's Use. This is important later on when the users decide to check --help of our application. For now, calling hello should just print a simple greeting.

// cmd/root.go
...
var RootCmd = &cobra.Command{
    Use: "hello",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Hello world :)")
    },
}

Now when we have the command in place, we need to call it from main.go file:

// main.go
...
func main() {
    if err := cmd.RootCmd.Execute(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

When we build the binary from our sources, we can start using our very first CLI tool. It's not the most complicated thing ever, and for now, it doesn't have any advantage over the default snippet from Go Playground:

$ hello
Hello world :)

Subcommands

The fun begins when we add subcommands to our root. Let's say that we want to be able to greet Martians as well, so we add a subcommand to the RootCmd:

// cmd/root.go
...
func init() {
    RootCmd.AddCommand(GreetPlanetCmd)
}

Then we define the mars subcommand and print an appropriate greeting there:

// cmd/planet.go
...
var GreetPlanetCmd = &cobra.Command{
    Use: "mars",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Hello Mars :)")
    },
}

After building the binary, we see our app gaining some flexibility:

$ hello
Hello world :)
$ hello mars
Hello Mars :)

Altering behavior with flags

It would be very sad to build tools that are static and that we are not able to alter their behavior anyhow. Fortunately, we can add flags, so that our greetings can be written in other languages as well. Who said that the Martians would prefer to speak English?

Let's create a dummy dictionary, which will allow us to use three languages, English (by default), Spanish and Polish:

// cmd/greeting.go
func greeting(lang string) string {
    switch lang {
    case "pl":
        return "Cześć"
    case "es":
        return "Hola"
    default: // "en"
        return "Hello"
    }
}

Then we need to add a flag to RootCmd and make it persistent, so tht it's also accessible in subcommands:

// cmd/root.go
func init() {
    RootCmd.PersistentFlags().String("lang", "en", "language to use")
    RootCmd.AddCommand(GreetPlanetCmd)
}

Then we access the flag's value using cmd.Flag("lang").Value call, pass that to the dictionary and print an appopriate greeting:

var RootCmd = &cobra.Command{
    Use: "hello",
    Run: func(cmd *cobra.Command, args []string) {
        lang := cmd.Flag("lang").Value.String()
        fmt.Printf("%s world :)\n", greeting(lang))
    },
}

Now when calling the binary we can see our CLI app getting some shape.

$ hello --lang es
Hola world :)
$ hello mars -lang pl
Cześć Mars :)

Full-dynamic with arguments

Last but not least, we can make our app handle fully-dynamic arguments, not only flags. For example, we might be ok with a defined set of supported languages, but what if we want to greet some unknown travelers? Where did they come from? We can't be sure when building the application, so let's be flexible.

To use arguments, you just need to read the contents of args slice that is automatically added to all commands. Basically, everything that comes after a command name is treated as an argument:

var GreetTravellerCmd = &cobra.Command{
    Use: "traveller",
    Run: func(cmd *cobra.Command, args []string) {
        lang := cmd.Flag("lang").Value.String()
        greet := greeting(lang)

        if len(args) == 0 {
            fmt.Printf("%s travelers!\n", greet)
            os.Exit(0)
        }

        fmt.Printf("%s travelers from %s!\n", greet, args[0])
    },
}

If you don't specify where our new friends are from, we have some generic greeting ready. If we know their origin, we can make them feel even more welcome:

$ hello traveler
Hello travelers!
$ hello traveler Boston
Hello travelers from Boston!

Summary

As you can see, with a couple of simple tricks and commands we were able to build a small CLI tool in Go using cobra. The possibilities are endless, as long as you find a problem that can be solved using the command line and some smart set or actions, flags and arguments. Next time I'll post an example of the application built on cobra that solved some real-life problems for me.

The full source code of this example is available on Github.