Search code examples
gogo-templates

Golang Template Empty, But no Error Thrown


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!


Solution

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