github.com/coveo/gotemplate@v2.7.7+incompatible/template/template_process.go (about)

     1  package template
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strings"
    12  
    13  	"github.com/coveo/gotemplate/collections"
    14  	"github.com/coveo/gotemplate/errors"
    15  	"github.com/coveo/gotemplate/utils"
    16  	"github.com/fatih/color"
    17  	"golang.org/x/crypto/ssh/terminal"
    18  )
    19  
    20  var (
    21  	templateExt    = []string{".gt", ".template"}
    22  	linePrefix     = `^template: ` + p(tagLocation, p(tagFile, `.*?`)+`:`+p(tagLine, `\d+`)+`(:`+p(tagCol, `\d+`)+`)?: `)
    23  	execPrefix     = linePrefix + `executing ".*" at <` + p(tagCode, `.*`) + `>: `
    24  	templateErrors = []string{
    25  		execPrefix + `map has no entry for key "` + p(tagKey, `.*`) + `"`,
    26  		execPrefix + `(?s)error calling (raise|assert): ` + p(tagMsg, `.*`),
    27  		execPrefix + p(tagErr, `.*`),
    28  		linePrefix + p(tagErr, `.*`),
    29  	}
    30  )
    31  
    32  func p(name, expr string) string { return fmt.Sprintf("(?P<%s>%s)", name, expr) }
    33  
    34  const (
    35  	noValue      = "<no value>"
    36  	noValueRepl  = "!NO_VALUE!"
    37  	nilValue     = "<nil>"
    38  	nilValueRepl = "!NIL_VALUE!"
    39  	undefError   = `"` + noValue + `"`
    40  	noValueError = "contains undefined value(s)"
    41  	runError     = `"<RUN_ERROR>"`
    42  	tagLine      = "line"
    43  	tagCol       = "column"
    44  	tagCode      = "code"
    45  	tagMsg       = "message"
    46  	tagLocation  = "location"
    47  	tagFile      = "file"
    48  	tagKey       = "key"
    49  	tagErr       = "error"
    50  )
    51  
    52  // ProcessContent loads and runs the file template.
    53  func (t Template) ProcessContent(content, source string) (string, error) {
    54  	return t.processContentInternal(content, source, nil, 0, true)
    55  }
    56  
    57  func (t Template) processContentInternal(originalContent, source string, originalSourceLines []string, retryCount int, cloneContext bool) (result string, err error) {
    58  	topCall := originalSourceLines == nil
    59  	content := originalContent
    60  	if topCall {
    61  		content = t.substitute(content)
    62  
    63  		if strings.HasPrefix(content, "#!") {
    64  			// If the content starts with a Shebang operator including gotemplate, we remove the first line
    65  			lines := strings.Split(content, "\n")
    66  			if strings.Contains(lines[0], "gotemplate") {
    67  				content = strings.Join(lines[1:], "\n")
    68  				t.options[OutputStdout] = true
    69  			}
    70  		}
    71  
    72  		content = string(t.applyRazor([]byte(content)))
    73  
    74  		if t.options[RenderingDisabled] || !t.IsCode(content) {
    75  			// There is no template element to evaluate or the template rendering is off
    76  			return content, nil
    77  		}
    78  		log.Notice("GoTemplate processing of", source)
    79  
    80  		if !t.options[AcceptNoValue] {
    81  			// We replace any pre-existing no value to avoid false error detection
    82  			content = strings.Replace(content, noValue, noValueRepl, -1)
    83  			content = strings.Replace(content, nilValue, nilValueRepl, -1)
    84  		}
    85  
    86  		defer func() {
    87  			// If we get errors and the file is not an explicit gotemplate file, or contains
    88  			// gotemplate! or the strict error check mode is not enabled, we simply
    89  			// add a trace with the error content and return the content unaltered
    90  			if err != nil {
    91  				strictMode := t.options[StrictErrorCheck]
    92  				strictMode = strictMode || strings.Contains(originalContent, explicitGoTemplate)
    93  				extension := filepath.Ext(source)
    94  				strictMode = strictMode || (extension != "" && strings.Contains(".gt,.gte,.template", extension))
    95  				if !(strictMode) {
    96  					Log.Noticef("Ignored gotemplate error in %s (file left unchanged):\n%s", color.CyanString(source), err.Error())
    97  					result, err = originalContent, nil
    98  				}
    99  			}
   100  		}()
   101  	}
   102  
   103  	// This local functions handle all errors from Parse or Execute and tries to fix the template to allow discovering
   104  	// of all errors in a template instead of stopping after the first one encountered
   105  	handleError := func(err error) (string, error) {
   106  		if originalSourceLines == nil {
   107  			originalSourceLines = strings.Split(originalContent, "\n")
   108  		}
   109  
   110  		regexGroup := must(utils.GetRegexGroup("Parse", templateErrors)).([]*regexp.Regexp)
   111  
   112  		if matches, _ := utils.MultiMatch(err.Error(), regexGroup...); len(matches) > 0 {
   113  			// We remove the faulty line and continue the processing to get all errors at once
   114  			lines := strings.Split(content, "\n")
   115  			faultyLine := toInt(matches[tagLine]) - 1
   116  			faultyColumn := 0
   117  			key, message, errText, code := matches[tagKey], matches[tagMsg], matches[tagErr], matches[tagCode]
   118  
   119  			if matches[tagCol] != "" {
   120  				faultyColumn = toInt(matches[tagCol]) - 1
   121  			}
   122  
   123  			errorText, parserBug := color.RedString(errText), ""
   124  
   125  			if faultyLine >= len(lines) {
   126  				faultyLine = len(lines) - 1
   127  				// TODO: This code can be removed once issue has been fixed
   128  				parserBug = color.HiRedString("\nBad error line reported: check: https://github.com/golang/go/issues/27319")
   129  			}
   130  
   131  			currentLine := String(lines[faultyLine])
   132  
   133  			if matches[tagFile] != source {
   134  				// An error occurred in an included external template file, we cannot try to recuperate
   135  				// and try to find further errors, so we just return the error.
   136  
   137  				if fileContent, err := ioutil.ReadFile(matches[tagFile]); err != nil {
   138  					currentLine = String(fmt.Sprintf("Unable to read file: %v", err))
   139  				} else {
   140  					currentLine = String(fileContent).Lines()[toInt(matches[tagLine])-1]
   141  				}
   142  				return "", fmt.Errorf("%s %v in: %s", color.WhiteString(source), err, color.HiBlackString(currentLine.Str()))
   143  			}
   144  			if faultyColumn != 0 && strings.Contains(" (", currentLine[faultyColumn:faultyColumn+1].Str()) {
   145  				// Sometime, the error is not reporting the exact column, we move 1 char forward to get the real problem
   146  				faultyColumn++
   147  			}
   148  
   149  			errorLine := fmt.Sprintf(" in: %s", color.HiBlackString(originalSourceLines[faultyLine]))
   150  			var logMessage string
   151  			if key != "" {
   152  				// Missing key and we disabled the <no value> mode
   153  				context := String(currentLine).SelectContext(faultyColumn, t.LeftDelim(), t.RightDelim())
   154  				if subContext := String(currentLine).SelectContext(faultyColumn, "(", ")"); subContext != "" {
   155  					// There is an sub-context, so we replace it first
   156  					context = subContext
   157  				}
   158  				current := String(currentLine).SelectWord(faultyColumn, ".")
   159  				newContext := context.Replace(current.Str(), undefError).Str()
   160  				newLine := currentLine.Replace(context.Str(), newContext)
   161  
   162  				left := fmt.Sprintf(`(?P<begin>(%s-?\s*(if|range|with)\s.*|\()\s*)?`, regexp.QuoteMeta(t.LeftDelim()))
   163  				right := fmt.Sprintf(`(?P<end>\s*(-?%s|\)))`, regexp.QuoteMeta(t.RightDelim()))
   164  				const (
   165  					ifUndef = "ifUndef"
   166  					isZero  = "isZero"
   167  					assert  = "assert"
   168  				)
   169  				undefRegexDefintions := []string{
   170  					fmt.Sprintf(`%[1]s(undef|ifUndef|default)\s+(?P<%[3]s>.*?)\s+%[4]s%[2]s`, left, right, ifUndef, undefError),
   171  					fmt.Sprintf(`%[1]s(?P<%[3]s>%[3]s|isNil|isNull|isEmpty|isSet)\s+%[4]s%[2]s`, left, right, isZero, undefError),
   172  					fmt.Sprintf(`%[1]s%[3]s\s+(?P<%[3]s>%[4]s).*?%[2]s`, left, right, assert, undefError),
   173  				}
   174  				expressions, errRegex := utils.GetRegexGroup(fmt.Sprintf("Undef%s", t.delimiters), undefRegexDefintions)
   175  				if errRegex != nil {
   176  					log.Error(errRegex)
   177  				}
   178  				undefMatches, n := utils.MultiMatch(newContext, expressions...)
   179  
   180  				if undefMatches[ifUndef] != "" {
   181  					logMessage = fmt.Sprintf("Managed undefined value %s: %s", key, context)
   182  					err = nil
   183  					lines[faultyLine] = newLine.Replace(newContext, expressions[n].ReplaceAllString(newContext, fmt.Sprintf("${begin}${%s}${end}", ifUndef))).Str()
   184  				} else if undefMatches[isZero] != "" {
   185  					logMessage = fmt.Sprintf("Managed undefined value %s: %s", key, context)
   186  					err = nil
   187  					value := fmt.Sprintf("%s%v%s", undefMatches["begin"], undefMatches[isZero] != "isSet", undefMatches["end"])
   188  					lines[faultyLine] = newLine.Replace(newContext, value).Str()
   189  				} else if undefMatches[assert] != "" {
   190  					logMessage = fmt.Sprintf("Managed assertion on %s: %s", key, context)
   191  					err = nil
   192  					lines[faultyLine] = newLine.Replace(newContext, strings.Replace(newContext, undefError, "0", 1)).Str()
   193  				} else {
   194  					logMessage = fmt.Sprintf("Unmanaged undefined value %s: %s", key, context)
   195  					errorText = color.RedString("Undefined value ") + color.YellowString(key)
   196  					lines[faultyLine] = newLine.Str()
   197  				}
   198  			} else if message != "" {
   199  				logMessage = fmt.Sprintf("User defined error: %s", message)
   200  				errorText = color.RedString(message)
   201  				lines[faultyLine] = fmt.Sprintf("ERROR %s", errText)
   202  			} else if code != "" {
   203  				logMessage = fmt.Sprintf("Execution error: %s", err)
   204  				context := String(currentLine).SelectContext(faultyColumn, t.LeftDelim(), t.RightDelim())
   205  				errorText = fmt.Sprintf(color.RedString("%s (%s)", errText, code))
   206  				if context == "" {
   207  					// We have not been able to find the current context, we wipe the erroneous line
   208  					lines[faultyLine] = fmt.Sprintf("ERROR %s", errText)
   209  				} else {
   210  					lines[faultyLine] = currentLine.Replace(context.Str(), runError).Str()
   211  				}
   212  			} else if errText != noValueError {
   213  				logMessage = fmt.Sprintf("Parsing error: %s", err)
   214  				lines[faultyLine] = fmt.Sprintf("ERROR %s", errText)
   215  			}
   216  			if currentLine.Contains(runError) || strings.Contains(code, undefError) {
   217  				// The erroneous line has already been replaced, we do not report the error again
   218  				err, errorText = nil, ""
   219  				log.Debugf("Ignored error %s", logMessage)
   220  			} else if logMessage != "" {
   221  				log.Debug(logMessage)
   222  			}
   223  
   224  			if err != nil {
   225  				err = fmt.Errorf("%s%s%s%s", color.WhiteString(matches[tagLocation]), errorText, errorLine, parserBug)
   226  			}
   227  			if lines[faultyLine] != currentLine.Str() || strings.Contains(err.Error(), noValueError) {
   228  				// If we changed something in the current text, we try to continue the evaluation to get further errors
   229  				result, err2 := t.processContentInternal(strings.Join(lines, "\n"), source, originalSourceLines, retryCount+1, false)
   230  				if err2 != nil {
   231  					if err != nil && errText != noValueError {
   232  						if err.Error() == err2.Error() {
   233  							// TODO See: https://github.com/golang/go/issues/27319
   234  							err = fmt.Errorf("%v\n%s", err, color.HiRedString("Unable to continue processing to check for further errors"))
   235  						} else {
   236  							err = fmt.Errorf("%v\n%v", err, err2)
   237  						}
   238  					} else {
   239  						err = err2
   240  					}
   241  				}
   242  				return result, err
   243  			}
   244  		}
   245  		return "", err
   246  	}
   247  
   248  	context := t.GetNewContext(filepath.Dir(source), true)
   249  	newTemplate := context.New(source)
   250  
   251  	if topCall {
   252  		newTemplate.Option("missingkey=default")
   253  	} else if !t.options[AcceptNoValue] {
   254  		// To help detect errors on second run, we enable the option to raise error on nil values
   255  		// log.Infof("%s(%d): Activating the missing key error option", source, retryCount)
   256  		newTemplate.Option("missingkey=error")
   257  	}
   258  
   259  	func() {
   260  		// Here, we invoke the parser within a pseudo func because we cannot
   261  		// call the parser without locking
   262  		templateMutex.Lock()
   263  		defer templateMutex.Unlock()
   264  		newTemplate, err = newTemplate.Parse(content)
   265  	}()
   266  	if err != nil {
   267  		log.Infof("%s(%d): Parsing error %v", source, retryCount, err)
   268  		return handleError(err)
   269  	}
   270  
   271  	var out bytes.Buffer
   272  	workingContext := t.context
   273  	if cloneContext {
   274  		workingContext = collections.AsDictionary(workingContext).Clone()
   275  	}
   276  	if err = newTemplate.Execute(&out, workingContext); err != nil {
   277  		log.Infof("%s(%d): Execution error %v", source, retryCount, err)
   278  		return handleError(err)
   279  	}
   280  	result = t.substitute(out.String())
   281  
   282  	if topCall && !t.options[AcceptNoValue] {
   283  		// Detect possible <no value> or <nil> that could be generated
   284  		if pos := strings.Index(strings.Replace(result, nilValue, noValue, -1), noValue); pos >= 0 {
   285  			line := len(strings.Split(result[:pos], "\n"))
   286  			return handleError(fmt.Errorf("template: %s:%d: %s", source, line, noValueError))
   287  		}
   288  	}
   289  
   290  	if !t.options[AcceptNoValue] {
   291  		// We restore the existing no value if any
   292  		result = strings.Replace(result, noValueRepl, noValue, -1)
   293  		result = strings.Replace(result, nilValueRepl, nilValue, -1)
   294  	}
   295  	return
   296  }
   297  
   298  // ProcessTemplate loads and runs the template if it is a file, otherwise, it simply process the content.
   299  func (t Template) ProcessTemplate(template, sourceFolder, targetFolder string) (resultFile string, err error) {
   300  	isCode := t.IsCode(template)
   301  	var content string
   302  
   303  	if isCode {
   304  		content = template
   305  		template = "."
   306  	} else if fileContent, err := ioutil.ReadFile(template); err == nil {
   307  		content = string(fileContent)
   308  	} else {
   309  		return "", err
   310  	}
   311  
   312  	result, err := t.ProcessContent(content, template)
   313  	if err != nil {
   314  		return
   315  	}
   316  
   317  	if isCode {
   318  		// This occurs when gotemplate code has been supplied as a filename. In that case, we simply render
   319  		// the result to the stdout
   320  		Println(result)
   321  		return "", nil
   322  	}
   323  	resultFile = template
   324  	for i := range templateExt {
   325  		resultFile = strings.TrimSuffix(resultFile, templateExt[i])
   326  	}
   327  	resultFile = getTargetFile(resultFile, sourceFolder, targetFolder)
   328  	isTemplate := t.isTemplate(template)
   329  	if isTemplate {
   330  		ext := path.Ext(resultFile)
   331  		if strings.TrimSpace(result)+ext == "" {
   332  			// We do not save anything for an empty resulting template that has no extension
   333  			return "", nil
   334  		}
   335  		if !t.options[Overwrite] {
   336  			resultFile = fmt.Sprint(strings.TrimSuffix(resultFile, ext), ".generated", ext)
   337  		}
   338  	}
   339  
   340  	if t.options[OutputStdout] {
   341  		err = t.printResult(template, resultFile, result)
   342  		if err != nil {
   343  			errors.Print(err)
   344  		}
   345  		return "", nil
   346  	}
   347  
   348  	if sourceFolder == targetFolder && result == content {
   349  		return "", nil
   350  	}
   351  
   352  	mode := must(os.Stat(template)).(os.FileInfo).Mode()
   353  	if !isTemplate && !t.options[Overwrite] {
   354  		newName := template + ".original"
   355  		log.Noticef("%s => %s", utils.Relative(t.folder, template), utils.Relative(t.folder, newName))
   356  		must(os.Rename(template, template+".original"))
   357  	}
   358  
   359  	if sourceFolder != targetFolder {
   360  		must(os.MkdirAll(filepath.Dir(resultFile), 0777))
   361  	}
   362  	log.Notice("Writing file", utils.Relative(t.folder, resultFile))
   363  
   364  	if utils.IsShebangScript(result) {
   365  		mode = 0755
   366  	}
   367  
   368  	if err = ioutil.WriteFile(resultFile, []byte(result), mode); err != nil {
   369  		return
   370  	}
   371  
   372  	if isTemplate && t.options[Overwrite] && sourceFolder == targetFolder {
   373  		os.Remove(template)
   374  	}
   375  	return
   376  }
   377  
   378  // ProcessTemplates loads and runs the file template or execute the content if it is not a file.
   379  func (t Template) ProcessTemplates(sourceFolder, targetFolder string, templates ...string) (resultFiles []string, err error) {
   380  	sourceFolder = iif(sourceFolder == "", t.folder, sourceFolder).(string)
   381  	targetFolder = iif(targetFolder == "", t.folder, targetFolder).(string)
   382  	resultFiles = make([]string, 0, len(templates))
   383  
   384  	print := t.options[OutputStdout]
   385  
   386  	var errors errors.Array
   387  	for i := range templates {
   388  		t.options[OutputStdout] = print // Some file may change this option at runtime, so we restore it back to its originalSourceLines value between each file
   389  		resultFile, err := t.ProcessTemplate(templates[i], sourceFolder, targetFolder)
   390  		if err == nil {
   391  			if resultFile != "" {
   392  				resultFiles = append(resultFiles, resultFile)
   393  			}
   394  		} else {
   395  			errors = append(errors, err)
   396  		}
   397  	}
   398  	if len(errors) > 0 {
   399  		err = errors
   400  	}
   401  	return
   402  }
   403  
   404  func (t Template) printResult(source, target, result string) (err error) {
   405  	if utils.IsTerraformFile(target) {
   406  		base := filepath.Base(target)
   407  		tempFolder := must(ioutil.TempDir(t.TempFolder, base)).(string)
   408  		tempFile := filepath.Join(tempFolder, base)
   409  		err = ioutil.WriteFile(tempFile, []byte(result), 0644)
   410  		if err != nil {
   411  			return
   412  		}
   413  		err = utils.TerraformFormat(tempFile)
   414  		bytes := must(ioutil.ReadFile(tempFile)).([]byte)
   415  		result = string(bytes)
   416  	}
   417  
   418  	if !t.isTemplate(source) && !t.options[Overwrite] {
   419  		source += ".original"
   420  	}
   421  
   422  	source = utils.Relative(t.folder, source)
   423  	if relTarget := utils.Relative(t.folder, target); !strings.HasPrefix(relTarget, "../../../") {
   424  		target = relTarget
   425  	}
   426  	if source != target {
   427  		log.Noticef("%s => %s", source, target)
   428  	} else {
   429  		log.Notice(target)
   430  	}
   431  	Print(result)
   432  	if result != "" && terminal.IsTerminal(int(os.Stdout.Fd())) {
   433  		Println()
   434  	}
   435  
   436  	return
   437  }