Writing better Cobra CLI help messages with glamour
Tags: go, cobra, cli, help
Published: 2023-11-05 05:14:03

Writing Cobra CLI help messages can be quite a pain, especially when styling and formatting it exactly how you want.

Wouldn't it be better to use Markdown?

Thanks to Charm, and their package Glamour, we can.


## Existing Help structure

Below is an example of a typical Cobra command structure
```go
cmd := &cobra.Command{
	Use:     "create",
	Short:   "Create an IdentityNow Transform from a file",
	Long:    "\nCreate an IdentityNow Transform from a file\n\n",
	Example: "sail transform c -f /path/to/transform.json\nsail transform c < /path/to/transform.json\necho /path/to/transform.json | sail transform c",
	Aliases: []string{"c"},
	Args:    cobra.NoArgs,
	RunE: ***TRUNCATED***
```

This was the way I structured my commands on the CLI I was working on for quite some time.

But then I discovered [Glamour](https://github.com/charmbracelet/glamour), a Stylesheet-based markdown renderer for GO CLI apps.

I looked at some of the examples in the repo, I came across this example photo
![glamour markdown example photo](https://github.com/charmbracelet/glamour/blob/master/examples/helloworld/helloworld.png?raw=true)

And I pretty much fell in love with how it looked. I decided I wanted my help messages to look like that.

## Building a better mousetrap

With the usage of glamour being so simple I decided I wanted to integrate it into the existing cobra framework as simply as possible. This led to me building 4 functions to scaffold this out in my CLI.

First I started with the renderer variable I wanted to use, and an `init()` function to instantiate it.

```go
var renderer *glamour.TermRenderer

func init() {
	var err error
	renderer, err = glamour.NewTermRenderer(
		// Detect the background color and pick either the default dark or light theme
		glamour.WithAutoStyle(),
	)
	if err != nil {
		panic(err)
	}

}
``` 

This configures a glamour Renderer with almost completely default settings but with the wonderful addition of automatic styling based on the terminal background.

With the renderer defined, I can build the Markdown render function. I elected to have it panic on render error.
```go
func RenderMarkdown(markdown string) string {
	out, err := renderer.Render(markdown)
	if err != nil {
		panic(err)
	}

	return out
}
```

Next, I want to define the different parts of the cobra help I want to format in markdown and put those in a struct. Then I want to figure out how I want to split up a single markdown file into those different help sections parts. 

First the help struct
```go
type Help struct {
	Long    string
	Example string
}
```


Then the help parsing function. I decided on a regex query with a capture group to aid in a more readable markdown file.
```go

func ParseHelp(help string) Help {
	helpParser, err := regexp.Compile(`==([A-Za-z]+)==([\s\S]*?)====`)
	if err != nil {
		panic(err)
	}

	matches := helpParser.FindAllStringSubmatch(help, -1)

	var helpObj Help
	for _, set := range matches {
		switch strings.ToLower(set[1]) {
		case "long":
			helpObj.Long = RenderMarkdown(set[2])
		case "example":
			helpObj.Example = RenderMarkdown(set[2])
		}
	}

	return helpObj
}
```


Now finally, putting all these together, and adding markdown formatting to a Cobra command is dead simple.

Below is an example of a commands help markdown file. I escaped the final backtick on the nested examples for the purpose of demonstration.

```md
==Long==
# Parse

Parse Log Files from SailPoint Virtual Appliances
====

==Example==

## Parsing CCG Logs: 

All the errors will be parsed out of the log file and sorted by date and connector name.

Supplying the `--all` flag will parse all the log traffic out, not just errors.

``\`bash 
sail va parse --type ccg ./path/to/ccg.log ./path/to/ccg.log 
sail va parse --type ccg ./path/to/ccg.log ./path/to/ccg.log --all
``\`

## Parsing CANAL Logs: 

``\`bash
sail va parse --type canal ./path/to/canal.log ./path/to/canal.log 
``\`
====
```

Parsing this file for the command is as easy as pie.

```go
// Embed the markdown file for easy inclusion of a variable. 
//go:embed parse.md
var parseHelp string

func newParseCommand() *cobra.Command {
        // Parse the embedded file into its separate help components
	help := util.ParseHelp(parseHelp)
	var fileType string
	var all bool
	cmd := &cobra.Command{
		Use:     "parse",
		Short:   "Parse Log Files from SailPoint Virtual Appliances",
                // Simply pass the parts of the help struct on to the corresponding Cobra value.
		Long:    help.Long,
		Example: help.Example,
```

This final implementation leaves us with some very styling help messages, with all the hard work thankfully handled by Glamour and Cobra


![example CLI command help](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rbwmmzi4aw8mnrr2bi3e.png)