From 2a9aa4dcac0c33f7fffefb94a1bc92a17fd7cfd3 Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Mon, 6 Jul 2020 11:28:52 -0400 Subject: [PATCH] html/template, text/template: add ParseFS Now templates can be parsed not just from operating system files but from arbitrary file systems, including zip files. For #41190. Change-Id: I2172001388ddb1f13defa6c5e644e8ec8703ee80 Reviewed-on: https://go-review.googlesource.com/c/go/+/243938 Trust: Russ Cox Run-TryBot: Russ Cox TryBot-Result: Go Bot Reviewed-by: Rob Pike --- src/html/template/multi_test.go | 43 ++++++++++++++++++++++ src/html/template/template.go | 58 ++++++++++++++++++++++++++--- src/html/template/testdata/fs.zip | Bin 0 -> 406 bytes src/text/template/helper.go | 59 +++++++++++++++++++++++++++--- src/text/template/multi_test.go | 30 +++++++++++++++ 5 files changed, 178 insertions(+), 12 deletions(-) create mode 100644 src/html/template/testdata/fs.zip diff --git a/src/html/template/multi_test.go b/src/html/template/multi_test.go index 50526c5b65..6535ab6c04 100644 --- a/src/html/template/multi_test.go +++ b/src/html/template/multi_test.go @@ -7,7 +7,9 @@ package template import ( + "archive/zip" "bytes" + "os" "testing" "text/template/parse" ) @@ -82,6 +84,35 @@ func TestParseGlob(t *testing.T) { testExecute(multiExecTests, template, t) } +func TestParseFS(t *testing.T) { + fs := os.DirFS("testdata") + + { + _, err := ParseFS(fs, "DOES NOT EXIST") + if err == nil { + t.Error("expected error for non-existent file; got none") + } + } + + { + template := New("root") + _, err := template.ParseFS(fs, "file1.tmpl", "file2.tmpl") + if err != nil { + t.Fatalf("error parsing files: %v", err) + } + testExecute(multiExecTests, template, t) + } + + { + template := New("root") + _, err := template.ParseFS(fs, "file*.tmpl") + if err != nil { + t.Fatalf("error parsing files: %v", err) + } + testExecute(multiExecTests, template, t) + } +} + // In these tests, actual content (not just template definitions) comes from the parsed files. var templateFileExecTests = []execTest{ @@ -104,6 +135,18 @@ func TestParseGlobWithData(t *testing.T) { testExecute(templateFileExecTests, template, t) } +func TestParseZipFS(t *testing.T) { + z, err := zip.OpenReader("testdata/fs.zip") + if err != nil { + t.Fatalf("error parsing zip: %v", err) + } + template, err := New("root").ParseFS(z, "tmpl*.tmpl") + if err != nil { + t.Fatalf("error parsing files: %v", err) + } + testExecute(templateFileExecTests, template, t) +} + const ( cloneText1 = `{{define "a"}}{{template "b"}}{{template "c"}}{{end}}` cloneText2 = `{{define "b"}}b{{end}}` diff --git a/src/html/template/template.go b/src/html/template/template.go index 75437879e2..bc960afe5f 100644 --- a/src/html/template/template.go +++ b/src/html/template/template.go @@ -7,7 +7,9 @@ package template import ( "fmt" "io" + "io/fs" "io/ioutil" + "path" "path/filepath" "sync" "text/template" @@ -384,7 +386,7 @@ func Must(t *Template, err error) *Template { // For instance, ParseFiles("a/foo", "b/foo") stores "b/foo" as the template // named "foo", while "a/foo" is unavailable. func ParseFiles(filenames ...string) (*Template, error) { - return parseFiles(nil, filenames...) + return parseFiles(nil, readFileOS, filenames...) } // ParseFiles parses the named files and associates the resulting templates with @@ -396,12 +398,12 @@ func ParseFiles(filenames ...string) (*Template, error) { // // ParseFiles returns an error if t or any associated template has already been executed. func (t *Template) ParseFiles(filenames ...string) (*Template, error) { - return parseFiles(t, filenames...) + return parseFiles(t, readFileOS, filenames...) } // parseFiles is the helper for the method and function. If the argument // template is nil, it is created from the first file. -func parseFiles(t *Template, filenames ...string) (*Template, error) { +func parseFiles(t *Template, readFile func(string) (string, []byte, error), filenames ...string) (*Template, error) { if err := t.checkCanParse(); err != nil { return nil, err } @@ -411,12 +413,11 @@ func parseFiles(t *Template, filenames ...string) (*Template, error) { return nil, fmt.Errorf("html/template: no files named in call to ParseFiles") } for _, filename := range filenames { - b, err := ioutil.ReadFile(filename) + name, b, err := 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 @@ -479,7 +480,7 @@ func parseGlob(t *Template, pattern string) (*Template, error) { if len(filenames) == 0 { return nil, fmt.Errorf("html/template: pattern matches no files: %#q", pattern) } - return parseFiles(t, filenames...) + return parseFiles(t, readFileOS, filenames...) } // IsTrue reports whether the value is 'true', in the sense of not the zero of its type, @@ -488,3 +489,48 @@ func parseGlob(t *Template, pattern string) (*Template, error) { func IsTrue(val interface{}) (truth, ok bool) { return template.IsTrue(val) } + +// ParseFS is like ParseFiles or ParseGlob but reads from the file system fs +// instead of the host operating system's file system. +// It accepts a list of glob patterns. +// (Note that most file names serve as glob patterns matching only themselves.) +func ParseFS(fs fs.FS, patterns ...string) (*Template, error) { + return parseFS(nil, fs, patterns) +} + +// ParseFS is like ParseFiles or ParseGlob but reads from the file system fs +// instead of the host operating system's file system. +// It accepts a list of glob patterns. +// (Note that most file names serve as glob patterns matching only themselves.) +func (t *Template) ParseFS(fs fs.FS, patterns ...string) (*Template, error) { + return parseFS(t, fs, patterns) +} + +func parseFS(t *Template, fsys fs.FS, patterns []string) (*Template, error) { + var filenames []string + for _, pattern := range patterns { + list, err := fs.Glob(fsys, pattern) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, fmt.Errorf("template: pattern matches no files: %#q", pattern) + } + filenames = append(filenames, list...) + } + return parseFiles(t, readFileFS(fsys), filenames...) +} + +func readFileOS(file string) (name string, b []byte, err error) { + name = filepath.Base(file) + b, err = ioutil.ReadFile(file) + return +} + +func readFileFS(fsys fs.FS) func(string) (string, []byte, error) { + return func(file string) (name string, b []byte, err error) { + name = path.Base(file) + b, err = fs.ReadFile(fsys, file) + return + } +} diff --git a/src/html/template/testdata/fs.zip b/src/html/template/testdata/fs.zip new file mode 100644 index 0000000000000000000000000000000000000000..8581313ae36f2b3980e320f3a8f747db9727386a GIT binary patch literal 406 zcmWIWW@Zs#U|`^2;3#<;P@%SDw+@hJ1jJknG7KfT1v!R#ATl(BlYx2t3ZD4q!ou;T z72FJrEV`i#3@pL%j11bIXLV0_>Uf$yS+&m3)A#fl@6do%0jt)oU9ryZtoO>btX$I_ zrfO(}GO6xh2tct<