How I write Golang CLI tools today (using Kong)

Thu, Aug 1, 2024 3-minute read

How I write Golang CLI tools today (using Kong)

I’ve ditched cobra for alecthomas/kong for good. Smaller, easier to grok, great interface design.

This is a short snippet on how I layout a basic CLI (which I do for every project, nearly). CLI’s power most of my app’s, including web servers.

Here is the most simple layout of a useless but instructive example CLI.

.
├── cmd
│   └── app
│       └── main.go
├── go.mod
├── go.sum
└── internal
    └── cmd
        ├── cmd.go
        └── echo.go

Each *.go file in detail.

// cmd/app/main.go

package main

import (
        "fmt"
        "me/my-cli/internal/cmd"
        "os"

        "github.com/alecthomas/kong"
)

const appName = "my-cli"

var version string

type VersionFlag string

func (v VersionFlag) Decode(_ *kong.DecodeContext) error { return nil }
func (v VersionFlag) IsBool() bool                       { return true }
func (v VersionFlag) BeforeApply(app *kong.Kong, vars kong.Vars) error {
        fmt.Println(vars["version"])
        app.Exit(0)
        return nil
}

type CLI struct {
        cmd.Globals

        Echo cmd.EchoCommand `cmd:"" help:"Example of an Echo command"`
        Version VersionFlag `       help:"Print version information and quit" short:"v" name:"version"`
}

func run() error {
        if version == "" {
                version = "development"
        }
        cli := CLI{
                Version: VersionFlag(version),
        }
        // Display help if no args are provided instead of an error message
        if len(os.Args) < 2 {
                os.Args = append(os.Args, "--help")
        }

        ctx := kong.Parse(&cli,
                kong.Name(appName),
                kong.Description("My new CLI"),
                kong.UsageOnError(),
                kong.ConfigureHelp(kong.HelpOptions{
                        Compact: true,
                }),
                kong.DefaultEnvars(appName),
                kong.Vars{
                        "version": string(cli.Version),
                })
        err := ctx.Run(&cli.Globals)
        ctx.FatalIfErrorf(err)
        return nil
}

func main() {
        if err := run(); err != nil {
                os.Exit(1)
        }
}
// internal/cmd/cmd.go

package cmd

type Globals struct {
        Format string `help:"Output format" default:"console" enum:"console,json"`
}
// internal/cmd/cmd.go

package cmd

import (
        "encoding/json"
        "fmt"
)

type EchoCommand struct {
        Text string `arg:"" help:"text to echo"`
}

type Formatter interface {
        Output(text string) string
}

type OutputFunc func(text string) string

func (o OutputFunc) Output(text string) string {
        return o(text)
}

func echoMessage(text string, format Formatter) {
        fmt.Println(format.Output(text))
}

func (e *EchoCommand) Run(g *Globals) error {
        ConsoleFormatted := OutputFunc(func(text string) string {
                return text
        })
        JSONFormatted := OutputFunc(func(text string) string {
                jsonData, _ := json.Marshal(map[string]string{"message": text})
                return string(jsonData)
        })

        if g.Format == "json" {
                echoMessage(e.Text, JSONFormatted)
        }
        if g.Format == "console" {
                echoMessage(e.Text, ConsoleFormatted)
        }

        return nil
}

Once these files exist, from the root level, run go run cmd/app/main.go and it will output:

Usage: my-cli <command> [flags]

My new CLI

Flags:
  -h, --help                Show context-sensitive help.
      --format="console"    Output format ($MY-CLI_FORMAT)
  -v, --version             Print version information and quit ($MY-CLI_VERSION)

Commands:
  echo    Example of an Echo command

Run "my-cli <command> --help" for more information on a command.

The echo command can be called with:

go run cmd/app/main.go echo "this is going to be written to the console"
# this is going to print to the console

and to print JSON:

go run cmd/app/main.go echo --format=json "this is going to print out some json"
# {"message":"this is going to written to the console"}

Tags:

#go #cli #kong