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 }