I have been working on getting READMEs to autogenerate for me for some Cobra CLI programs. Initially things went well and I got the first README to generate just fine. However for some reason the second attempt failed using the same function. So after several attempts at getting it to work, I decided to post a question here.
All of the code can be found here with a Makefile (make generate is the command to generate the README files). The template that is not generating is coming up as an empty string for some reason.
The same code is used for the README that gets generated and the one that is just an empty string. Here is the shared code:
package cmdhandler
import (
"bufio"
"bytes"
"errors"
"fmt"
"strings"
"text/template"
"github.com/MakeNowJust/heredoc"
"github.com/davecgh/go-spew/spew"
cmdtomd "github.com/pjkaufman/go-go-gadgets/pkg/cmd-to-md"
filehandler "github.com/pjkaufman/go-go-gadgets/pkg/file-handler"
"github.com/pjkaufman/go-go-gadgets/pkg/logger"
"github.com/spf13/cobra"
)
var generationDir string
const GenerationDirArgEmpty = "generation-dir must have a non-whitespace value"
type TmplData struct {
CommandStrings string
Todos []string
Description string
Title string
CustomValues map[string]any
}
func AddGenerateCmd(rootCmd *cobra.Command, title, description string, todos []string, getCustomValues func(string) (map[string]any, error)) {
var generateCmd = &cobra.Command{
Use: "generate",
Short: "Generates the readme file for the program",
Example: heredoc.Doc(`
` + rootCmd.Use + ` generate -d ./` + rootCmd.Use + `
will look for a file called README.md.tmpl and if it is found generate a readme based
on that file and the file command info.
`),
Run: func(cmd *cobra.Command, args []string) {
err := ValidateGenerateFlags(generationDir)
if err != nil {
logger.WriteError(err.Error())
}
err = filehandler.FolderMustExist(generationDir, "generation-dir")
if err != nil {
logger.WriteError(err.Error())
}
tmpl, err := template.ParseFiles(filehandler.JoinPath(generationDir, "README.md.tmpl"))
if err != nil {
logger.WriteError(err.Error())
}
var customValues = make(map[string]any)
if getCustomValues != nil {
customValues, err = getCustomValues(generationDir)
if err != nil {
logger.WriteError(err.Error())
}
}
var b bytes.Buffer
writer := bufio.NewWriter(&b)
err = tmpl.Execute(writer, TmplData{
CommandStrings: cmdtomd.RootToMd(rootCmd),
Todos: todos,
Description: description,
Title: title,
CustomValues: customValues,
})
if err != nil {
logger.WriteError(err.Error())
}
spew.Dump(TmplData{
CommandStrings: cmdtomd.RootToMd(rootCmd),
Todos: todos,
Description: description,
Title: title,
CustomValues: customValues,
})
err = filehandler.WriteFileContents(filehandler.JoinPath(generationDir, "README.md"), b.String())
if err != nil {
logger.WriteError(err.Error())
}
},
}
rootCmd.AddCommand(generateCmd)
generateCmd.Flags().StringVarP(&generationDir, "generation-dir", "g", "", "the path to the base folder of the "+rootCmd.Use+" program source code")
err := generateCmd.MarkFlagRequired("generation-dir")
if err != nil {
logger.WriteError(fmt.Sprintf(`failed to mark flag "generation-dir" as required on generate command: %v`, err))
}
// keep from showing up in the output of the command generation
generateCmd.Hidden = true
}
func ValidateGenerateFlags(generationDir string) error {
if strings.TrimSpace(generationDir) == "" {
return errors.New(GenerationDirArgEmpty)
}
return nil
}
The code that is using this logic and not working properly:
//go:build generate
package cmd
import (
cmdhandler "github.com/pjkaufman/go-go-gadgets/pkg/cmd-handler"
)
const (
title = "Jpeg and Png Processor"
description = `This is meant to be a replacement for my usage of imgp.
Currently I use imgp for the following things:
- image resizing
- exif data removal
- image quality setting
Given how this works, I find it easier to just go ahead and do a simple program in Go to see how things stack up and not be so reliant on Python. This also helps me learn some more about imaging processing as well. So a win-win in my book.`
)
func init() {
cmdhandler.AddGenerateCmd(rootCmd, title, description, []string{
"Resize png test",
}, nil)
}
The template that is passed in is:
<!-- This file is generated from https://github.com/pjkaufman/go-go-gadgets/jp-proc/README.md.tmpl. Please make any necessary changes there. -->
# {{ .Title }}
{{ .Description }}
## How does this program compare with imgp?
| Operation | Original Size | New Size (imgp) | New Size (imgp with optimize flag) | New Size (jp-proc) |
| --------- | ------------- | --------------- | ---------------------------------- | ------------------ |
| Resize jpeg to 800x600 and remove exif data | 3.4M | 57KB | 56KB | 68KB |
| Resize jpeg to 800x600 and remove exif data and set quality to 40 | 3.4M | 32KB | 28KB | 37KB |
{{- if .Todos }}
## TODOs
{{- range .Todos }}
- {{ . }}
{{- end}}
{{- end}}
## Commands
{{ .CommandStrings }}
I made a sample program with the same template and it does work, but for some reason I cannot get the other setup to work. Here is the working standalone program:
package main
import (
"log"
"os"
"text/template"
)
const format = `# {{ .Title }}
{{ .Description }}
## How does this program compare with imgp?
| Operation | Original Size | New Size (imgp) | New Size (imgp with optimize flag) | New Size (jp-proc) |
| --------- | ------------- | --------------- | ---------------------------------- | ------------------ |
| Resize jpeg to 800x600 and remove exif data | 3.4M | 57KB | 56KB | 68KB |
| Resize jpeg to 800x600 and remove exif data and set quality to 40 | 3.4M | 32KB | 28KB | 37KB |
{{- if .Todos }}
## TODOs
{{- range .Todos }}
- {{ . }}
{{- end}}
{{- end}}
## Commands
{{ .CommandStrings }}
`
type TmplData struct {
CommandStrings string
Todos []string
Description string
Title string
CustomValues map[string]any
}
func main() {
test := template.New("testTemp")
tmpl, err := test.Parse(format)
if err != nil {
log.Fatal(err)
}
err = tmpl.ExecuteTemplate(os.Stdout, "testTemp", TmplData{
Title: "Test Title",
Description: "This is a test template",
Todos: []string{
"TODO 1",
"TODO 2",
},
CommandStrings: "Some command string text here...",
})
if err != nil {
log.Fatal(err)
}
}
Any ideas why the template would work in the standalone program, but not in the cobra commands?
Please let me know if any other information would be helpful. Thanks!
Flush the bufio.Writer:
var b bytes.Buffer
writer := bufio.NewWriter(&b)
err = tmpl.Execute(writer, TmplData{
CommandStrings: cmdtomd.RootToMd(rootCmd),
Todos: todos,
Description: description,
Title: title,
CustomValues: customValues,
})
if err != nil {
logger.WriteError(err.Error())
}
writer.Flush() // <--- add this line
A better solution is to write to the bytes.Buffer directly:
var b bytes.Buffer
err = tmpl.Execute(&b, TmplData{ // <--- write to buffer directly
CommandStrings: cmdtomd.RootToMd(rootCmd),
Todos: todos,
Description: description,
Title: title,
CustomValues: customValues,
})
if err != nil {
logger.WriteError(err.Error())
}