github.com/wynshop-open-source/gomplate@v3.5.0+incompatible/template.go (about)

     1  package gomplate
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"text/template"
    11  
    12  	"github.com/hairyhenderson/gomplate/tmpl"
    13  
    14  	"github.com/hairyhenderson/gomplate/conv"
    15  	"github.com/hairyhenderson/gomplate/env"
    16  	"github.com/pkg/errors"
    17  
    18  	"github.com/spf13/afero"
    19  	"github.com/zealic/xignore"
    20  )
    21  
    22  // ignorefile name, like .gitignore
    23  const gomplateignore = ".gomplateignore"
    24  
    25  // for overriding in tests
    26  var stdin io.ReadCloser = os.Stdin
    27  var fs = afero.NewOsFs()
    28  
    29  // Stdout allows overriding the writer to use when templates are written to stdout ("-").
    30  var Stdout io.WriteCloser = os.Stdout
    31  
    32  // tplate - models a gomplate template file...
    33  type tplate struct {
    34  	name         string
    35  	targetPath   string
    36  	target       io.Writer
    37  	contents     string
    38  	mode         os.FileMode
    39  	modeOverride bool
    40  }
    41  
    42  func addTmplFuncs(f template.FuncMap, root *template.Template, ctx interface{}) {
    43  	t := tmpl.New(root, ctx)
    44  	tns := func() *tmpl.Template { return t }
    45  	f["tmpl"] = tns
    46  	f["tpl"] = t.Inline
    47  }
    48  
    49  func (t *tplate) toGoTemplate(g *gomplate) (tmpl *template.Template, err error) {
    50  	if g.rootTemplate != nil {
    51  		tmpl = g.rootTemplate.New(t.name)
    52  	} else {
    53  		tmpl = template.New(t.name)
    54  		g.rootTemplate = tmpl
    55  	}
    56  	tmpl.Option("missingkey=error")
    57  	// the "tmpl" funcs get added here because they need access to the root template and context
    58  	addTmplFuncs(g.funcMap, g.rootTemplate, g.context)
    59  	tmpl.Funcs(g.funcMap)
    60  	tmpl.Delims(g.leftDelim, g.rightDelim)
    61  	_, err = tmpl.Parse(t.contents)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	for alias, path := range g.nestedTemplates {
    66  		// nolint: gosec
    67  		b, err := ioutil.ReadFile(path)
    68  		if err != nil {
    69  			return nil, err
    70  		}
    71  		_, err = tmpl.New(alias).Parse(string(b))
    72  		if err != nil {
    73  			return nil, err
    74  		}
    75  	}
    76  	return tmpl, nil
    77  }
    78  
    79  // loadContents - reads the template in _once_ if it hasn't yet been read. Uses the name!
    80  func (t *tplate) loadContents() (err error) {
    81  	if t.contents == "" {
    82  		t.contents, err = readInput(t.name)
    83  	}
    84  	return err
    85  }
    86  
    87  func (t *tplate) addTarget() (err error) {
    88  	if t.name == "<arg>" && t.targetPath == "" {
    89  		t.targetPath = "-"
    90  	}
    91  	if t.target == nil {
    92  		t.target, err = openOutFile(t.targetPath, t.mode, t.modeOverride)
    93  	}
    94  	return err
    95  }
    96  
    97  // gatherTemplates - gather and prepare input template(s) and output file(s) for rendering
    98  // nolint: gocyclo
    99  func gatherTemplates(o *Config, outFileNamer func(string) (string, error)) (templates []*tplate, err error) {
   100  	o.defaults()
   101  	mode, modeOverride, err := o.getMode()
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	switch {
   107  	// the arg-provided input string gets a special name
   108  	case o.Input != "":
   109  		templates = []*tplate{{
   110  			name:         "<arg>",
   111  			contents:     o.Input,
   112  			mode:         mode,
   113  			modeOverride: modeOverride,
   114  			targetPath:   o.OutputFiles[0],
   115  		}}
   116  	case o.InputDir != "":
   117  		// input dirs presume output dirs are set too
   118  		templates, err = walkDir(o.InputDir, outFileNamer, o.ExcludeGlob, mode, modeOverride)
   119  		if err != nil {
   120  			return nil, err
   121  		}
   122  	case o.Input == "":
   123  		templates = make([]*tplate, len(o.InputFiles))
   124  		for i := range o.InputFiles {
   125  			templates[i], err = fileToTemplates(o.InputFiles[i], o.OutputFiles[i], mode, modeOverride)
   126  			if err != nil {
   127  				return nil, err
   128  			}
   129  		}
   130  	}
   131  
   132  	return processTemplates(templates)
   133  }
   134  
   135  func processTemplates(templates []*tplate) ([]*tplate, error) {
   136  	for _, t := range templates {
   137  		if err := t.loadContents(); err != nil {
   138  			return nil, err
   139  		}
   140  
   141  		if err := t.addTarget(); err != nil {
   142  			return nil, err
   143  		}
   144  	}
   145  
   146  	return templates, nil
   147  }
   148  
   149  // walkDir - given an input dir `dir` and an output dir `outDir`, and a list
   150  // of .gomplateignore and exclude globs (if any), walk the input directory and create a list of
   151  // tplate objects, and an error, if any.
   152  func walkDir(dir string, outFileNamer func(string) (string, error), excludeGlob []string, mode os.FileMode, modeOverride bool) ([]*tplate, error) {
   153  	dir = filepath.Clean(dir)
   154  
   155  	dirStat, err := fs.Stat(dir)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	dirMode := dirStat.Mode()
   160  
   161  	templates := make([]*tplate, 0)
   162  	matcher := xignore.NewMatcher(fs)
   163  	matches, err := matcher.Matches(dir, &xignore.MatchesOptions{
   164  		Ignorefile:    gomplateignore,
   165  		Nested:        true, // allow nested ignorefile
   166  		AfterPatterns: excludeGlob,
   167  	})
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	// Unmatched ignorefile rules's files
   173  	files := matches.UnmatchedFiles
   174  	for _, file := range files {
   175  		nextInPath := filepath.Join(dir, file)
   176  		nextOutPath, err := outFileNamer(file)
   177  		if err != nil {
   178  			return nil, err
   179  		}
   180  
   181  		fMode := mode
   182  		if mode == 0 {
   183  			stat, perr := fs.Stat(nextInPath)
   184  			if perr == nil {
   185  				fMode = stat.Mode()
   186  			} else {
   187  				fMode = dirMode
   188  			}
   189  		}
   190  
   191  		// Ensure file parent dirs
   192  		if err = fs.MkdirAll(filepath.Dir(nextOutPath), dirMode); err != nil {
   193  			return nil, err
   194  		}
   195  
   196  		templates = append(templates, &tplate{
   197  			name:         nextInPath,
   198  			targetPath:   nextOutPath,
   199  			mode:         fMode,
   200  			modeOverride: modeOverride,
   201  		})
   202  	}
   203  
   204  	return templates, nil
   205  }
   206  
   207  func fileToTemplates(inFile, outFile string, mode os.FileMode, modeOverride bool) (*tplate, error) {
   208  	if inFile != "-" {
   209  		si, err := fs.Stat(inFile)
   210  		if err != nil {
   211  			return nil, err
   212  		}
   213  		if mode == 0 {
   214  			mode = si.Mode()
   215  		}
   216  	}
   217  	tmpl := &tplate{
   218  		name:         inFile,
   219  		targetPath:   outFile,
   220  		mode:         mode,
   221  		modeOverride: modeOverride,
   222  	}
   223  
   224  	return tmpl, nil
   225  }
   226  
   227  func openOutFile(filename string, mode os.FileMode, modeOverride bool) (out io.WriteCloser, err error) {
   228  	if conv.ToBool(env.Getenv("GOMPLATE_SUPPRESS_EMPTY", "false")) {
   229  		out = newEmptySkipper(func() (io.WriteCloser, error) {
   230  			if filename == "-" {
   231  				return Stdout, nil
   232  			}
   233  			return createOutFile(filename, mode, modeOverride)
   234  		})
   235  		return out, nil
   236  	}
   237  
   238  	if filename == "-" {
   239  		return Stdout, nil
   240  	}
   241  	return createOutFile(filename, mode, modeOverride)
   242  }
   243  
   244  func createOutFile(filename string, mode os.FileMode, modeOverride bool) (out io.WriteCloser, err error) {
   245  	out, err = fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
   246  	if err != nil {
   247  		return out, err
   248  	}
   249  	if modeOverride {
   250  		err = fs.Chmod(filename, mode.Perm())
   251  	}
   252  	return out, err
   253  }
   254  
   255  func readInput(filename string) (string, error) {
   256  	var err error
   257  	var inFile io.ReadCloser
   258  	if filename == "-" {
   259  		inFile = stdin
   260  	} else {
   261  		inFile, err = fs.OpenFile(filename, os.O_RDONLY, 0)
   262  		if err != nil {
   263  			return "", fmt.Errorf("failed to open %s\n%v", filename, err)
   264  		}
   265  		// nolint: errcheck
   266  		defer inFile.Close()
   267  	}
   268  	bytes, err := ioutil.ReadAll(inFile)
   269  	if err != nil {
   270  		err = fmt.Errorf("read failed for %s\n%v", filename, err)
   271  		return "", err
   272  	}
   273  	return string(bytes), nil
   274  }
   275  
   276  // emptySkipper is a io.WriteCloser wrapper that will only start writing once a
   277  // non-whitespace byte has been encountered. The writer must be provided by the
   278  // `open` func
   279  type emptySkipper struct {
   280  	open func() (io.WriteCloser, error)
   281  
   282  	// internal
   283  	w   io.WriteCloser
   284  	buf *bytes.Buffer
   285  	nw  bool
   286  }
   287  
   288  func newEmptySkipper(open func() (io.WriteCloser, error)) *emptySkipper {
   289  	return &emptySkipper{
   290  		w:    nil,
   291  		buf:  &bytes.Buffer{},
   292  		nw:   false,
   293  		open: open,
   294  	}
   295  }
   296  
   297  func (f *emptySkipper) Write(p []byte) (n int, err error) {
   298  	if !f.nw {
   299  		if allWhitespace(p) {
   300  			// buffer the whitespace
   301  			return f.buf.Write(p)
   302  		}
   303  
   304  		// first time around, so open the writer
   305  		f.nw = true
   306  		f.w, err = f.open()
   307  		if err != nil {
   308  			return 0, err
   309  		}
   310  		if f.w == nil {
   311  			return 0, errors.New("nil writer returned by open")
   312  		}
   313  		// empty the buffer into the wrapped writer
   314  		_, err = f.buf.WriteTo(f.w)
   315  		if err != nil {
   316  			return 0, err
   317  		}
   318  	}
   319  
   320  	return f.w.Write(p)
   321  }
   322  
   323  func (f *emptySkipper) Close() error {
   324  	if f.w != nil {
   325  		return f.w.Close()
   326  	}
   327  	return nil
   328  }
   329  
   330  func allWhitespace(p []byte) bool {
   331  	for _, b := range p {
   332  		if b == ' ' || b == '\t' || b == '\n' || b == '\r' || b == '\v' {
   333  			continue
   334  		}
   335  		return false
   336  	}
   337  	return true
   338  }