diff --git a/compiler/stdlib/exec.go b/compiler/stdlib/exec.go new file mode 100644 index 0000000..2577d10 --- /dev/null +++ b/compiler/stdlib/exec.go @@ -0,0 +1,116 @@ +package stdlib + +import ( + "os/exec" + + "github.com/d5/tengo/objects" +) + +var execModule = map[string]objects.Object{ + // look_path(file string) => string/error + "look_path": FuncASRSE(exec.LookPath), + // command(name string, args array(string)) => imap(cmd) + "command": &objects.UserFunction{Value: execCommand}, +} + +func execCmdImmutableMap(cmd *exec.Cmd) *objects.ImmutableMap { + return &objects.ImmutableMap{ + Value: map[string]objects.Object{ + // combined_output() => bytes/error + "combined_output": FuncARYE(cmd.CombinedOutput), + // output() => bytes/error + "output": FuncARYE(cmd.Output), + // run() => error + "run": FuncARE(cmd.Run), + // start() => error + "start": FuncARE(cmd.Start), + // wait() => error + "wait": FuncARE(cmd.Wait), + // set_path(path string) + "set_path": &objects.UserFunction{ + Value: func(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + return nil, objects.ErrWrongNumArguments + } + + s1, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + cmd.Path = s1 + + return objects.UndefinedValue, nil + }, + }, + // set_dir(dir string) + "set_dir": &objects.UserFunction{ + Value: func(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + return nil, objects.ErrWrongNumArguments + } + + s1, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + cmd.Dir = s1 + + return objects.UndefinedValue, nil + }, + }, + // set_env(env array(string)) + "set_env": &objects.UserFunction{ + Value: func(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + return nil, objects.ErrWrongNumArguments + } + + envs, err := stringArray(args[0]) + if err != nil { + return nil, err + } + + cmd.Env = envs + + return objects.UndefinedValue, nil + }, + }, + // process() => imap(process) + "process": &objects.UserFunction{ + Value: func(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 0 { + return nil, objects.ErrWrongNumArguments + } + + return osProcessImmutableMap(cmd.Process), nil + }, + }, + // TODO: implement pipes + //"stderr_pipe": nil, + //"stdin_pipe": nil, + //"stdout_pipe": nil, + }, + } +} + +func execCommand(args ...objects.Object) (objects.Object, error) { + if len(args) != 2 { + return nil, objects.ErrWrongNumArguments + } + + name, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + arg, err := stringArray(args[1]) + if err != nil { + return nil, err + } + + res := exec.Command(name, arg...) + + return execCmdImmutableMap(res), nil +} diff --git a/compiler/stdlib/func_typedefs.go b/compiler/stdlib/func_typedefs.go index 022c77f..ab07845 100644 --- a/compiler/stdlib/func_typedefs.go +++ b/compiler/stdlib/func_typedefs.go @@ -95,6 +95,25 @@ func FuncARSE(fn func() (string, error)) *objects.UserFunction { } } +// FuncARYE transform a function of 'func() ([]byte, error)' signature +// into a user function object. +func FuncARYE(fn func() ([]byte, error)) *objects.UserFunction { + return &objects.UserFunction{ + Value: func(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 0 { + return nil, objects.ErrWrongNumArguments + } + + res, err := fn() + if err != nil { + return wrapError(err), nil + } + + return &objects.Bytes{Value: res}, nil + }, + } +} + // FuncARF transform a function of 'func() float64' signature // into a user function object. func FuncARF(fn func() float64) *objects.UserFunction { diff --git a/compiler/stdlib/func_typedefs_test.go b/compiler/stdlib/func_typedefs_test.go index d6baed4..610e132 100644 --- a/compiler/stdlib/func_typedefs_test.go +++ b/compiler/stdlib/func_typedefs_test.go @@ -286,6 +286,75 @@ func TestFuncAFIRB(t *testing.T) { assert.Equal(t, objects.ErrWrongNumArguments, err) } +func TestFuncAIRSsE(t *testing.T) { + uf := stdlib.FuncAIRSsE(func(a int) ([]string, error) { + return []string{"foo", "bar"}, nil + }) + ret, err := uf.Call(&objects.Int{Value: 10}) + assert.NoError(t, err) + assert.Equal(t, array(&objects.String{Value: "foo"}, &objects.String{Value: "bar"}), ret) + uf = stdlib.FuncAIRSsE(func(a int) ([]string, error) { + return nil, errors.New("some error") + }) + ret, err = uf.Call(&objects.Int{Value: 10}) + assert.NoError(t, err) + assert.Equal(t, &objects.Error{Value: &objects.String{Value: "some error"}}, ret) + ret, err = uf.Call() + assert.Equal(t, objects.ErrWrongNumArguments, err) +} + +func TestFuncARB(t *testing.T) { + uf := stdlib.FuncARB(func() bool { return true }) + ret, err := uf.Call() + assert.NoError(t, err) + assert.Equal(t, objects.TrueValue, ret) + ret, err = uf.Call(objects.TrueValue) + assert.Equal(t, objects.ErrWrongNumArguments, err) +} + +func TestFuncARYE(t *testing.T) { + uf := stdlib.FuncARYE(func() ([]byte, error) { + return []byte("foo bar"), nil + }) + ret, err := uf.Call() + assert.NoError(t, err) + assert.Equal(t, &objects.Bytes{Value: []byte("foo bar")}, ret) + uf = stdlib.FuncARYE(func() ([]byte, error) { + return nil, errors.New("some error") + }) + ret, err = uf.Call() + assert.NoError(t, err) + assert.Equal(t, &objects.Error{Value: &objects.String{Value: "some error"}}, ret) + ret, err = uf.Call(objects.TrueValue) + assert.Equal(t, objects.ErrWrongNumArguments, err) +} + +func TestFuncASRIE(t *testing.T) { + uf := stdlib.FuncASRIE(func(a string) (int, error) { return 5, nil }) + ret, err := uf.Call(&objects.String{Value: "foo"}) + assert.NoError(t, err) + assert.Equal(t, &objects.Int{Value: 5}, ret) + uf = stdlib.FuncASRIE(func(a string) (int, error) { return 0, errors.New("some error") }) + ret, err = uf.Call(&objects.String{Value: "foo"}) + assert.NoError(t, err) + assert.Equal(t, &objects.Error{Value: &objects.String{Value: "some error"}}, ret) + ret, err = uf.Call() + assert.Equal(t, objects.ErrWrongNumArguments, err) +} + +func TestFuncAYRIE(t *testing.T) { + uf := stdlib.FuncAYRIE(func(a []byte) (int, error) { return 5, nil }) + ret, err := uf.Call(&objects.Bytes{Value: []byte("foo")}) + assert.NoError(t, err) + assert.Equal(t, &objects.Int{Value: 5}, ret) + uf = stdlib.FuncAYRIE(func(a []byte) (int, error) { return 0, errors.New("some error") }) + ret, err = uf.Call(&objects.Bytes{Value: []byte("foo")}) + assert.NoError(t, err) + assert.Equal(t, &objects.Error{Value: &objects.String{Value: "some error"}}, ret) + ret, err = uf.Call() + assert.Equal(t, objects.ErrWrongNumArguments, err) +} + func array(elements ...objects.Object) *objects.Array { return &objects.Array{Value: elements} } diff --git a/compiler/stdlib/stdlib.go b/compiler/stdlib/stdlib.go index cd24166..3ca5dd3 100644 --- a/compiler/stdlib/stdlib.go +++ b/compiler/stdlib/stdlib.go @@ -6,4 +6,5 @@ import "github.com/d5/tengo/objects" var Modules = map[string]*objects.ImmutableMap{ "math": {Value: mathModule}, "os": {Value: osModule}, + "exec": {Value: execModule}, } diff --git a/runtime/vm_module_test.go b/runtime/vm_module_test.go index 9dc8d49..b63b8b2 100644 --- a/runtime/vm_module_test.go +++ b/runtime/vm_module_test.go @@ -45,6 +45,21 @@ if write_file("./temp", "foobar") { os.remove("./temp") `, "foobar") + // exec.command + expect(t, ` +exec := import("exec") + +echo := func(args) { + cmd := exec.command("echo", args) + if is_error(cmd) { return cmd.value } + output := cmd.output() + if is_error(output) { return output.value } + return output +} + +out = echo(["foo", "bar"]) +`, []byte("foo bar\n")) + // user modules expectWithUserModules(t, `out = import("mod1").bar()`, 5.0, map[string]string{ "mod1": `bar := func() { return 5.0 }`,