From 4d4f1058d374ec293660dcc20bc9087fb85f8f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Cuadros?= Date: Thu, 9 Apr 2020 04:25:52 +0200 Subject: [PATCH] starlark/module: os, added command function --- go.mod | 1 + go.sum | 2 + starlark/module/os/os.go | 99 +++++++++++++++++++++++++++ starlark/module/os/os_test.go | 3 + starlark/module/os/testdata/test.star | 23 ++++++- 5 files changed, 127 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 9b39fe0..1f38c9f 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/go-git/go-git/v5 v5.0.0 + github.com/gobs/args v0.0.0-20180315064131-86002b4df18c github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect github.com/hashicorp/go-hclog v0.11.0 github.com/hashicorp/go-plugin v1.0.1-0.20190610192547-a1bc61569a26 diff --git a/go.sum b/go.sum index f382ba7..d495530 100644 --- a/go.sum +++ b/go.sum @@ -211,6 +211,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gobs/args v0.0.0-20180315064131-86002b4df18c h1:3r/O0iUDMwVJx8XCrjcUvfmfbVP3poiT+1dLyYzx8+w= +github.com/gobs/args v0.0.0-20180315064131-86002b4df18c/go.mod h1:ZpqkpUmnBz2Jz7hMGSPRbHtYC82FP/IZ1Y7A2riYH0s= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= diff --git a/starlark/module/os/os.go b/starlark/module/os/os.go index 3acd2fa..1708d22 100644 --- a/starlark/module/os/os.go +++ b/starlark/module/os/os.go @@ -1,10 +1,13 @@ package os import ( + "fmt" "io/ioutil" "os" + "os/exec" "sync" + gobs "github.com/gobs/args" "go.starlark.net/starlark" "go.starlark.net/starlarkstruct" ) @@ -26,6 +29,7 @@ const ( removeAllFuncName = "remove_all" renameFuncName = "rename" tempDirFuncName = "temp_dir" + commandFuncName = "command" ) var ( @@ -57,6 +61,7 @@ func LoadModule() (starlark.StringDict, error) { removeAllFuncName: starlark.NewBuiltin(mkdirFuncName, RemoveAll), renameFuncName: starlark.NewBuiltin(renameFuncName, Rename), tempDirFuncName: starlark.NewBuiltin(tempDirFuncName, TempDir), + commandFuncName: starlark.NewBuiltin(commandFuncName, Command), }, }, } @@ -335,3 +340,97 @@ func Rename(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, k func TempDir(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { return starlark.String(os.TempDir()), nil } + +// Command runs the command and returns its standard output. +// +// outline: os +// functions: +// command(command, shell?, dir?, combined?, env?) +// runs the command and returns its standard output. If the exit code +// it different to zero, an error is triggered. +// params: +// shell bool +// if True execute the command inside of a shell. +// dir string +// working directory of the command. +// combined bool +// if True returns combined standard output and standard error. +// env list +// specifies the environment of the process, each value of the list +// should follow the pattern "key=value". +func Command(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var ( + command string + env *starlark.List + dir string + combined bool + shell bool + ) + + err := starlark.UnpackArgs(renameFuncName, args, kwargs, + "command", &command, + "env?", &env, + "dir?", &dir, + "combined?", &combined, + "shell?", &shell, + ) + + if err != nil { + return nil, err + } + + if shell { + command = fmt.Sprintf("sh -c %q", command) + } + + cmdArgs := gobs.GetArgs(command) + bin, err := exec.LookPath(cmdArgs[0]) + if err != nil { + return nil, err + } + + environment, err := unpackListArg(renameFuncName, "env", env) + if err != nil { + return nil, err + } + + cmd := &exec.Cmd{ + Path: bin, + Args: cmdArgs, + Env: append(os.Environ(), environment...), + Dir: dir, + } + + var output []byte + if combined { + output, err = cmd.CombinedOutput() + } else { + output, err = cmd.Output() + } + + if len(output) >= 1 && output[len(output)-1] == '\n' { + output = output[:len(output)-1] + } + + return starlark.String(output), err +} + +func unpackListArg(fnName, argName string, l *starlark.List) ([]string, error) { + if l == nil { + return []string{}, nil + } + + output := make([]string, l.Len()) + for i := 0; i < l.Len(); i++ { + s, ok := l.Index(i).(starlark.String) + if ok { + output[i] = s.GoString() + continue + } + + return nil, fmt.Errorf("%s: parameter %q expected string at index %d", fnName, argName, i) + + } + + return output, nil +} diff --git a/starlark/module/os/os_test.go b/starlark/module/os/os_test.go index 2883da8..eccbb4e 100644 --- a/starlark/module/os/os_test.go +++ b/starlark/module/os/os_test.go @@ -17,6 +17,9 @@ func TestFile(t *testing.T) { } resolve.AllowFloat = true + resolve.AllowGlobalReassign = true + resolve.AllowLambda = true + thread := &starlark.Thread{Load: testdata.NewLoader(LoadModule, ModuleName)} starlarktest.SetReporter(thread, t) diff --git a/starlark/module/os/testdata/test.star b/starlark/module/os/testdata/test.star index bdcf76b..945ed4e 100644 --- a/starlark/module/os/testdata/test.star +++ b/starlark/module/os/testdata/test.star @@ -33,4 +33,25 @@ os.rename("foo", "bar") os.remove_all("bar") def deleteNotExistant(): os.remove("foo") -assert.fails(deleteNotExistant, "remove foo: no such file or directory") \ No newline at end of file +assert.fails(deleteNotExistant, "remove foo: no such file or directory") + +# test command +temp = os.temp_dir() + "/example-dir" +os.mkdir_all(temp + "/foo/bar", 0o755) +os.chdir(temp) + +assert.eq(os.command("ls -1"), "foo") + +# test command dir +assert.eq(os.command("ls -1", dir="foo"), "bar") + +# test command shell and env +assert.eq(os.command("echo $FOO", shell=True, env=["FOO=foo"]), "foo") + +# test command combined +assert.ne(os.command("not-exists || true", shell=True, combined=True), "") + +# test command error +assert.fails(lambda: os.command("not-exists"), "executable file not found") + +os.remove_all(temp)