// Package scpt provides an interpreter for my simple applescript-like // scripting language package scpt import ( "errors" "fmt" "github.com/alecthomas/participle" "github.com/antonmedv/expr" "io" "reflect" "strings" ) // Vars stores any variables set during script runtime var Vars = map[string]interface{}{} // FuncMap is a map of strings mapped to suitable script functions type FuncMap map[string]func(map[string]interface{}) (interface{}, error) // Funcs stores the functions allowed for use in a script var Funcs = FuncMap{ "display-dialog": displayDialog, "do-shell-script": doShellScript, } // AddFuncs adds all functions from the provided FuncMap into // the Funcs variable func AddFuncs(fnMap FuncMap) { // Add each function to Funcs for name, fn := range fnMap { Funcs[name] = fn } } // AddVars adds all functions from the provided map into // the Vars variable func AddVars(varMap map[string]interface{}) { for name, val := range varMap { // Add each variable to Vars Vars[name] = val } } // Parse uses participle to parse a script from r into a new AST func Parse(r io.Reader) (*AST, error) { // Build parser from empty AST struct parser, err := participle.Build(&AST{}) if err != nil { return nil, err } // Create new empty AST struct ast := &AST{} // Parse script from provided reader into ast err = parser.Parse(r, ast) if err != nil { return nil, err } // Return filled AST struct return ast, nil } // ParseValue parses a Value struct into a go value func ParseValue(val *Value) (interface{}, error) { // Determine which value was provided and return it if val.String != nil { // Return unquoted string return strings.Trim(*val.String, `"`), nil } else if val.Bool != nil { // Return dereferenced boolean return *val.Bool, nil } else if val.Float != nil { // Return dereferenced float return *val.Float, nil } else if val.Integer != nil { // Return dereferenced integer return *val.Integer, nil } else if val.SubCmd != nil { // Return reference to subcommand return val.SubCmd, nil } else if val.VarVal != nil { // Return value of provided key return Vars[*val.VarVal], nil } else if val.Expr != nil { // Parse value of left side of expression left, _ := ParseValue(val.Expr.Left) // If value is string, requote if isStr(left) { left = requoteStr(left.(string)) } // Parse value of right side of expression right, _ := ParseValue(val.Expr.Right) // If value is string, requote if isStr(right) { right = requoteStr(right.(string)) } // Create string expression from halves and operator exp := fmt.Sprintf( "%v %s %v", left, val.Expr.Op, right, ) // Compile string expression program, err := expr.Compile(strings.ReplaceAll(exp, "^", "**")) if err != nil { return nil, err } // Run expression out, err := expr.Run(program, Vars) if err != nil { return nil, err } // Return expression output value return out, nil } return nil, nil } // Add quotes to an unquoted string func requoteStr(s string) string { // Return quoted string return `"` + s + `"` } // Check if i is a string func isStr(i interface{}) bool { // if type of input is string, return true if reflect.TypeOf(i).String() == "string" { return true } return false } // UnwrapArgs takes a slice of Arg structs and returns a map // storing the argument name and its value. If the argument has // no name, its key will be an empty string func UnwrapArgs(args []*Arg) (map[string]interface{}, error) { // Create new empty map of strings to any type argMap := map[string]interface{}{} // For each argument for _, arg := range args { // Parse value into interface{} val, err := ParseValue(arg.Value) if err != nil { return nil, err } // If value is function call if IsFuncCall(val) { // Call function, setting its value as the argument's value argMap[arg.Key], err = CallFunction(val.(*FuncCall)) if err != nil { return nil, err } // Skip further code and start next loop continue } // Set argument value to parsed value argMap[arg.Key] = val } // Return map of arguments return argMap, nil } // IsFuncCall checks if val is a FuncCall struct func IsFuncCall(val interface{}) bool { // If type of val contains .FuncCall, return true if strings.Contains(reflect.TypeOf(val).String(), ".FuncCall") { return true } return false } // CallFunction executes a given function call in the form of // a FuncCall struct func CallFunction(call *FuncCall) (interface{}, error) { // Unwrap provided arguments argMap, err := UnwrapArgs(call.Args) if err != nil { return nil, err } // Attempt to get function from Funcs map fn, ok := Funcs[call.Name] if !ok { return nil, errors.New("no such function: " + call.Name) } // Return value received from function return fn(argMap) }