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  }