text/template issue Parse() vs. ParseFiles()
Asked Answered
D

2

6

I'm trying to do some simple work with the text/template package. The sample given at the top of template is what I'm working with.

How do I write the 'parsed' file so template.ParseFiles() properly reads and executes it?

package main

import (
    "text/template"
    "os"
)

type Inventory struct {
    Material string
    Count    uint
}

func main() {
    sweaters := Inventory{"wool", 17}
    tmpl, err := template.New("test").Parse("{{.Count}} items are made of {{.Material}}")
    // tmpl, err := template.New("test").ParseFiles("file.txt")

    if err != nil { panic(err) }
    err = tmpl.Execute(os.Stdout, sweaters)
    if err != nil { panic(err) }
}

/*
Contents of file.txt:
{{.Count}} items are made of {{.Material}}

Error thrown:
panic: template: test:1: "test" is an incomplete or empty template

goroutine 1 [running]:
main.main()
    /tmp/templates/t.go:19 +0x21a

goroutine 2 [syscall]:
created by runtime.main
    /var/tmp/portage/dev-lang/go-1.0.1/work/go/src/pkg/runtime/proc.c:221
*/

I have a copy of this code posted at the golang playground here

Edit #1: I've been doing some research on this issue... since it's the Execute() method that actually throws the exception, and not the ParseFiles() part, I checked the method definition:

// Execute applies a parsed template to the specified data object,
// and writes the output to wr.
func (t *Template) Execute(wr io.Writer, data interface{}) (err error) {
    defer errRecover(&err)
    value := reflect.ValueOf(data)
    state := &state{
        tmpl: t,
        wr:   wr,
        line: 1,
        vars: []variable{{"$", value}},
    }
    if t.Tree == nil || t.Root == nil {
        state.errorf("%q is an incomplete or empty template", t.name)
    }
    state.walk(value, t.Root)
    return
}

So, on a hunch, I dumped the value of t.Tree for the inline 'non-file' style, tmpl is: &parse.Tree{Name:"test", Root:(*parse.ListNode)(0xf840030700), funcs:[]map[string]interface {}(nil), lex:(*parse.lexer)(nil), token:[2]parse.item{parse.item{typ:6, val:""}, parse.item{typ:9, val:"{{"}}, peekCount:1, vars:[]string(nil)} and when ran with ParseFiles(), tmpl is: (*parse.Tree)(nil). I find it odd that one is a dereference, and one value is a pointer. This may help solve the riddle

Drew answered 4/8, 2012 at 2:4 Comment(3)
Note, the interesting thing about this example, and other ParseFiles() tests I've done, they don't fail on parsing, they fail when they get to the Execute() phase, always with the incomplete or empty template messageDrew
Your linked golang playground example works for me. Are you sure this is actually broken?Mccowyn
Yes, it works fine, because you don't have access to a filesystem on the playground. However, if you uncomment the ParseFiles() line, comment out the other Parse() line run it locally, you'll see the error that I have in the comments in the code.Drew
G
17
sweaters := Inventory{"wool", 17}
tmpl, err := template.ParseFiles("file.txt")
if err != nil {
    panic(err)
}
err = tmpl.ExecuteTemplate(os.Stdout, "file.txt", sweaters)
if err != nil {
    panic(err)
}

If you have many files, you can use ParseGlob:

tmpl, err := template.ParseGlob("*.txt")
if err != nil {
    panic(err)
}
err = tmpl.ExecuteTemplate(os.Stdout, "file.txt", sweaters)
if err != nil {
    panic(err)
}
err = tmpl.ExecuteTemplate(os.Stdout, "file2.txt", sweaters)
if err != nil {
    panic(err)
}
Gies answered 4/8, 2012 at 10:3 Comment(5)
Yup, I actually found that answer myself later on, but stackoverflow would not let me post it. It is confusing and unfortunate that while Parse() works in a similar fashion to (t *Template) Parse(), ParseFiles() works in a different way than (t *Template) ParseFiles() which caused all of my confusion. Thanks for answering.Drew
Key point: use template.ParseFiles("filename.txt"), NOT (t *Template) ParseFiles... and I will never get the last hour back again.Hollandia
@EvanPlumlee: Ran into the same issue and your comment is what cleared it up for me. Don't think I would have found it otherwise. Had to look at the pkg docs again.Kauffman
Additionally, if your template is in a subdirectory the argument to ExecuteTemplate is the file name part only without the path to the directory.Contextual
template.ParseFiles("file.txt") returns a Template named file.txt. Therefore when you execute template.New("test").ParseFiles("file.txt") you end up with 2 Templates: test and file.txt. The latter is associated with the former.Encyclopedic
B
11

There is a little trick in Go template parseFiles.

func parseFiles(t *Template, filenames ...string) (*Template, error) {
    if len(filenames) == 0 {
        // Not really a problem, but be consistent.
        return nil, fmt.Errorf("template: no files named in call to ParseFiles")
    }
    for _, filename := range filenames {
        b, err := ioutil.ReadFile(filename)
        if err != nil {
            return nil, err
        }
        s := string(b)
        name := filepath.Base(filename)
        // First template becomes return value if not already defined,
        // and we use that one for subsequent New calls to associate
        // all the templates together. Also, if this file has the same name
        // as t, this file becomes the contents of t, so
        //  t, err := New(name).Funcs(xxx).ParseFiles(name)
        // works. Otherwise we create a new template associated with t.
        var tmpl *Template
        if t == nil {
            t = New(name)
        }
        if name == t.Name() {
            tmpl = t
        } else {
            tmpl = t.New(name)
        }
        _, err = tmpl.Parse(s)
        if err != nil {
            return nil, err
        }
    }
    return t, nil
}

Only the template with same name will be reuse, otherwise create new one. as your sample:

tmpl, err := template.New("test").ParseFiles("file.txt")

tmpl is the template named "test", and associated another template named "file.txt", you call Execute on "test" template, this template is a empty template, so raise the error "test is an incomplete or empty template".

It worked when you change the template name to file.txt

tmpl, err := template.New("file.txt").ParseFiles("file.txt")
Bautista answered 3/1, 2017 at 8:39 Comment(1)
this is what make it suck, ignore the template defined name and use the filename insteadCormack

© 2022 - 2024 — McMap. All rights reserved.