github.com/coveo/gotemplate@v2.7.7+incompatible/template/extra_runtime.go (about) 1 package template 2 3 import ( 4 "bytes" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "os/exec" 9 "path" 10 "reflect" 11 "strings" 12 13 "github.com/coveo/gotemplate/collections" 14 "github.com/coveo/gotemplate/hcl" 15 "github.com/coveo/gotemplate/utils" 16 "github.com/fatih/color" 17 ) 18 19 const ( 20 runtimeFunc = "Runtime" 21 ) 22 23 var runtimeFuncsArgs = arguments{ 24 "alias": {"name", "function", "source"}, 25 "assert": {"test", "message", "arguments"}, 26 "assertWarning": {"test", "message", "arguments"}, 27 "categories": {"functionsGroups"}, 28 "ellipsis": {"function"}, 29 "exec": {"command"}, 30 "exit": {"exitValue"}, 31 "func": {"name", "function", "source", "config"}, 32 "function": {"name"}, 33 "include": {"source", "context"}, 34 "localAlias": {"name", "function", "source"}, 35 "run": {"command"}, 36 "substitute": {"content"}, 37 } 38 39 var runtimeFuncsAliases = aliases{ 40 "assert": {"assertion"}, 41 "assertWarning": {"assertw"}, 42 "exec": {"execute"}, 43 "getAttributes": {"attr", "attributes"}, 44 "getMethods": {"methods"}, 45 "getSignature": {"sign", "signature"}, 46 "raise": {"raiseError"}, 47 } 48 49 var runtimeFuncsHelp = descriptions{ 50 "alias": "Defines an alias (go template function) using the function (exec, run, include, template). Executed in the context of the caller.", 51 "aliases": "Returns the list of all functions that are simply an alias of another function.", 52 "allFunctions": "Returns the list of all available functions.", 53 "assert": "Raises a formated error if the test condition is false.", 54 "assertWarning": "Issues a formated warning if the test condition is false.", 55 "categories": strings.TrimSpace(collections.UnIndent(` 56 Returns all functions group by categories. 57 58 The returned value has the following properties: 59 Name string 60 Functions []string 61 `)), 62 "current": "Returns the current folder (like pwd, but returns the folder of the currently running folder).", 63 "ellipsis": "Returns the result of the function by expanding its last argument that must be an array into values. It's like calling function(arg1, arg2, otherArgs...).", 64 "exec": "Returns the result of the shell command as structured data (as string if no other conversion is possible).", 65 "exit": "Exits the current program execution.", 66 "func": "Defines a function with the current context using the function (exec, run, include, template). Executed in the context of the caller.", 67 "function": strings.TrimSpace(collections.UnIndent(` 68 Returns the information relative to a specific function. 69 70 The returned value has the following properties: 71 Name string 72 Description string 73 Signature string 74 Group string 75 Aliases []string 76 Arguments string 77 Result string 78 `)), 79 "functions": "Returns the list of all available functions (excluding aliases).", 80 "getAttributes": "List all attributes accessible from the supplied object.", 81 "getMethods": "List all methods signatures accessible from the supplied object.", 82 "getSignature": "List all attributes and methods signatures accessible from the supplied object.", 83 "include": "Returns the result of the named template rendering (like template but it is possible to capture the output).", 84 "localAlias": "Defines an alias (go template function) using the function (exec, run, include, template). Executed in the context of the function it maps to.", 85 "raise": "Raise a formated error.", 86 "run": "Returns the result of the shell command as string.", 87 "substitute": "Applies the supplied regex substitute specified on the command line on the supplied string (see --substitute).", 88 "templateNames": "Returns the list of available templates names.", 89 "templates": "Returns the list of available templates.", 90 } 91 92 func (t *Template) addRuntimeFuncs() { 93 var funcs = dictionary{ 94 "alias": t.alias, 95 "aliases": t.getAliases, 96 "allFunctions": t.getAllFunctions, 97 "assert": assert, 98 "assertWarning": assertWarning, 99 "categories": t.getCategories, 100 "current": t.current, 101 "ellipsis": t.ellipsis, 102 "exec": t.execCommand, 103 "exit": exit, 104 "func": t.defineFunc, 105 "function": t.getFunction, 106 "functions": t.getFunctions, 107 "getAttributes": getAttributes, 108 "getMethods": getMethods, 109 "getSignature": getSignature, 110 "include": t.include, 111 "localAlias": t.localAlias, 112 "raise": raise, 113 "run": t.runCommand, 114 "substitute": t.substitute, 115 "templateNames": t.getTemplateNames, 116 "templates": t.Templates, 117 } 118 119 t.AddFunctions(funcs, runtimeFunc, FuncOptions{ 120 FuncHelp: runtimeFuncsHelp, 121 FuncArgs: runtimeFuncsArgs, 122 FuncAliases: runtimeFuncsAliases, 123 }) 124 } 125 126 func exit(exitValue int) int { os.Exit(exitValue); return exitValue } 127 func (t Template) current() string { return t.folder } 128 129 func (t *Template) alias(name, function string, source interface{}, args ...interface{}) (string, error) { 130 return t.addAlias(name, function, source, false, false, args...) 131 } 132 133 func (t *Template) localAlias(name, function string, source interface{}, args ...interface{}) (string, error) { 134 return t.addAlias(name, function, source, true, false, args...) 135 } 136 137 func (t *Template) defineFunc(name, function string, source, config interface{}) (string, error) { 138 return t.addAlias(name, function, source, true, true, config) 139 } 140 141 func (t *Template) execCommand(command interface{}, args ...interface{}) (interface{}, error) { 142 return t.exec(collections.Interface2string(command), args...) 143 } 144 145 func (t *Template) runCommand(command interface{}, args ...interface{}) (interface{}, error) { 146 return t.run(collections.Interface2string(command), args...) 147 } 148 149 func (t *Template) include(source interface{}, context ...interface{}) (interface{}, error) { 150 content, _, err := t.runTemplate(collections.Interface2string(source), context...) 151 if source == content { 152 return nil, fmt.Errorf("Unable to find a template or a file named %s", source) 153 } 154 return content, err 155 } 156 157 // Define alias to an existing command 158 func (t *Template) addAlias(name, function string, source interface{}, local, context bool, defaultArgs ...interface{}) (result string, err error) { 159 for !local && t.parent != nil { 160 // local specifies if the alias should be executed in the context of the template where it is 161 // defined or in the context of the top parent 162 t = t.parent 163 } 164 165 f := t.run 166 167 switch function { 168 case "run": 169 case "exec": 170 f = t.exec 171 case "template", "include": 172 f = t.runTemplateItf 173 default: 174 err = fmt.Errorf("%s unsupported for alias %s (only run, exec, template and include are supported)", function, name) 175 return 176 } 177 178 if !context { 179 t.aliases[name] = FuncInfo{ 180 function: func(args ...interface{}) (result interface{}, err error) { 181 return f(collections.Interface2string(source), append(defaultArgs, args...)...) 182 }, 183 group: "User defined aliases", 184 } 185 return 186 } 187 188 var config iDictionary 189 190 switch len(defaultArgs) { 191 case 0: 192 config = collections.CreateDictionary() 193 case 1: 194 if defaultArgs[0] == nil { 195 err = fmt.Errorf("Default configuration is nil") 196 return 197 } 198 if reflect.TypeOf(defaultArgs[0]).Kind() == reflect.String { 199 var configFromString interface{} 200 if err = collections.ConvertData(fmt.Sprint(defaultArgs[0]), &configFromString); err != nil { 201 err = fmt.Errorf("Function configuration must be valid type: %v\n%v", defaultArgs[0], err) 202 return 203 } 204 defaultArgs[0] = configFromString 205 } 206 if config, err = collections.TryAsDictionary(defaultArgs[0]); err != nil { 207 err = fmt.Errorf("Function configuration must be valid dictionary: %[1]T %[1]v", defaultArgs[0]) 208 return 209 } 210 default: 211 return "", fmt.Errorf("Too many parameters supplied") 212 } 213 214 for key, val := range config.AsMap() { 215 switch strings.ToLower(key) { 216 case "d", "desc", "description": 217 config.Set("description", val) 218 case "g", "group": 219 config.Set("group", val) 220 case "a", "args", "arguments": 221 switch val := val.(type) { 222 case iList: 223 config.Set("args", val) 224 default: 225 err = fmt.Errorf("%[1]s must be a list of strings: %[2]T %[2]v", key, val) 226 return 227 } 228 case "aliases": 229 switch val := val.(type) { 230 case iList: 231 config.Set("aliases", val) 232 default: 233 err = fmt.Errorf("%[1]s must be a list of strings: %[2]T %[2]v", key, val) 234 return 235 } 236 case "def", "default", "defaults": 237 switch val := val.(type) { 238 case iDictionary: 239 config.Set("def", val) 240 default: 241 err = fmt.Errorf("%s must be a dictionary: %T", key, val) 242 return 243 } 244 default: 245 return "", fmt.Errorf("Unknown configuration %s", key) 246 } 247 } 248 249 emptyList := collections.CreateList() 250 fi := FuncInfo{ 251 name: name, 252 group: defval(config.Get("group"), "User defined functions").(string), 253 description: defval(config.Get("description"), "").(string), 254 arguments: defval(config.Get("args"), emptyList).(iList).Strings(), 255 aliases: defval(config.Get("aliases"), emptyList).(iList).Strings(), 256 } 257 258 defaultValues := defval(config.Get("def"), collections.CreateDictionary()).(iDictionary) 259 260 fi.in = fmt.Sprintf("%s", strings.Join(fi.arguments, ", ")) 261 for i := range fi.arguments { 262 // We only keep the arg name and get rid of any supplemental information (likely type) 263 fi.arguments[i] = strings.Fields(fi.arguments[i])[0] 264 } 265 266 fi.function = func(args ...interface{}) (result interface{}, err error) { 267 context := collections.CreateDictionary() 268 parentContext, err := collections.TryAsDictionary(t.context) 269 if err != nil { 270 context.Set("DEFAULT", t.context) 271 } 272 273 switch len(args) { 274 case 1: 275 if len(fi.arguments) != 1 { 276 switch arg := args[0].(type) { 277 case string: 278 var out interface{} 279 if collections.ConvertData(arg, &out) == nil { 280 args[0] = out 281 } 282 } 283 284 if arg, err := collections.TryAsDictionary(args[0]); err == nil { 285 context.Merge(arg, defaultValues, parentContext) 286 break 287 } 288 } 289 fallthrough 290 default: 291 templateContext, err := collections.TryAsDictionary(t.context) 292 if err != nil { 293 return nil, err 294 } 295 296 context.Merge(defaultValues, templateContext) 297 for i := range args { 298 if i >= len(fi.arguments) { 299 context.Set("ARGS", args[i:]) 300 break 301 } 302 context.Set(fi.arguments[i], args[i]) 303 } 304 } 305 return f(collections.Interface2string(source), context) 306 } 307 308 t.aliases[name] = fi 309 return 310 } 311 312 // Execute the command (command could be a file, a template or a script) 313 func (t *Template) run(command string, args ...interface{}) (result interface{}, err error) { 314 var filename string 315 316 // We check if the supplied command is a template 317 if command, filename, err = t.runTemplate(command, args...); err != nil { 318 return 319 } 320 321 var cmd *exec.Cmd 322 if filename != "" { 323 cmd, err = utils.GetCommandFromFile(filename, args...) 324 } else { 325 var tempFile string 326 cmd, tempFile, err = utils.GetCommandFromString(command, args...) 327 if tempFile != "" { 328 defer func() { os.Remove(tempFile) }() 329 } 330 } 331 332 if cmd == nil { 333 return 334 } 335 336 var stdout, stderr bytes.Buffer 337 cmd.Stdin = os.Stdin 338 cmd.Stdout = &stdout 339 cmd.Stderr = &stderr 340 cmd.Dir = t.folder 341 log.Notice("Launching", cmd.Args, "in", cmd.Dir) 342 343 if err = cmd.Run(); err == nil { 344 result = stdout.String() 345 } else { 346 err = fmt.Errorf("Error %v: %s", err, stderr.String()) 347 } 348 return 349 } 350 351 func (t *Template) tryConvert(value string) interface{} { 352 if data, err := t.dataConverter(value); err == nil { 353 // The result of the command is a valid data structure 354 if reflect.TypeOf(data).Kind() != reflect.String { 355 return data 356 } 357 } 358 return value 359 } 360 361 // Execute the command (command could be a file, a template or a script) and convert its result as data if possible 362 func (t *Template) exec(command string, args ...interface{}) (result interface{}, err error) { 363 if result, err = t.run(command, args...); err == nil { 364 if result == nil { 365 return 366 } 367 result = t.tryConvert(result.(string)) 368 } 369 return 370 } 371 372 func (t Template) runTemplate(source string, context ...interface{}) (resultContent, filename string, err error) { 373 var out bytes.Buffer 374 375 if len(context) == 0 { 376 context = []interface{}{t.context} 377 } 378 // We first try to find a template named <source> 379 internalTemplate := t.Lookup(source) 380 if internalTemplate == nil { 381 // This is not a template, so we try to load file named <source> 382 if !strings.Contains(source, "\n") { 383 tryFile := source 384 if !path.IsAbs(tryFile) { 385 tryFile = path.Join(t.folder, tryFile) 386 } 387 if fileContent, e := ioutil.ReadFile(tryFile); e != nil { 388 if _, ok := e.(*os.PathError); !ok { 389 err = e 390 return 391 } 392 } else { 393 source = string(t.applyRazor(fileContent)) 394 filename = tryFile 395 } 396 } 397 // There is no file named <source>, so we consider that <source> is the content 398 inline, e := t.New("inline").Parse(source) 399 if e != nil { 400 err = e 401 return 402 } 403 internalTemplate = inline 404 } 405 406 // We execute the resulting template 407 if err = internalTemplate.Execute(&out, hcl.SingleContext(context...)); err != nil { 408 return 409 } 410 411 resultContent = out.String() 412 if resultContent != source { 413 // If the content is different from the source, that is because the source contains 414 // templating, In that case, we do not consider the original filename as unaltered source. 415 filename = "" 416 } 417 return 418 } 419 420 func (t Template) runTemplateItf(source string, context ...interface{}) (interface{}, error) { 421 content, _, err := t.runTemplate(source, context...) 422 return content, err 423 } 424 425 // This function is used to call a function that requires its last argument to be expanded ... 426 func (t Template) ellipsis(function string, args ...interface{}) (interface{}, error) { 427 last := len(args) - 1 428 if last >= 0 && args[last] == nil { 429 args[last] = []interface{}{} 430 } else if last < 0 || reflect.TypeOf(args[last]).Kind() != reflect.Slice { 431 return nil, fmt.Errorf("The last argument must be a slice") 432 } 433 434 lastArg := reflect.ValueOf(args[last]) 435 argsStr := make([]string, 0, last+lastArg.Len()) 436 context := make(dictionary) 437 438 convertArg := func(arg interface{}) { 439 argName := fmt.Sprintf("ARG%d", len(argsStr)+1) 440 argsStr = append(argsStr, fmt.Sprintf(".%s", argName)) 441 context[argName] = arg 442 } 443 444 for i := range args[:last] { 445 convertArg(args[i]) 446 } 447 448 for i := 0; i < lastArg.Len(); i++ { 449 convertArg(lastArg.Index(i).Interface()) 450 } 451 452 template := fmt.Sprintf("%s %s %s %s", t.delimiters[0], function, strings.Join(argsStr, " "), t.delimiters[1]) 453 result, _, err := t.runTemplate(template, context) 454 return t.tryConvert(result), err 455 } 456 457 func getAttributes(object interface{}) string { 458 if object == nil { 459 return "" 460 } 461 462 t := reflect.TypeOf(object) 463 if t.Kind() == reflect.Ptr { 464 t = t.Elem() 465 } 466 numField := 0 467 if t.Kind() == reflect.Struct { 468 numField = t.NumField() 469 } 470 result := make([]string, 0, numField) 471 for i := 0; i < numField; i++ { 472 name := t.Field(i).Name 473 if !collections.IsExported(name) { 474 continue 475 } 476 typeName := color.HiBlackString(fmt.Sprint(t.Field(i).Type)) 477 attrName := color.HiGreenString(name) 478 result = append(result, fmt.Sprintf("%s %s", attrName, typeName)) 479 } 480 return strings.Join(result, "\n") 481 } 482 483 func getMethods(object interface{}) string { 484 if object == nil { 485 return "" 486 } 487 488 t := reflect.TypeOf(object) 489 result := make([]string, 0, t.NumMethod()) 490 for i := 0; i < t.NumMethod(); i++ { 491 result = append(result, FuncInfo{ 492 name: t.Method(i).Name, 493 function: t.Method(i).Func.Interface(), 494 }.getSignature(true)) 495 } 496 return strings.Join(result, "\n") 497 } 498 499 func getSignature(object interface{}) string { 500 attributes := getAttributes(object) 501 methods := getMethods(object) 502 if attributes != "" && methods != "" { 503 return attributes + "\n\n" + methods 504 } 505 return attributes + methods 506 } 507 508 func raise(args ...interface{}) (string, error) { 509 return "", fmt.Errorf(utils.FormatMessage(args...)) 510 } 511 512 func assert(test interface{}, args ...interface{}) (string, error) { 513 if isZero(test) { 514 if len(args) == 0 { 515 args = []interface{}{"Assertion failed"} 516 } 517 return raise(args...) 518 } 519 return "", nil 520 } 521 522 func assertWarning(test interface{}, args ...interface{}) string { 523 if isZero(test) { 524 if len(args) == 0 { 525 args = []interface{}{"Assertion failed"} 526 } 527 Log.Warning(utils.FormatMessage(args...)) 528 } 529 return "" 530 }