diff --git a/ast.go b/ast.go index b45f84a..3c278c5 100644 --- a/ast.go +++ b/ast.go @@ -32,52 +32,71 @@ type AST struct { func (ast *AST) Execute() error { // For each command in AST for _, cmd := range ast.Commands { - // If parsing variables - if cmd.Vars != nil { - // For each variable - for _, Var := range cmd.Vars { - // Parse value of variable - val, err := ParseValue(Var.Value) + // Execute current command + err := executeCmd(cmd) + if err != nil { + return err + } + } + return nil +} + +// Parse and execute command +func executeCmd(cmd *Command) error { + // If parsing variables + if cmd.Vars != nil { + // For each variable + for _, Var := range cmd.Vars { + // Parse value of variable + val, err := ParseValue(Var.Value) + if err != nil { + return fmt.Errorf("%s: %s", Var.Value.Pos, err) + } + // If value of variable is a function call + if IsFuncCall(val) { + // Assert type of val as *FuncCall + Call := val.(*FuncCall) + // Set variable value to function return value + Vars[Var.Key], err = CallFunction(Call) if err != nil { return fmt.Errorf("%s: %s", Var.Value.Pos, err) } - // If value of variable is a function call - if IsFuncCall(val) { - // Assert type of val as *FuncCall - Call := val.(*FuncCall) - // Set variable value to function return value - Vars[Var.Key], err = CallFunction(Call) + } else { + // If value is not a function call, set variable value to parsed value + Vars[Var.Key] = val + } + } + } else if cmd.Calls != nil { + // For each function call + for _, Call := range cmd.Calls { + // Attempt to call function + _, err := CallFunction(Call) + if err != nil { + return fmt.Errorf("%s: %s", Call.Pos, err) + } + } + } else if cmd.Ifs != nil { + // For each if statement + for _, If := range cmd.Ifs { + // Get condition value + condVal, err := callIfFunc(ParseValue(If.Condition)) + if err != nil { + return fmt.Errorf("%s: %s", If.Condition.Pos, err) + } + // Attempt to assert condition type as bool + condBool, ok := condVal.(bool) + if !ok { + return errors.New("condition must be a boolean") + } + // If condition is true + if condBool { + // For each inner command + for _, InnerCmd := range If.InnerCmds { + // Execute command recursively + err := executeCmd(InnerCmd) if err != nil { - return fmt.Errorf("%s: %s", Var.Value.Pos, err) + return fmt.Errorf("%s: %s", InnerCmd.Pos, err) } - } else { - // If value is not a function call, set variable value to parsed value - Vars[Var.Key] = val - } - } - // If parsing function calls - } else if cmd.Calls != nil { - // For each function call - for _, Call := range cmd.Calls { - // Attempt to call function - _, err := CallFunction(Call) - if err != nil { - return fmt.Errorf("%s: %s", Call.Pos, err) - } - } - } else if cmd.Ifs != nil { - for _, If := range cmd.Ifs { - condVal, err := callIfFunc(ParseValue(If.Condition)) - if err != nil { - return err - } - condBool, ok := condVal.(bool) - if !ok { - return errors.New("condition must be a boolean") - } - if condBool { - _, err := CallFunction(If.Action) - return err } } } @@ -88,15 +107,17 @@ func (ast *AST) Execute() error { // Command stores any commands encountered while parsing a script type Command struct { Pos lexer.Position + Tokens []lexer.Token Vars []*Var `( @@` Ifs []*If `| @@` - Calls []*FuncCall `| @@ )` + Calls []*FuncCall `| @@)` } +// If stores any if statements encountered while parsing a script type If struct { Pos lexer.Position - Condition *Value `"if" @@` - Action *FuncCall `"then" @@` + Condition *Value `"if" @@ "{"` + InnerCmds []*Command `@@*"}"` } // FuncCall stores any function calls encountered while parsing a script @@ -124,8 +145,7 @@ type Var struct { type Value struct { Pos lexer.Position String *string ` @String` - Float *float64 `| @Float` - Integer *int64 `| @Int` + Number *float64 `| @Number` Bool *Bool `| @("true" | "false")` SubCmd *FuncCall `| "(" @@ ")"` VarVal *string `| "$" @Ident` @@ -152,7 +172,8 @@ type Expression struct { RightSegs []*ExprRightSeg `@@*` } +// ExprRightSeg stores segments of the right side of an expression type ExprRightSeg struct { - Op string `@( ">" | ">" "=" | "<" | "<" "=" | "!" "=" | "=" "=" | "+" | "-" | "*" | "/" | "^" | "%")` + Op string `@Operator` Right *Value `@@` } diff --git a/lexer.go b/lexer.go new file mode 100644 index 0000000..7346e68 --- /dev/null +++ b/lexer.go @@ -0,0 +1,17 @@ +package scpt + +import ( + "github.com/alecthomas/participle/lexer" + "github.com/alecthomas/participle/lexer/stateful" +) + +// Create custom stateful regex lexer +var scptLexer = lexer.Must(stateful.NewSimple([]stateful.Rule{ + {"Ident", `[a-zA-Z]\w*`, nil}, + {"String", `"[^"]*"`, nil}, + {"Number", `(?:\d*\.)?\d+`, nil}, + {"Punct", `[-[!@#$&()_{}\|:;"',.?/]|]`, nil}, + {"Whitespace", `[ \t\r\n]+`, nil}, + {"Comment", `#[^\n]+`, nil}, + {"Operator", `(>=|<=|>|<|==|!=)|[-+*/^%]`, nil}, +})) \ No newline at end of file diff --git a/scpt.go b/scpt.go index 3cb16c0..6d0380b 100644 --- a/scpt.go +++ b/scpt.go @@ -58,8 +58,12 @@ func AddVars(varMap map[string]interface{}) { // 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{}) + // Build parser from empty AST struct with custom lexer + parser, err := participle.Build( + &AST{}, + participle.Lexer(scptLexer), + participle.Elide("Whitespace", "Comment"), + ) if err != nil { return nil, err } @@ -83,12 +87,9 @@ func ParseValue(val *Value) (interface{}, error) { } else if val.Bool != nil { // Return dereferenced Bool converted to bool return bool(*val.Bool), nil - } else if val.Float != nil { + } else if val.Number != nil { // Return dereferenced float - return *val.Float, nil - } else if val.Integer != nil { - // Return dereferenced integer - return *val.Integer, nil + return *val.Number, nil } else if val.SubCmd != nil { // Return reference to subcommand return val.SubCmd, nil diff --git a/test.scpt b/test.scpt index e31a8a5..c132aba 100644 --- a/test.scpt +++ b/test.scpt @@ -3,5 +3,14 @@ display-dialog "Goodbye" with title 21 with type "error" do-shell-script "notify-send Test Notification" do-shell-script {"echo " + (display-dialog {"Test " + "2"} with title 30 with type "entry" with default "text")} set x to $y -if {3 == 4} then display-dialog "What? Why is 3 equal to 4?" with title "What?!" with type "info" -if {3 == 3} then display-dialog "3 is equal to 3!" with title "New Discovery" with type "info" \ No newline at end of file + +if {3 == 3} { + set x to "hi" + do-shell-script {"echo " + $x} + display-dialog "3 is equal to 3!" with title "New Discovery" with type "info" + if {3 == 4} { + display-dialog "What? Why is 3 equal to 4?" with title "What?!" with type "error" + do-shell-script "echo hi" + } +} +