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***
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
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)
}
}
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
}
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
}
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
}
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
``\`
====
```
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)
Top comments (0)