From de4c4ae52670b847dccc687765021a179e1e0c39 Mon Sep 17 00:00:00 2001 From: xushiwei Date: Sun, 18 Feb 2024 17:11:19 +0800 Subject: [PATCH 1/6] gsh: exec --- gsh/classfile.go | 19 +++++++++++++++++-- gsh/env.go | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 gsh/env.go diff --git a/gsh/classfile.go b/gsh/classfile.go index cf7b899..9705533 100644 --- a/gsh/classfile.go +++ b/gsh/classfile.go @@ -42,16 +42,31 @@ func (p *App) initApp() { p.ferr = os.Stderr } -// Gop_Exec executes a shell command. -func (p *App) Gop_Exec(name string, args ...string) error { +// Exec executes a shell command with specified environs. +func (p *App) Exec__0(env map[string]string, name string, args ...string) error { + var cmdEnv []string + if env != nil { + cmdEnv = Setenv__0(os.Environ(), env) + } cmd := exec.Command(name, args...) cmd.Stdin = p.fin cmd.Stdout = p.fout cmd.Stderr = p.ferr + cmd.Env = cmdEnv p.err = cmd.Run() return p.err } +// Exec executes a shell command. +func (p *App) Exec__1(name string, args ...string) error { + return p.Exec__0(nil, name, args...) +} + +// Gop_Exec executes a shell command. +func (p *App) Gop_Exec(name string, args ...string) error { + return p.Exec__0(nil, name, args...) +} + // LastErr returns error of last command execution. func (p *App) LastErr() error { return p.err diff --git a/gsh/env.go b/gsh/env.go new file mode 100644 index 0000000..4774cfa --- /dev/null +++ b/gsh/env.go @@ -0,0 +1,41 @@ +/* + Copyright 2024 Qiniu Limited (qiniu.com) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package gsh + +import ( + "strings" +) + +// Setenv overwrites environments with specified env. +func Setenv__0(ret []string, env map[string]string) []string { + for k, v := range env { + ret = Setenv__1(ret, k, v) + } + return ret +} + +// Setenv overwrites environments with specified (name, val) pair. +func Setenv__1(ret []string, name, val string) []string { + name += "=" + for i, e := range ret { + if strings.HasPrefix(e, name) { + ret[i] = name + val + return ret + } + } + return append(ret, name+val) +} From df26cfc4567aa6c4a9a28e325d62ffe956f6f560 Mon Sep 17 00:00:00 2001 From: xushiwei Date: Sun, 18 Feb 2024 22:23:35 +0800 Subject: [PATCH 2/6] gsh: exec --- gsh/classfile.go | 73 +++++++++++++++++++++++++++++++++++-------- gsh/env.go | 80 +++++++++++++++++++++++++++++++++++++++++++++--- gsh/gsh_test.go | 71 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 207 insertions(+), 17 deletions(-) create mode 100644 gsh/gsh_test.go diff --git a/gsh/classfile.go b/gsh/classfile.go index 9705533..5eef21d 100644 --- a/gsh/classfile.go +++ b/gsh/classfile.go @@ -18,9 +18,11 @@ package gsh import ( "bytes" + "errors" "io" "os" "os/exec" + "strings" ) const ( @@ -42,29 +44,76 @@ func (p *App) initApp() { p.ferr = os.Stderr } -// Exec executes a shell command with specified environs. -func (p *App) Exec__0(env map[string]string, name string, args ...string) error { - var cmdEnv []string - if env != nil { - cmdEnv = Setenv__0(os.Environ(), env) - } +func (p *App) execWith(env []string, name string, args ...string) error { cmd := exec.Command(name, args...) cmd.Stdin = p.fin cmd.Stdout = p.fout cmd.Stderr = p.ferr - cmd.Env = cmdEnv - p.err = cmd.Run() + cmd.Env = env + p.err = Sys.Run(cmd) return p.err } +// Gop_Exec executes a shell command. +func (p *App) Gop_Exec(name string, args ...string) error { + return p.execWith(nil, name, args...) +} + +// Exec executes a shell command with specified environs. +func (p *App) Exec__0(env map[string]string, name string, args ...string) error { + var cmdEnv []string + if env != nil { + cmdEnv = Setenv__0(Sys.Environ(), env) + } + return p.execWith(cmdEnv, name, args...) +} + // Exec executes a shell command. func (p *App) Exec__1(name string, args ...string) error { - return p.Exec__0(nil, name, args...) + return p.execWith(nil, name, args...) } -// Gop_Exec executes a shell command. -func (p *App) Gop_Exec(name string, args ...string) error { - return p.Exec__0(nil, name, args...) +// Exec executes a shell command line with $env variables support. +// - exec "GOP_GOCMD=tinygo gop run ." +// - exec "ls -l $HOME" +func (p *App) Exec__2(cmdline string) error { + var iCmd = -1 + var items = strings.Fields(cmdline) + var env []string + var initEnv = func() { + env = Sys.Environ() + if iCmd > 0 { + env = Setenv__2(env, items[:iCmd]) + } + } + var mapping = func(name string) string { + if env == nil { + initEnv() + } + return Getenv(env, name) + } + for i, e := range items { + pos := strings.IndexAny(e, "=$") + if pos >= 0 && e[pos] == '=' { + if strings.IndexByte(e[pos+1:], '$') >= 0 { + items[i] = Sys.ExpandEnv(e) + } + continue + } + if iCmd < 0 { + iCmd = i + } + if pos >= 0 { + items[i] = os.Expand(e, mapping) + } + } + if iCmd < 0 { + return errors.New("exec: no command") + } + if env == nil && iCmd > 0 { + initEnv() + } + return p.execWith(env, items[iCmd], items[iCmd+1:]...) } // LastErr returns error of last command execution. diff --git a/gsh/env.go b/gsh/env.go index 4774cfa..0e00cc9 100644 --- a/gsh/env.go +++ b/gsh/env.go @@ -17,25 +17,95 @@ package gsh import ( + "os" + "os/exec" "strings" ) +// ----------------------------------------------------------- + // Setenv overwrites environments with specified env. func Setenv__0(ret []string, env map[string]string) []string { for k, v := range env { - ret = Setenv__1(ret, k, v) + nameEq := k + "=" + ret = setenv(ret, nameEq+v, len(nameEq)) } return ret } // Setenv overwrites environments with specified (name, val) pair. func Setenv__1(ret []string, name, val string) []string { - name += "=" + nameEq := name + "=" + return setenv(ret, nameEq+val, len(nameEq)) +} + +func setenv(ret []string, pair string, idxVal int) []string { + nameEq := pair[:idxVal] for i, e := range ret { - if strings.HasPrefix(e, name) { - ret[i] = name + val + if strings.HasPrefix(e, nameEq) { + ret[i] = pair return ret } } - return append(ret, name+val) + return append(ret, pair) +} + +// Setenv overwrites environments with specified "name=val" pairs. +func Setenv__2(ret []string, env []string) []string { + for _, pair := range env { + pos := strings.IndexByte(pair, '=') + if pos > 0 { + ret = setenv(ret, pair, pos+1) + } + } + return ret } + +// Getenv retrieves the value of the environment variable named by the key. +// It returns the value, which will be empty if the variable is not present. +// To distinguish between an empty value and an unset value, use LookupEnv. +func Getenv(env []string, name string) string { + nameEq := name + "=" + for _, e := range env { + if strings.HasPrefix(e, nameEq) { + return e[len(nameEq):] + } + } + return "" +} + +// ----------------------------------------------------------- + +type OS interface { + // Environ returns a copy of strings representing the environment, + // in the form "key=value". + Environ() []string + + // ExpandEnv replaces ${var} or $var in the string according to the values + // of the current environment variables. References to undefined + // variables are replaced by the empty string. + ExpandEnv(s string) string + + // Run starts the specified command and waits for it to complete. + Run(c *exec.Cmd) error +} + +// ----------------------------------------------------------- + +type defaultOS struct{} + +func (p defaultOS) Environ() []string { + return os.Environ() +} + +func (p defaultOS) ExpandEnv(s string) string { + return os.ExpandEnv(s) +} + +func (p defaultOS) Run(c *exec.Cmd) error { + return c.Run() +} + +var Sys OS = defaultOS{} + +// ----------------------------------------------------------- diff --git a/gsh/gsh_test.go b/gsh/gsh_test.go new file mode 100644 index 0000000..bf4ac8f --- /dev/null +++ b/gsh/gsh_test.go @@ -0,0 +1,71 @@ +/* + Copyright 2024 Qiniu Limited (qiniu.com) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package gsh + +import ( + "io" + "os" + "os/exec" + "testing" +) + +type mockOS struct{} + +func (p mockOS) Environ() []string { + return mockEnv +} + +func (p mockOS) ExpandEnv(s string) string { + return os.Expand(s, func(name string) string { + return Getenv(mockEnv, name) + }) +} + +func (p mockOS) Run(c *exec.Cmd) error { + if mockRunOut != "" { + io.WriteString(c.Stdout, mockRunOut) + } + return mockRunErr +} + +var ( + mockEnv []string + mockRunOut string + mockRunErr error +) + +func init() { + Sys = mockOS{} +} + +// ----------------------------------------------------------- + +func TestBasic(t *testing.T) { + var app App + app.initApp() + err := app.Gop_Exec("ls", "-l") + check(t, err) +} + +func check(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatal(err) + } +} + +// ----------------------------------------------------------- From 07757b4fb54aa75f2b30818d7cdbf24692686a24 Mon Sep 17 00:00:00 2001 From: xushiwei Date: Sun, 18 Feb 2024 22:35:34 +0800 Subject: [PATCH 3/6] TestExecWithEnv --- gsh/gsh_test.go | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/gsh/gsh_test.go b/gsh/gsh_test.go index bf4ac8f..7567fc3 100644 --- a/gsh/gsh_test.go +++ b/gsh/gsh_test.go @@ -17,7 +17,7 @@ package gsh import ( - "io" + "fmt" "os" "os/exec" "testing" @@ -36,24 +36,33 @@ func (p mockOS) ExpandEnv(s string) string { } func (p mockOS) Run(c *exec.Cmd) error { - if mockRunOut != "" { - io.WriteString(c.Stdout, mockRunOut) + if mockEcho { + fmt.Fprintln(c.Stdout, c.Env, c.Args) } return mockRunErr } var ( - mockEnv []string - mockRunOut string + mockEnv = []string{"FOO=foo", "BAR=bar"} mockRunErr error + mockEcho bool ) func init() { Sys = mockOS{} } +func capout(app *App, doSth func()) (ret string, err error) { + mockEcho = true + ret, err = app.Capout(doSth) + mockEcho = false + return +} + // ----------------------------------------------------------- +type M map[string]string + func TestBasic(t *testing.T) { var app App app.initApp() @@ -61,6 +70,18 @@ func TestBasic(t *testing.T) { check(t, err) } +func TestExecWithEnv(t *testing.T) { + var app App + app.initApp() + capout(&app, func() { + err := app.Exec__0(M{"FOO": "123"}, "./app", "$FOO") + check(t, err) + }) + if v := app.Output(); v != "[FOO=123 BAR=bar] [./app $FOO]\n" { + t.Fatal("TestExecWithEnv:", v) + } +} + func check(t *testing.T, err error) { t.Helper() if err != nil { From 959355a14aeadf031cd8e9c7ae43c62d09e7c8f4 Mon Sep 17 00:00:00 2001 From: xushiwei Date: Sun, 18 Feb 2024 23:00:58 +0800 Subject: [PATCH 4/6] TestExecSh, TestExitCode --- gsh/gsh_test.go | 85 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/gsh/gsh_test.go b/gsh/gsh_test.go index 7567fc3..fae6d9c 100644 --- a/gsh/gsh_test.go +++ b/gsh/gsh_test.go @@ -17,6 +17,7 @@ package gsh import ( + "errors" "fmt" "os" "os/exec" @@ -59,6 +60,12 @@ func capout(app *App, doSth func()) (ret string, err error) { return } +func lasterr(app *App, err error) { + mockRunErr = err + app.Gop_Exec("ls") + mockRunErr = nil +} + // ----------------------------------------------------------- type M map[string]string @@ -68,6 +75,8 @@ func TestBasic(t *testing.T) { app.initApp() err := app.Gop_Exec("ls", "-l") check(t, err) + err = app.Exec__1("ls", "-l") + check(t, err) } func TestExecWithEnv(t *testing.T) { @@ -82,6 +91,75 @@ func TestExecWithEnv(t *testing.T) { } } +func TestExecSh(t *testing.T) { + var app App + app.initApp() + capout(&app, func() { + err := app.Exec__2("FOO=123 ./app $BAR") + check(t, err) + }) + if v := app.Output(); v != "[FOO=123 BAR=bar] [./app bar]\n" { + t.Fatal("TestExecSh:", v) + } +} + +func TestExecSh2(t *testing.T) { + var app App + app.initApp() + capout(&app, func() { + err := app.Exec__2("FOO=$BAR ./app $FOO") + check(t, err) + }) + if v := app.Output(); v != "[FOO=bar BAR=bar] [./app bar]\n" { + t.Fatal("TestExecSh2:", v) + } +} + +func TestExecSh3(t *testing.T) { + var app App + app.initApp() + err := app.Exec__2("FOO=$BAR X=1") + checkErr(t, err, "exec: no command") +} + +func TestExecSh4(t *testing.T) { + var app App + app.initApp() + capout(&app, func() { + err := app.Exec__2("FOO=$BAR X=1 ./app") + check(t, err) + }) + if v := app.Output(); v != "[FOO=bar BAR=bar X=1] [./app]\n" { + t.Fatal("TestExecSh4:", v) + } +} + +func TestExitCode(t *testing.T) { + var app App + app.initApp() + lasterr(&app, nil) + check(t, app.LastErr()) + if v := app.ExitCode(); v != 0 { + t.Fatal("ExitCode:", v) + } + lasterr(&app, errors.New("exec: no command")) + if v := app.ExitCode(); v != 127 { + t.Fatal("ExitCode:", v) + } + lasterr(&app, errors.New("exec: not started")) + if v := app.ExitCode(); v != 126 { + t.Fatal("ExitCode:", v) + } + lasterr(&app, errors.New("unknown")) + if v := app.ExitCode(); v != 254 { + t.Fatal("ExitCode:", v) + } + lasterr(&app, new(exec.ExitError)) + if v := app.ExitCode(); v != -1 { + t.Fatal("ExitCode:", v) + } +} + func check(t *testing.T, err error) { t.Helper() if err != nil { @@ -89,4 +167,11 @@ func check(t *testing.T, err error) { } } +func checkErr(t *testing.T, err error, msg string) { + t.Helper() + if err == nil || err.Error() != msg { + t.Fatal(err) + } +} + // ----------------------------------------------------------- From dac20c2c66936f48e98d50c029edbc32b5df77f2 Mon Sep 17 00:00:00 2001 From: xushiwei Date: Sun, 18 Feb 2024 23:03:56 +0800 Subject: [PATCH 5/6] TestMainEntry --- gsh/gsh_test.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/gsh/gsh_test.go b/gsh/gsh_test.go index fae6d9c..54f7f51 100644 --- a/gsh/gsh_test.go +++ b/gsh/gsh_test.go @@ -70,15 +70,23 @@ func lasterr(app *App, err error) { type M map[string]string -func TestBasic(t *testing.T) { - var app App - app.initApp() - err := app.Gop_Exec("ls", "-l") +type myApp struct { + App + t *testing.T +} + +func (p *myApp) MainEntry() { + t := p.t + err := p.Gop_Exec("ls", "-l") check(t, err) - err = app.Exec__1("ls", "-l") + err = p.Exec__1("ls", "-l") check(t, err) } +func TestMainEntry(t *testing.T) { + Gopt_App_Main(&myApp{t: t}) +} + func TestExecWithEnv(t *testing.T) { var app App app.initApp() From 9d6338e58cb8a87bc9a723aa2f33b5db327f102f Mon Sep 17 00:00:00 2001 From: xushiwei Date: Sun, 18 Feb 2024 23:08:22 +0800 Subject: [PATCH 6/6] TestEnv --- gsh/gsh_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/gsh/gsh_test.go b/gsh/gsh_test.go index 54f7f51..9404319 100644 --- a/gsh/gsh_test.go +++ b/gsh/gsh_test.go @@ -87,6 +87,22 @@ func TestMainEntry(t *testing.T) { Gopt_App_Main(&myApp{t: t}) } +func TestOS(t *testing.T) { + var sys defaultOS + sys.Environ() + sys.ExpandEnv("foo") + sys.Run(new(exec.Cmd)) +} + +func TestEnv(t *testing.T) { + if Getenv(nil, "foo") != "" { + t.Fatal("TestEnv: Getenv") + } + if ret := Setenv__1(nil, "k", "v"); len(ret) != 1 || ret[0] != "k=v" { + t.Fatal("TestEnv Setenv:", ret) + } +} + func TestExecWithEnv(t *testing.T) { var app App app.initApp()