diff --git a/ast.go b/ast.go index 6b41421..61900b1 100644 --- a/ast.go +++ b/ast.go @@ -1,66 +1,105 @@ package scpt import ( - "reflect" - "strings" + "fmt" + "github.com/alecthomas/participle/lexer" ) +// AST stores the root of the Abstract Syntax Tree for scpt type AST struct { + Pos lexer.Position Commands []*Command `@@*` } -func (ast *AST) Execute() { +// Execute traverses the AST and executes any commands, it returns an error +// containing the position at which the error was encountered and the error +// itself +func (ast *AST) Execute() error { for _, cmd := range ast.Commands { if cmd.Vars != nil { for _, Var := range cmd.Vars { - val := ParseValue(Var.Value) - if strings.Contains(reflect.TypeOf(val).String(), ".FuncCall") { + val, err := ParseValue(Var.Value) + if err != nil { + return fmt.Errorf("%s: %s", Var.Value.Pos, err) + } + if IsFuncCall(val) { Call := val.(*FuncCall) - Vars[Var.Key], _ = CallFunction(Call) + Vars[Var.Key], err = CallFunction(Call) + if err != nil { + return fmt.Errorf("%s: %s", Var.Value.Pos, err) + } } else { Vars[Var.Key] = val } } } else if cmd.Calls != nil { for _, Call := range cmd.Calls { - _, _ = CallFunction(Call) + _, err := CallFunction(Call) + if err != nil { + return fmt.Errorf("%s: %s", Call.Pos, err) + } } } } + return nil } +// Command stores any commands encountered while parsing a script type Command struct { + Pos lexer.Position Vars []*Var `( @@` Calls []*FuncCall `| @@ )` } +// FuncCall stores any function calls encountered while parsing a script type FuncCall struct { - Name string `@Ident` - Args []*Arg `@@*` + Pos lexer.Position + Name string `@Ident @("-" Ident)*` + Args []*Arg `@@*` } +// Arg stores arguments for function calls type Arg struct { - Key string `"with" @Ident` - Value *Value `@@` + Pos lexer.Position + Key string `("with" @Ident)?` + Value *Value `@@` } +// Var stores any variables encountered while parsing a script type Var struct { - Key string `"set" @Ident "to"` - Value *Value `@@` + Pos lexer.Position + Key string `"set" @Ident "to"` + Value *Value `@@` } +// Value stores any literal values encountered while parsing a script type Value struct { - String *string ` @String` - Float *float64 `| @Float` - Integer *int64 `| @Int` - Bool *Bool `| @("true" | "false")` - SubCmd *FuncCall `| "(" @@ ")"` - VarVal *string `| "$" @Ident` + Pos lexer.Position + String *string ` @String` + Float *float64 `| @Float` + Integer *int64 `| @Int` + Bool *Bool `| @("true" | "false")` + SubCmd *FuncCall `| "(" @@ ")"` + VarVal *string `| "$" @Ident` + Expr *Expression `| "{" @@ "}"` } +// Bool stores boolean values encountered while parsing a script. +// It is required for the Capture method type Bool bool +// Capture parses a boolean literal encountered in the script into +// a Go boolean value func (b *Bool) Capture(values []string) error { *b = values[0] == "true" return nil } + +// Expression stores any expressions encountered while parsing a +// script for later evaluation +type Expression struct { + Pos lexer.Position + Left *Value `@@` + Op string `@( ">" | ">" "=" | "<" | "<" "=" | "!" "=" | "=" "=" | "+" | "-" | "*" | "/" | "^" | "%")` + Right *Value `@@` +} diff --git a/cmd/scpt/main.go b/cmd/scpt/main.go index 9fbd2c0..2842a51 100644 --- a/cmd/scpt/main.go +++ b/cmd/scpt/main.go @@ -23,5 +23,8 @@ func main() { if err != nil { log.Fatalln("Error parsing file:", err) } - ast.Execute() -} \ No newline at end of file + err = ast.Execute() + if err != nil { + log.Fatalln("Error executing script:", err) + } +} diff --git a/defaults.go b/defaults.go new file mode 100644 index 0000000..2aba936 --- /dev/null +++ b/defaults.go @@ -0,0 +1,54 @@ +package scpt + +import ( + "errors" + "fmt" + "github.com/gen2brain/dlgs" + "os" + "os/exec" +) + +func displayDialog(args map[string]interface{}) (interface{}, error) { + title, ok := args["title"] + if !ok { + return nil, errors.New("title not provided") + } + text, ok := args[""] + if !ok { + return nil, errors.New("text not provided") + } + switch args["type"] { + case "yesno": + return dlgs.Question(fmt.Sprint(title), fmt.Sprint(text), true) + case "info": + return dlgs.Info(fmt.Sprint(title), fmt.Sprint(text)) + case "error": + return dlgs.Error(fmt.Sprint(title), fmt.Sprint(text)) + case "entry": + defaultText, ok := args["default"] + if !ok { + defaultText = "" + } + input, _, err := dlgs.Entry(fmt.Sprint(title), fmt.Sprint(text), fmt.Sprint(defaultText)) + return input, err + default: + return nil, fmt.Errorf("unknown dialog type: %v", args["type"]) + } +} + +func doShellScript(args map[string]interface{}) (interface{}, error) { + script, ok := args[""].(string) + if ok { + cmd := exec.Command("sh", "-c", script) + cmd.Stdout = os.Stdout + _ = cmd.Run() + return "", nil + } else { + return nil, errors.New("script not provided") + } +} + +func print(args map[string]interface{}) (interface{}, error) { + fmt.Println(args) + return nil, nil +} \ No newline at end of file diff --git a/go.mod b/go.mod index eb1926c..e6cb23a 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,8 @@ go 1.16 require ( github.com/alecthomas/participle v0.7.1 + github.com/antonmedv/expr v1.8.9 github.com/gen2brain/dlgs v0.0.0-20210222160047-2f436553172f + github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe // indirect + github.com/rs/zerolog v1.20.0 ) diff --git a/go.sum b/go.sum index 2352baf..3d8e880 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,70 @@ +github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/alecthomas/participle v0.7.1 h1:2bN7reTw//5f0cugJcTOnY/NYZcWQOaajW+BwZB5xWs= github.com/alecthomas/participle v0.7.1/go.mod h1:HfdmEuwvr12HXQN44HPWXR0lHmVolVYe4dyL6lQ3duY= +github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 h1:GDQdwm/gAcJcLAKQQZGOJ4knlw+7rfEQQcmwTbt4p5E= github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= +github.com/antonmedv/expr v1.8.9 h1:O9stiHmHHww9b4ozhPx7T6BK7fXfOCHJ8ybxf0833zw= +github.com/antonmedv/expr v1.8.9/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM= +github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= github.com/gen2brain/dlgs v0.0.0-20210222160047-2f436553172f h1:HPKrg4xeWLWOGAJGVJIYXWhVSdy2kaihuoSy7kBP7S4= github.com/gen2brain/dlgs v0.0.0-20210222160047-2f436553172f/go.mod h1:/eFcjDXaU2THSOOqLxOPETIbHETnamk8FA/hMjhg/gU= github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe h1:rcf1P0fm+1l0EjG16p06mYLj9gW9X36KgdHJ/88hS4g= github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= +github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0= +github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498 h1:4CFNy7/q7P06AsIONZzuWy7jcdqEmYQvOZ9FAFZdbls= +github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= +github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= +github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= +github.com/sanity-io/litter v1.2.0 h1:DGJO0bxH/+C2EukzOSBmAlxmkhVMGqzvcx/rvySYw9M= +github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74 h1:4cFkmztxtMslUX2SctSl+blCyXfpzhGOy9LhKAqSMA4= +golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/scpt.go b/scpt.go index 6d7a2e0..21a5f19 100644 --- a/scpt.go +++ b/scpt.go @@ -4,34 +4,42 @@ import ( "errors" "fmt" "github.com/alecthomas/participle" - "github.com/gen2brain/dlgs" + "github.com/antonmedv/expr" "io" - "os" - "os/exec" + "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{ - "displaydialog": displayDialog, - "doshellscript": doShellScript, + "display-dialog": displayDialog, + "do-shell-script": doShellScript, + "print": print, } +// AddFuncs adds all functions from the provided FuncMap into +// the Funcs variable func AddFuncs(fnMap FuncMap) { 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 { Vars[name] = val } } +// Parse uses participle to parse a script from r into a new AST func Parse(r io.Reader) (*AST, error) { parser, err := participle.Build(&AST{}) if err != nil { @@ -45,71 +53,102 @@ func Parse(r io.Reader) (*AST, error) { return ast, nil } -func ParseValue(val *Value) interface{} { +// ParseValue parses a Value struct into a go value +func ParseValue(val *Value) (interface{}, error) { if val.String != nil { - return strings.Trim(*val.String, `"`) + return strings.Trim(*val.String, `"`), nil } else if val.Bool != nil { - return *val.Bool + return *val.Bool, nil } else if val.Float != nil { - return *val.Float + return *val.Float, nil } else if val.Integer != nil { - return *val.Integer + return *val.Integer, nil } else if val.SubCmd != nil { - return val.SubCmd + return val.SubCmd, nil } else if val.VarVal != nil { - return Vars[*val.VarVal] - } - return nil -} - -func UnwrapArgs(args []*Arg) map[string]interface{} { - argMap := map[string]interface{}{} - for _, arg := range args { - argMap[arg.Key] = ParseValue(arg.Value) - } - return argMap -} - -func CallFunction(call *FuncCall) (interface{}, error) { - argMap := UnwrapArgs(call.Args) - return Funcs[call.Name](argMap) -} - -func displayDialog(args map[string]interface{}) (interface{}, error) { - title, ok := args["title"] - if !ok { - return nil, errors.New("title not provided") - } - text, ok := args["text"] - if !ok { - return nil, errors.New("text not provided") - } - switch args["type"] { - case "yesno": - return dlgs.Question(fmt.Sprint(title), fmt.Sprint(text), true) - case "info": - return dlgs.Info(fmt.Sprint(title), fmt.Sprint(text)) - case "error": - return dlgs.Error(fmt.Sprint(title), fmt.Sprint(text)) - case "entry": - defaultText, ok := args["default"] - if !ok { - defaultText = "" + return Vars[*val.VarVal], nil + } else if val.Expr != nil { + left, _ := ParseValue(val.Expr.Left) + if isStr(left) { + left = requoteStr(left.(string)) } - input, _, err := dlgs.Entry(fmt.Sprint(title), fmt.Sprint(text), fmt.Sprint(defaultText)) - return input, err + right, _ := ParseValue(val.Expr.Right) + if isStr(right) { + right = requoteStr(right.(string)) + } + exp := fmt.Sprintf( + "%v %s %v", + left, + val.Expr.Op, + right, + ) + fmt.Println(exp) + program, err := expr.Compile(strings.ReplaceAll(exp, "^", "**")) + if err != nil { + return nil, err + } + out, err := expr.Run(program, Vars) + if err != nil { + return nil, err + } + return out, nil } return nil, nil } -func doShellScript(args map[string]interface{}) (interface{}, error) { - script, ok := args["content"].(string) - if ok { - cmd := exec.Command("sh", "-c", script) - cmd.Stdout = os.Stdout - _ = cmd.Run() - return "", nil - } else { - return nil, errors.New("script not provided") +// Add quotes to an unquoted string +func requoteStr(s string) string { + return `"` + s + `"` +} + +// Check if i is a string +func isStr(i interface{}) bool { + if reflect.TypeOf(i).String() == "string" { + return true } -} \ No newline at end of file + 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) { + argMap := map[string]interface{}{} + for _, arg := range args { + val, err := ParseValue(arg.Value) + if err != nil { + return nil, err + } + if IsFuncCall(val) { + argMap[arg.Key], err = CallFunction(val.(*FuncCall)) + if err != nil { + return nil, err + } + continue + } + argMap[arg.Key] = val + } + return argMap, nil +} + +// IsFuncCall checks if val is a FuncCall struct +func IsFuncCall(val interface{}) bool { + 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) { + argMap, err := UnwrapArgs(call.Args) + if err != nil { + return nil, err + } + fn, ok := Funcs[call.Name] + if !ok { + return nil, errors.New("no such function: " + call.Name) + } + return fn(argMap) +} diff --git a/test.scpt b/test.scpt index 0ce9336..6886c2b 100644 --- a/test.scpt +++ b/test.scpt @@ -1,5 +1,6 @@ -set y to (displaydialog with text "Hello" with title 12 with type "yesno") -displaydialog with text "Goodbye" with title 21 with type "error" -doshellscript with content "notify-send Test Notification" -set g to (displaydialog with text "Test 2" with title 30 with type "entry" with default "text") -set x to $g \ No newline at end of file +set y to (display-dialog "Hello" with title 12 with type "yesno") +display-dialog "Goodbye" with title 21 with type "error" +do-shell-script "notify-send Test Notification" +print (display-dialog {"Test " + "2"} with title 30 with type "entry" with default "text") +set x to $y +print { 3+4 } \ No newline at end of file