DEV Community

Luke Hagar
Luke Hagar

Posted on

Writing better Cobra CLI help messages with glamour

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

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***
Enter fullscreen mode Exit fullscreen mode

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

But then I discovered 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

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.

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)
    }

}
Enter fullscreen mode Exit fullscreen mode

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.

func RenderMarkdown(markdown string) string {
    out, err := renderer.Render(markdown)
    if err != nil {
        panic(err)
    }

    return out
}
Enter fullscreen mode Exit fullscreen mode

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

type Help struct {
    Long    string
    Example string
}
Enter fullscreen mode Exit fullscreen mode

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


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
}
Enter fullscreen mode Exit fullscreen mode

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.

==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 
``\`
====
Enter fullscreen mode Exit fullscreen mode

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

// 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,
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)