├── examples ├── pipeline │ ├── cmd │ │ └── main.go │ ├── pipeline_test.go │ └── pipeline.go ├── gotest │ ├── cmd │ │ └── main.go │ ├── gotest.go │ └── gotest_test.go ├── ginkgotest │ ├── cmd │ │ └── main.go │ ├── ginkgotest.go │ └── ginkgotest_test.go ├── go.mod ├── arctest │ ├── cmd │ │ └── main.go │ ├── arctest_acc_test.go │ ├── arctest_test.go │ └── arctest.go ├── commands │ ├── cmd │ │ └── main.go │ └── commands.go ├── getting-started │ └── main.go └── go.sum ├── .gitignore ├── grpc ├── go.mod ├── remote │ ├── remote.proto │ ├── remote_grpc.pb.go │ └── remote.pb.go ├── client │ └── client.go ├── server │ └── server.go └── go.sum ├── context.go ├── go.mod ├── context ├── variables.go ├── context_test.go └── context.go ├── run ├── main_test.go └── main.go ├── project └── build.go ├── pipe.go ├── Makefile ├── flags_test.go ├── struct_test.go ├── data ├── data.go └── x509 │ ├── server_cert.pem │ ├── ca_cert.pem │ └── server_key.pem ├── args_test.go ├── go.sum ├── goshtest └── goshtest.go ├── call.go ├── examples_test.go ├── LICENSE ├── struct.go ├── app.go └── README.md /examples/pipeline/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | sessions 2 | .bin 3 | .cmds 4 | diags.out 5 | /getting-started 6 | .e2e 7 | bashenv.* 8 | -------------------------------------------------------------------------------- /examples/gotest/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/mumoshu/gosh/examples/gotest" 7 | ) 8 | 9 | func main() { 10 | gotest.MustExec(os.Args) 11 | } 12 | -------------------------------------------------------------------------------- /examples/ginkgotest/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/mumoshu/gosh/examples/ginkgotest" 7 | ) 8 | 9 | func main() { 10 | ginkgotest.MustExec(os.Args) 11 | } 12 | -------------------------------------------------------------------------------- /grpc/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mumoshu/gosh/grpc 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/mumoshu/gosh v0.0.0-20210615091137-1c3c901ef3ec 7 | google.golang.org/grpc v1.39.0 8 | google.golang.org/protobuf v1.27.1 9 | ) 10 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package gosh 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type FunID string 8 | 9 | func NewFunID(f Dependency) FunID { 10 | bs, err := json.Marshal(f) 11 | if err != nil { 12 | panic(err) 13 | } 14 | 15 | return FunID(bs) 16 | } 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mumoshu/gosh 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 // indirect 7 | github.com/stretchr/testify v1.7.0 8 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a 9 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mumoshu/gosh/examples 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/mumoshu/gosh v0.0.0-20210808060649-5f5899dc0db4 7 | github.com/onsi/ginkgo v1.16.2 8 | github.com/onsi/gomega v1.13.0 9 | github.com/stretchr/testify v1.7.0 10 | ) 11 | 12 | replace github.com/mumoshu/gosh => ../ 13 | -------------------------------------------------------------------------------- /examples/arctest/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/mumoshu/gosh/examples/arctest" 8 | ) 9 | 10 | func main() { 11 | var args []interface{} 12 | for _, a := range os.Args[1:] { 13 | args = append(args, a) 14 | } 15 | if err := arctest.New().Run(args...); err != nil { 16 | log.Fatal(err) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/commands/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/mumoshu/gosh/examples/commands" 8 | ) 9 | 10 | func main() { 11 | var args []interface{} 12 | for _, a := range os.Args[1:] { 13 | args = append(args, a) 14 | } 15 | if err := commands.New().Run(args...); err != nil { 16 | log.Fatal(err) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /grpc/remote/remote.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/mumoshu/gosh/remote/remote"; 4 | option java_multiple_files = true; 5 | option java_package = "io.grpc.examples.remote"; 6 | option java_outer_classname = "RemoteProto"; 7 | 8 | package remote; 9 | 10 | service Remote { 11 | rpc ShellSession(stream Message) returns (stream Message) {} 12 | } 13 | 14 | message Message { 15 | string message = 1; 16 | } 17 | -------------------------------------------------------------------------------- /context/variables.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import "sync" 4 | 5 | type Variables struct { 6 | sync.Mutex 7 | 8 | vars map[string]interface{} 9 | } 10 | 11 | func (c *Variables) Set(k string, v interface{}) { 12 | c.Lock() 13 | defer c.Unlock() 14 | 15 | if c.vars == nil { 16 | c.vars = map[string]interface{}{} 17 | } 18 | 19 | c.vars[k] = v 20 | } 21 | 22 | func (c *Variables) Get(k string) interface{} { 23 | return c.vars[k] 24 | } 25 | -------------------------------------------------------------------------------- /examples/gotest/gotest.go: -------------------------------------------------------------------------------- 1 | package gotest 2 | 3 | import ( 4 | "github.com/mumoshu/gosh" 5 | "github.com/mumoshu/gosh/context" 6 | ) 7 | 8 | func New() *gosh.Shell { 9 | sh := &gosh.Shell{} 10 | 11 | sh.Export("hello", func(ctx context.Context, target string) { 12 | context.Stdout(ctx).Write([]byte("hello " + target + "\n")) 13 | }) 14 | 15 | return sh 16 | } 17 | 18 | func MustExec(osArgs []string) { 19 | New().MustExec(osArgs) 20 | } 21 | -------------------------------------------------------------------------------- /examples/ginkgotest/ginkgotest.go: -------------------------------------------------------------------------------- 1 | package ginkgotest 2 | 3 | import ( 4 | "github.com/mumoshu/gosh" 5 | "github.com/mumoshu/gosh/context" 6 | ) 7 | 8 | func New() *gosh.Shell { 9 | sh := &gosh.Shell{} 10 | 11 | sh.Export("hello", func(ctx context.Context, target string) { 12 | context.Stdout(ctx).Write([]byte("hello " + target + "\n")) 13 | }) 14 | 15 | return sh 16 | } 17 | 18 | func MustExec(osArgs []string) { 19 | New().MustExec(osArgs) 20 | } 21 | -------------------------------------------------------------------------------- /context/context_test.go: -------------------------------------------------------------------------------- 1 | package context_test 2 | 3 | import ( 4 | "github.com/mumoshu/gosh/context" 5 | 6 | gocontext "context" 7 | "testing" 8 | ) 9 | 10 | func TestGoContextInterop(t *testing.T) { 11 | f := func(_ context.Context) { 12 | 13 | } 14 | 15 | f(gocontext.TODO()) 16 | f(context.WithError(gocontext.TODO(), nil)) 17 | 18 | g := func(_ gocontext.Context) { 19 | 20 | } 21 | 22 | g(context.TODO()) 23 | f(context.WithError(context.TODO(), nil)) 24 | } 25 | -------------------------------------------------------------------------------- /examples/getting-started/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/mumoshu/gosh" 7 | "github.com/mumoshu/gosh/context" 8 | ) 9 | 10 | func main() { 11 | sh := &gosh.Shell{} 12 | 13 | sh.Export("hello", func(ctx context.Context, target string) { 14 | // sh.Diagf("My own debug message someData=%s someNumber=%d", "foobar", 123) 15 | 16 | context.Stdout(ctx).Write([]byte("hello " + target + "\n")) 17 | }) 18 | 19 | sh.MustExec(os.Args) 20 | } 21 | -------------------------------------------------------------------------------- /run/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/mumoshu/gosh" 8 | "github.com/mumoshu/gosh/goshtest" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestMain(t *testing.T) { 13 | sh := New() 14 | 15 | goshtest.Run(t, sh, func() { 16 | t.Run("foo", func(t *testing.T) { 17 | var stdout bytes.Buffer 18 | 19 | err := sh.Run(t, "foo", "a", "b", gosh.WriteStdout(&stdout)) 20 | 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | assert.Equal(t, "running setup1\ndir=aa\na b\na b\n", stdout.String()) 26 | }) 27 | }) 28 | 29 | } 30 | -------------------------------------------------------------------------------- /project/build.go: -------------------------------------------------------------------------------- 1 | //+build project 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "os" 8 | 9 | . "github.com/mumoshu/gosh" 10 | ) 11 | 12 | // dsl 13 | var ( 14 | sh = &Shell{} 15 | Task = sh.Export 16 | Run = sh.Run 17 | MustExec = sh.MustExec 18 | ) 19 | 20 | func init() { 21 | Task("all", Dep("build"), Dep("test"), func() { 22 | 23 | }) 24 | 25 | Task("build", func(ctx context.Context) { 26 | var examples = []string{ 27 | "arctest", 28 | "commands", 29 | "getting-started", 30 | "ginkgotest", 31 | "gotest", 32 | "pipeline", 33 | } 34 | 35 | const dir = "examples" 36 | 37 | for _, name := range examples { 38 | Run(ctx, "go", "build", "-o", "bin/"+name, "./"+name, Dir(dir)) 39 | } 40 | }) 41 | 42 | Task("test", func() { 43 | Run("go", "test", "./...") 44 | Run("go", "test", "./...", Dir("examples")) 45 | }) 46 | } 47 | 48 | func main() { 49 | MustExec(os.Args) 50 | } 51 | -------------------------------------------------------------------------------- /pipe.go: -------------------------------------------------------------------------------- 1 | package gosh 2 | 3 | import ( 4 | "io" 5 | "log" 6 | 7 | "github.com/mumoshu/gosh/context" 8 | ) 9 | 10 | func (sh *Shell) Pipe(ctx context.Context, vars ...interface{}) (context.Context, <-chan error) { 11 | a, b, close := sh.PipeFromContext(ctx) 12 | 13 | err := make(chan error) 14 | 15 | go func() { 16 | vars = append([]interface{}{a}, vars...) 17 | e := sh.Run(vars...) 18 | close() 19 | err <- e 20 | }() 21 | 22 | return b, err 23 | } 24 | 25 | func (sh *Shell) PipeFromContext(ctx context.Context) (context.Context, context.Context, func()) { 26 | a, b := ctx, ctx 27 | 28 | r, w := io.Pipe() 29 | 30 | a = context.WithStdin(a, context.Stdin(ctx)) 31 | a = context.WithStdout(a, w) 32 | a = context.WithStderr(a, context.Stderr(ctx)) 33 | 34 | b = context.WithStdin(b, r) 35 | b = context.WithStdout(b, context.Stdout(ctx)) 36 | b = context.WithStderr(b, context.Stderr(ctx)) 37 | 38 | return a, b, func() { 39 | if err := w.Close(); err != nil { 40 | log.Fatal(err) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/pipeline/pipeline_test.go: -------------------------------------------------------------------------------- 1 | package pipeline_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "testing" 7 | 8 | "github.com/mumoshu/gosh" 9 | "github.com/mumoshu/gosh/examples/pipeline" 10 | "github.com/mumoshu/gosh/goshtest" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestPipeline(t *testing.T) { 15 | gotest := pipeline.New() 16 | 17 | goshtest.Run(t, gotest, func() { 18 | var stdout bytes.Buffer 19 | 20 | if err := gotest.Run(t, "ctx3", gosh.WriteStdout(&stdout)); err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | assert.Equal(t, "footest\n", stdout.String()) 25 | }) 26 | } 27 | 28 | func TestCancel(t *testing.T) { 29 | gotest := pipeline.New() 30 | 31 | goshtest.Run(t, gotest, func() { 32 | var stdout bytes.Buffer 33 | 34 | ctx, cancel := context.WithCancel(context.Background()) 35 | cancel() 36 | 37 | err := gotest.Run(t, ctx, "ctx3", gosh.WriteStdout(&stdout)) 38 | if err == nil { 39 | t.Fatal("Missing error") 40 | } 41 | 42 | assert.Equal(t, "context canceled", err.Error()) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /examples/commands/commands.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/mumoshu/gosh" 11 | "github.com/mumoshu/gosh/context" 12 | ) 13 | 14 | func New() *gosh.Shell { 15 | sh := &gosh.Shell{} 16 | 17 | sh.Export("gogrep", func(ctx context.Context, pattern string) { 18 | scanner := bufio.NewScanner(context.Stdin(ctx)) 19 | 20 | for scanner.Scan() { 21 | line := scanner.Text() 22 | if strings.Contains(line, pattern) { 23 | fmt.Fprint(os.Stdout, line+"\n") 24 | } 25 | } 26 | }) 27 | 28 | sh.Export("gocat", func(ctx context.Context, file ...string) error { 29 | var in io.Reader 30 | 31 | if len(file) == 1 { 32 | f, err := os.Open(file[0]) 33 | if err != nil { 34 | return err 35 | } 36 | in = f 37 | } else if len(file) == 0 { 38 | in = context.Stdin(ctx) 39 | } else { 40 | return fmt.Errorf("unexpected length of args %d: %v", len(file), file) 41 | } 42 | 43 | scanner := bufio.NewScanner(in) 44 | 45 | for scanner.Scan() { 46 | line := scanner.Text() 47 | fmt.Fprint(os.Stdout, line+"\n") 48 | } 49 | 50 | return nil 51 | }) 52 | 53 | return sh 54 | } 55 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run-server: gen 2 | go run ./server 3 | 4 | run-client: gen 5 | go run ./client 6 | 7 | gen: deps 8 | PATH=$(PATH):.bin protoc --go_out=. --go_opt=paths=source_relative \ 9 | --go-grpc_out=. --go-grpc_opt=paths=source_relative \ 10 | remote/remote.proto 11 | 12 | deps: protoc protoc-gen-go protoc-gen-go-gprc 13 | 14 | protoc: .bin/protoc 15 | 16 | .bin/protoc: 17 | @{ \ 18 | ARCHIVE=protoc-3.17.0-linux-x86_64.zip ;\ 19 | curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v3.17.0/$$ARCHIVE ;\ 20 | mkdir -p .bin ;\ 21 | unzip -j -o $$ARCHIVE -d .bin bin/protoc ;\ 22 | rm -f $$ARCHIVE ;\ 23 | } 24 | 25 | protoc-gen-go: .bin/protoc-gen-go 26 | 27 | .bin/protoc-gen-go: 28 | GOBIN=$(abspath .bin) go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26 29 | 30 | protoc-gen-go-gprc: .bin/protoc-gen-go-grpc 31 | 32 | .bin/protoc-gen-go-grpc: 33 | GOBIN=$(abspath .bin) go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1 34 | 35 | .PHONY: all 36 | all: 37 | go run -tags=project ./project all 38 | 39 | .PHONY: build 40 | build: 41 | go run -tags=project ./project build 42 | 43 | .PHONY: test 44 | test: 45 | go run -tags=project ./project test 46 | -------------------------------------------------------------------------------- /examples/pipeline/pipeline.go: -------------------------------------------------------------------------------- 1 | package pipeline 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mumoshu/gosh" 7 | "github.com/mumoshu/gosh/context" 8 | ) 9 | 10 | func New() *gosh.Shell { 11 | sh := &gosh.Shell{} 12 | 13 | sh.Export("ctx3", func(ctx context.Context) error { 14 | b, lsErr := sh.Pipe(ctx, "echo", "footest") 15 | 16 | grepErr := sh.GoRun(b, "grep", "test", gosh.WriteStdout(context.Stdout(ctx))) 17 | 18 | var count int 19 | for { 20 | fmt.Fprintf(context.Stderr(ctx), "x count=%d\n", count) 21 | select { 22 | case err := <-lsErr: 23 | if err != nil { 24 | fmt.Fprintf(context.Stderr(ctx), "lserr %v\n", err) 25 | return err 26 | } 27 | fmt.Fprintf(context.Stderr(ctx), "ls completed\n") 28 | 29 | count++ 30 | case err := <-grepErr: 31 | if err != nil { 32 | fmt.Fprintf(context.Stderr(ctx), "greperr\n") 33 | return err 34 | } 35 | fmt.Fprintf(context.Stderr(ctx), "grep completed.\n") 36 | count++ 37 | } 38 | fmt.Fprintf(context.Stderr(ctx), "selected count=%d\n", count) 39 | if count == 2 { 40 | break 41 | } 42 | } 43 | 44 | fmt.Fprintf(context.Stderr(ctx), "exiting\n") 45 | 46 | return nil 47 | }) 48 | 49 | return sh 50 | } 51 | -------------------------------------------------------------------------------- /examples/gotest/gotest_test.go: -------------------------------------------------------------------------------- 1 | package gotest_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/mumoshu/gosh" 8 | "github.com/mumoshu/gosh/examples/gotest" 9 | "github.com/mumoshu/gosh/goshtest" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestUnitSmoke(t *testing.T) { 14 | gotest := gotest.New() 15 | 16 | goshtest.Run(t, gotest, func() { 17 | if err := gotest.Run(t, "hello", "world"); err != nil { 18 | t.Fatal(err) 19 | } 20 | }) 21 | } 22 | 23 | func TestUnit(t *testing.T) { 24 | gotest := gotest.New() 25 | 26 | goshtest.Run(t, gotest, func() { 27 | var stdout bytes.Buffer 28 | 29 | if err := gotest.Run(t, "hello", "world", gosh.WriteStdout(&stdout)); err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | assert.Equal(t, "hello world\n", stdout.String()) 34 | }) 35 | } 36 | 37 | func TestIntegration(t *testing.T) { 38 | sh := gotest.New() 39 | 40 | goshtest.Run(t, sh, func() { 41 | var stdout bytes.Buffer 42 | 43 | err := sh.Run(t, "bash", "-c", "for ((i=0;i<3;i++)); do hello world; done", gosh.WriteStdout(&stdout)) 44 | 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | assert.Equal(t, "hello world\nhello world\nhello world\n", stdout.String()) 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /flags_test.go: -------------------------------------------------------------------------------- 1 | package gosh_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/mumoshu/gosh" 11 | "github.com/mumoshu/gosh/context" 12 | "github.com/mumoshu/gosh/goshtest" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestFlags(t *testing.T) { 17 | sh := &gosh.Shell{} 18 | 19 | type Opts struct { 20 | UpperCase bool `flag:"upper-case"` 21 | } 22 | 23 | sh.Export("hello", func(ctx context.Context, a string, opts Opts) { 24 | a = "hello " + a 25 | if opts.UpperCase { 26 | a = strings.ToUpper(a) 27 | } 28 | fmt.Fprintf(context.Stdout(ctx), "%s\n", a) 29 | }) 30 | 31 | goshtest.Run(t, sh, func() { 32 | t.Run("direct", func(t *testing.T) { 33 | fmt.Fprintf(os.Stderr, "%v\n", os.Args) 34 | var stdout bytes.Buffer 35 | 36 | err := sh.Run(t, "hello", "world", "-upper-case", gosh.WriteStdout(&stdout)) 37 | 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | assert.Equal(t, "HELLO WORLD\n", stdout.String()) 43 | }) 44 | 45 | t.Run("flags", func(t *testing.T) { 46 | fmt.Fprintf(os.Stderr, "%v\n", os.Args) 47 | var stdout bytes.Buffer 48 | 49 | err := sh.Run(t, "hello", "world", Opts{UpperCase: true}, gosh.WriteStdout(&stdout)) 50 | 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | assert.Equal(t, "HELLO WORLD\n", stdout.String()) 56 | }) 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /struct_test.go: -------------------------------------------------------------------------------- 1 | package gosh 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestStruct(t *testing.T) { 11 | f := &structFieldsReflector{ 12 | TagToEnvName: defaultFilter, 13 | TagToUsage: defaultFilter, 14 | FieldToFlagName: defaultFilter, 15 | } 16 | 17 | type Opts struct { 18 | Foo string `flag:"foo"` 19 | Bar string `flag:"bar"` 20 | Num int `flag:"num"` 21 | M map[string]string `flag:"set"` 22 | 23 | // Note that you can't set private fields like this, 24 | // due to that go's reflection doesn't allow setting a value for a private field. 25 | foo string 26 | } 27 | 28 | t.Run("flags", func(t *testing.T) { 29 | var opts Opts 30 | 31 | err := f.SetStruct("teststruct", reflect.ValueOf(&opts), []interface{}{"-foo=FOO", "-bar", "BAR", "-set=a=A", "-set", "b=B"}) 32 | 33 | assert.NoError(t, err) 34 | 35 | assert.Equal(t, Opts{Foo: "FOO", Bar: "BAR", M: map[string]string{"a": "A", "b": "B"}}, opts) 36 | }) 37 | 38 | t.Run("direct", func(t *testing.T) { 39 | var opts Opts 40 | 41 | err := f.SetStruct("teststruct", reflect.ValueOf(&opts), []interface{}{Opts{Foo: "FOO2", Bar: "BAR2", M: map[string]string{"a": "A", "b": "B"}}}) 42 | 43 | assert.NoError(t, err) 44 | 45 | assert.Equal(t, Opts{Foo: "FOO2", Bar: "BAR2", M: map[string]string{"a": "A", "b": "B"}}, opts) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /data/data.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 gRPC authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | // Package data provides convenience routines to access files in the data 19 | // directory. 20 | package data 21 | 22 | import ( 23 | "path/filepath" 24 | "runtime" 25 | ) 26 | 27 | // basepath is the root directory of this package. 28 | var basepath string 29 | 30 | func init() { 31 | _, currentFile, _, _ := runtime.Caller(0) 32 | basepath = filepath.Dir(currentFile) 33 | } 34 | 35 | // Path returns the absolute path the given relative file or directory path, 36 | // relative to the google.golang.org/grpc/examples/data directory in the 37 | // user's GOPATH. If rel is already absolute, it is returned unmodified. 38 | func Path(rel string) string { 39 | if filepath.IsAbs(rel) { 40 | return rel 41 | } 42 | 43 | return filepath.Join(basepath, rel) 44 | } 45 | -------------------------------------------------------------------------------- /args_test.go: -------------------------------------------------------------------------------- 1 | package gosh_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/mumoshu/gosh" 10 | "github.com/mumoshu/gosh/context" 11 | "github.com/mumoshu/gosh/goshtest" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestArgs(t *testing.T) { 16 | sh := &gosh.Shell{} 17 | 18 | sh.Export("add", func(ctx context.Context, a, b int) { 19 | fmt.Fprintf(context.Stdout(ctx), "%d\n", a+b) 20 | }) 21 | 22 | sh.Export("join1", func(ctx context.Context, delim string, elems []string) { 23 | v := strings.Join(elems, delim) 24 | fmt.Fprintf(context.Stdout(ctx), "%s\n", v) 25 | }) 26 | 27 | sh.Export("join2", func(ctx context.Context, delim string, elems ...string) { 28 | v := strings.Join(elems, delim) 29 | fmt.Fprintf(context.Stdout(ctx), "%s\n", v) 30 | }) 31 | 32 | goshtest.Run(t, sh, func() { 33 | t.Run("add", func(t *testing.T) { 34 | var stdout bytes.Buffer 35 | 36 | err := sh.Run(t, "add", 1, 2, gosh.WriteStdout(&stdout)) 37 | 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | assert.Equal(t, "3\n", stdout.String()) 43 | }) 44 | 45 | t.Run("join1", func(t *testing.T) { 46 | var stdout bytes.Buffer 47 | 48 | err := sh.Run(t, "join1", ",", "A", "B", gosh.WriteStdout(&stdout)) 49 | 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | assert.Equal(t, "A,B\n", stdout.String()) 55 | }) 56 | 57 | t.Run("join2", func(t *testing.T) { 58 | var stdout bytes.Buffer 59 | 60 | err := sh.Run(t, "join2", ",", "A", "B", gosh.WriteStdout(&stdout)) 61 | 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | assert.Equal(t, "A,B\n", stdout.String()) 67 | }) 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /data/x509/server_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFeDCCA2CgAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwUDELMAkGA1UEBhMCVVMx 3 | CzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTALBgNVBAoMBGdSUEMxFzAVBgNV 4 | BAMMDnRlc3Qtc2VydmVyX2NhMB4XDTIwMDgwNDAxNTk1OFoXDTMwMDgwMjAxNTk1 5 | OFowTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTAL 6 | BgNVBAoMBGdSUEMxFTATBgNVBAMMDHRlc3Qtc2VydmVyMTCCAiIwDQYJKoZIhvcN 7 | AQEBBQADggIPADCCAgoCggIBAKonkszKvSg1IUvpfW3PAeDPLgLrXboOWJCXv3RD 8 | 5q6vf29+IBCaljSJmU6T7SplokUML5ZkY6adjX6awG+LH3tOMg9zvXpHuSPRpFUk 9 | 2oLFtaWuzJ+NC5HIM0wWDvdZ6KQsiPFbNxk2Rhkk+QKsiiptZy2yf/AbDY0sVieZ 10 | BJZJ+os+BdFIk7+XUgDutPdSAutTANhrGycYa4iYAfDGQApz3sndSSsM2KVc0w5F 11 | gW6w2UBC4ggc1ZaWdbVtkYo+0dCsrl1J7WUNsz8v8mjGsvm9eFuJjKFBiDhCF+xg 12 | 4Xzu1Wz7zV97994la/xMImQR4QDdky9IgKcJMVUGua6U0GE5lmt2wnd3aAI228Vm 13 | 6SnK7kKvnD8vRUyM9ByeRoMlrAuYb0AjnVBr/MTFbOaii6w2v3RjU0j6YFzp8+67 14 | ihOW9nkb1ayqSXD3T4QUD0p75Ne7/zz1r2amIh9pmSJlugLexVDpb86vXg9RnXjb 15 | Zn2HTEkXsL5eHUIlQzuhK+gdmj+MLGf/Yzp3fdaJsA0cJfMjj5Ubb2gR4VwzrHy9 16 | AD2Kjjzs06pTtpULChwpr9IBTLEsZfw/4uW4II4pfe6Rwn4bGHFifjx0+3svlsSo 17 | jdHcXEMHvdRPhWGUZ0rne+IK6Qxgb3OMZu7a04vV0RqvgovxM6hre3e0UzBJG45Y 18 | qlQjAgMBAAGjXjBcMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFFL5HUzehgKNfgdz 19 | 4nuw5fru5OTPMA4GA1UdDwEB/wQEAwIDqDAdBgNVHREEFjAUghIqLnRlc3QuZXhh 20 | bXBsZS5jb20wDQYJKoZIhvcNAQEFBQADggIBAHMPYTF4StfSx9869EoitlEi7Oz2 21 | YTOForDbsY9i0VnIamhIi9CpjekAGLo8SVojeAk7UV3ayiu0hEMAHJWbicgWTwWM 22 | JvZWWfrIk/2WYyBWWTa711DuW26cvtbSebFzXsovNeTqMICiTeYbvOAK826UdH/o 23 | OqNiHL+UO5xR1Xmqa2hKmLSl5J1n+zgm94l6SROzc9c5YDzn03U+8dlhoyXCwlTv 24 | JRprOD+lupccxcKj5Tfh9/G6PjKsgxW+DZ+rvQV5f/l7c4m/bBrgS8tru4t2Xip0 25 | NhQW4qHnL0wXdTjaOG/1liLppjcp7SsP+vKF4shUvp+P8NQuAswBp/QtqUse5EYl 26 | EUARWrjEpV4OHSKThkMackMg5E32keiOvQE6iICxtU+m2V+C3xXM3G2cGlDDx5Ob 27 | tan0c9fZXoygrN2mc94GPogfwFGxwivajvvJIs/bsB3RkcIuLbi2UB76Wwoq+ZvH 28 | 15xxNZI1rpaDhjEuqwbSGPMPVpFtF5VERgYQ9LaDgj7yorwSQ1YLY8R1y0vSiAR2 29 | 2YeOaBH1ZLPF9v9os1iK4TIC8XQfPv7ll2WdDwfbe2ux5GVbDBD4bPhP9s3F4a+f 30 | oPhikWsUY4eN5CfS76x6xL0L60TL1AlWLlwuubTxpvNhv3GSyxjfunjcGiXDml20 31 | 6S80qO4hepxzzjol 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /examples/ginkgotest/ginkgotest_test.go: -------------------------------------------------------------------------------- 1 | package ginkgotest_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/mumoshu/gosh" 8 | "github.com/mumoshu/gosh/examples/ginkgotest" 9 | "github.com/mumoshu/gosh/goshtest" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var app *gosh.Shell 15 | var tt *testing.T 16 | 17 | func TestAcc(t *testing.T) { 18 | app = ginkgotest.New() 19 | 20 | goshtest.Run(t, app, func() { 21 | tt = t 22 | RegisterFailHandler(Fail) 23 | RunSpecs(t, "Your App's Suite") 24 | }) 25 | } 26 | 27 | var _ = Describe("Your App", func() { 28 | var ( 29 | config struct { 30 | cmd string 31 | args []interface{} 32 | } 33 | 34 | err error 35 | stdout string 36 | ) 37 | 38 | JustBeforeEach(func() { 39 | // This doesn't work as then we have no way to "hook" into the test framework 40 | // for handling indirectly run commands. 41 | // 42 | // sh := ginkgotest.New() 43 | 44 | var stdoutBuf bytes.Buffer 45 | 46 | var args []interface{} 47 | 48 | args = append(args, tt) 49 | args = append(args, config.cmd) 50 | args = append(args, config.args...) 51 | args = append(args, gosh.WriteStdout(&stdoutBuf)) 52 | 53 | err = app.Run(args...) 54 | 55 | stdout = stdoutBuf.String() 56 | }) 57 | 58 | Describe("hello", func() { 59 | BeforeEach(func() { 60 | config.cmd = "hello" 61 | }) 62 | 63 | Context("world", func() { 64 | BeforeEach(func() { 65 | config.args = []interface{}{"world"} 66 | }) 67 | 68 | It("should output \"hello world\"", func() { 69 | Expect(stdout).To(Equal("hello world\n")) 70 | }) 71 | 72 | It("should not error", func() { 73 | Expect(err).ToNot(HaveOccurred()) 74 | }) 75 | }) 76 | 77 | Context("sekai", func() { 78 | BeforeEach(func() { 79 | config.args = []interface{}{"sekai"} 80 | }) 81 | 82 | It("should output \"hello sekai\"", func() { 83 | Expect(stdout).To(Equal("hello sekai\n")) 84 | }) 85 | 86 | It("should not error", func() { 87 | Expect(err).ToNot(HaveOccurred()) 88 | }) 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /examples/arctest/arctest_acc_test.go: -------------------------------------------------------------------------------- 1 | package arctest_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/mumoshu/gosh" 8 | "github.com/mumoshu/gosh/examples/arctest" 9 | "github.com/mumoshu/gosh/goshtest" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var tt *testing.T 15 | var arctestSh *gosh.Shell 16 | 17 | func TestAcc(t *testing.T) { 18 | arctestSh = arctest.New() 19 | 20 | goshtest.Run(t, arctestSh, func() { 21 | tt = t 22 | RegisterFailHandler(Fail) 23 | RunSpecs(t, "Books Suite") 24 | }) 25 | } 26 | 27 | var _ = Describe("arctest", func() { 28 | var ( 29 | config struct { 30 | cmd string 31 | args []interface{} 32 | } 33 | 34 | err error 35 | stdout string 36 | stderr string 37 | ) 38 | 39 | JustBeforeEach(func() { 40 | // This doesn't work as then we have no way to "hook" into the test framework 41 | // for handling indirectly run commands. 42 | // 43 | // sh := arctest.New() 44 | 45 | var stdoutBuf, stderrBuf bytes.Buffer 46 | 47 | var args []interface{} 48 | 49 | args = append(args, tt) 50 | args = append(args, config.cmd) 51 | args = append(args, config.args...) 52 | args = append(args, "-dry-run", "-test-id=abcdefg") 53 | args = append(args, gosh.WriteStdout(&stdoutBuf), gosh.WriteStderr(&stderrBuf)) 54 | 55 | err = arctestSh.Run(args...) 56 | 57 | stdout = stdoutBuf.String() 58 | stderr = stderrBuf.String() 59 | }) 60 | 61 | Describe("e2e", func() { 62 | BeforeEach(func() { 63 | config.cmd = "e2e" 64 | }) 65 | 66 | Context("default", func() { 67 | BeforeEach(func() { 68 | config.args = []interface{}{} 69 | }) 70 | 71 | It("should output \"hello world\"", func() { 72 | Expect(stdout).To(Equal("hello world\n")) 73 | }) 74 | 75 | It("should write \"hello world (stderr)\" to stderr", func() { 76 | Expect(stderr).To(Equal("Using workdir at .e2e/workabcdefg\nhello world (stderr)\nDeleting cluster \"workabcdefg\" ...\n")) 77 | }) 78 | 79 | It("should not error", func() { 80 | Expect(err).ToNot(HaveOccurred()) 81 | }) 82 | }) 83 | 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 8 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 9 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= 10 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 11 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 12 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 13 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= 14 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 15 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 16 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 17 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 18 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 22 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | -------------------------------------------------------------------------------- /data/x509/ca_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIF6jCCA9KgAwIBAgIJAKnJpgBC9CHNMA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNV 3 | BAYTAlVTMQswCQYDVQQIDAJDQTEMMAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBD 4 | MRcwFQYDVQQDDA50ZXN0LXNlcnZlcl9jYTAeFw0yMDA4MDQwMTU5NTdaFw0zMDA4 5 | MDIwMTU5NTdaMFAxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEMMAoGA1UEBwwD 6 | U1ZMMQ0wCwYDVQQKDARnUlBDMRcwFQYDVQQDDA50ZXN0LXNlcnZlcl9jYTCCAiIw 7 | DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMZFKSUi+PlQ6z/aTz1Jp9lqrFAY 8 | 38cEIzpxS9ktQiWvLoYICImXRFhCH/h+WjmiyV8zYHcbft63BTUwgXJFuE0cxsJY 9 | mqOUYL2wTD5PzgoN0B9KVgKyyi0SQ6WH9+D2ZvYAolHb1l6pYuxxk1bQL2OA80Cc 10 | K659UioynIQtJ52NRqGRDI2EYsC9XRuhfddnDu/RwBaiv3ix84R3VAqcgRyOeGwH 11 | cX2e+aX0m6ULnsiyPXG9y9wQi956CGGZimInV63S+sU3Mc6PuUt8rwFlmSXCZ/07 12 | D8No5ljNUo6Vt2BpAMQzSz+SU4PUFE7Vxbq4ypI+2ZbkI80YjDwF52/pMauqZFIP 13 | Kjw0b2yyWD/F4hLmR7Rx9d8EFWRLZm2VYSVMiQTwANpb+uL7+kH8UE3QF7tryH8K 14 | G65mMh18XiERgSAWgs5Z8j/B1W5bl17PVx2Ii1dYp0IquyAVjCIKRrFituvoXXZj 15 | FHHpb/aUDpW0SYrT5dmDhAAGFkYfMTFd4EOj6bWepZtRRjPeIHR9B2yx8U0tFSMf 16 | tuHCj95l2izJDUfKhVIkigpbRrElI2QqXAPIyIOqcdzlgtI6DIanCd/CwsfdyaEs 17 | 7AnW2mFWarbkxpw92RdGxYy6WXbdM+2EdY+cWKys06upINcnG2zvkCflAE39fg9F 18 | BVCJC71oO3laXnf7AgMBAAGjgcYwgcMwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E 19 | FgQUBuToaw2a+AV/vfbooJn3yzwA3lMwgYAGA1UdIwR5MHeAFAbk6GsNmvgFf732 20 | 6KCZ98s8AN5ToVSkUjBQMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExDDAKBgNV 21 | BAcMA1NWTDENMAsGA1UECgwEZ1JQQzEXMBUGA1UEAwwOdGVzdC1zZXJ2ZXJfY2GC 22 | CQCpyaYAQvQhzTAOBgNVHQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADggIBALUz 23 | P2SiZAXZDwCH8kzHbLqsqacSM81bUSuG153t3fhwZU8hzXgQqifFububLkrLaRCj 24 | VvtIS3XsbHmKYD1TBOOCZy5zE2KdpWYW47LmogBqUllKCSD099UHFB2YUepK9Zci 25 | oxYJMhNWIhkoJ/NJMp70A8PZtxUvZafeUQl6xueo1yPbfQubg0lG9Pp2xkmTypSv 26 | WJkpRyX8GSJYFoFFYdNcvICVw7E/Zg+PGXe8gjpAGWW8KxxaohPsdLid6f3KauJM 27 | UCi/WQECzIpNzxQDSqnGeoqbZp+2y6mhgECQ3mG/K75n0fX0aV88DNwTd1o0xOpv 28 | lHJo8VD9mvwnapbm/Bc7NWIzCjL8fo0IviRkmAuoz525eBy6NsUCf1f432auvNbg 29 | OUaGGrY6Kse9sF8Tsc8XMoT9AfGQaR8Ay7oJHjaCZccvuxpB2n//L1UAjMRPYd2y 30 | XAiSN2xz7WauUh4+v48lKbWa+dwn1G0pa6ZGB7IGBUbgva8Fi3iqVh3UZoz+0PFM 31 | qVLG2SzhfMTMHg0kF+rI4eOcEKc1j3A83DmTTPZDz3APn53weJLJhKzrgQiI1JRW 32 | boAJ4VFQF6zjxeecCIIiekH6saYKnol2yL6ksm0jyHoFejkrHWrzoRAwIhTf9avj 33 | G7QS5fiSQk4PXCX42J5aS/zISy85RT120bkBjV/P 34 | -----END CERTIFICATE----- 35 | -------------------------------------------------------------------------------- /context/context.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | ) 8 | 9 | type Context = context.Context 10 | 11 | var TODO = context.TODO 12 | var Background = context.Background 13 | var WithValue = context.WithValue 14 | 15 | type stdinKey struct{} 16 | type stdoutKey struct{} 17 | type stderrKey struct{} 18 | type errorKey struct{} 19 | type varsKey struct{} 20 | 21 | func WithStdin(ctx context.Context, in io.Reader) Context { 22 | return context.WithValue(ctx, stdinKey{}, in) 23 | } 24 | 25 | func Stdin(ctx context.Context) io.Reader { 26 | v := ctx.Value(stdinKey{}) 27 | if v == nil { 28 | return os.Stdin 29 | } 30 | 31 | return v.(io.Reader) 32 | } 33 | 34 | func WithStdout(ctx context.Context, out io.Writer) Context { 35 | return context.WithValue(ctx, stdoutKey{}, out) 36 | } 37 | 38 | func Stdout(ctx context.Context) io.Writer { 39 | v := ctx.Value(stdoutKey{}) 40 | if v == nil { 41 | return os.Stdout 42 | } 43 | 44 | return v.(io.Writer) 45 | } 46 | 47 | func WithStderr(ctx context.Context, out io.Writer) Context { 48 | return context.WithValue(ctx, stderrKey{}, out) 49 | } 50 | 51 | func Stderr(ctx context.Context) io.Writer { 52 | v := ctx.Value(stderrKey{}) 53 | if v == nil { 54 | return os.Stderr 55 | } 56 | 57 | return v.(io.Writer) 58 | } 59 | 60 | func WithError(ctx context.Context, err error) Context { 61 | return context.WithValue(ctx, errorKey{}, err) 62 | } 63 | 64 | func Error(ctx context.Context) error { 65 | v := ctx.Value(errorKey{}) 66 | if v == nil { 67 | return nil 68 | } 69 | 70 | return v.(error) 71 | } 72 | 73 | func WithVariables(ctx context.Context, vars map[string]interface{}) Context { 74 | return context.WithValue(ctx, varsKey{}, &Variables{vars: vars}) 75 | } 76 | 77 | func Get(ctx context.Context, key string) interface{} { 78 | vars := getVars(ctx) 79 | if vars == nil { 80 | return nil 81 | } 82 | 83 | return vars.Get(key) 84 | } 85 | 86 | func Set(ctx context.Context, key string, value interface{}) { 87 | vars := getVars(ctx) 88 | if vars == nil { 89 | return 90 | } 91 | 92 | vars.Set(key, value) 93 | } 94 | 95 | func getVars(ctx context.Context) *Variables { 96 | v := ctx.Value(varsKey{}) 97 | if v == nil { 98 | return nil 99 | } 100 | 101 | return v.(*Variables) 102 | } 103 | -------------------------------------------------------------------------------- /examples/arctest/arctest_test.go: -------------------------------------------------------------------------------- 1 | package arctest_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/mumoshu/gosh" 8 | "github.com/mumoshu/gosh/examples/arctest" 9 | "github.com/mumoshu/gosh/goshtest" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestUndefinedCommand(t *testing.T) { 14 | arctest := arctest.New() 15 | 16 | goshtest.Run(t, arctest, func() { 17 | if err := arctest.Run(t, "all"); err == nil { 18 | t.Fatal("expected error didnt occur") 19 | } 20 | }) 21 | } 22 | 23 | func TestE2E(t *testing.T) { 24 | if testing.Short() { 25 | t.Skip("skipped e2e") 26 | } 27 | 28 | arctest := arctest.New() 29 | 30 | goshtest.Run(t, arctest, func() { 31 | testenv := "foo" 32 | 33 | goshtest.Cleanup(t, func() { 34 | _ = arctest.Run(t, "clean-e2e", "--test-id", testenv) 35 | }) 36 | 37 | if err := arctest.Run(t, "e2e", "--skip-clean", "--test-id", testenv); err != nil { 38 | t.Fatalf("unexpected error: %v", err) 39 | } 40 | }) 41 | } 42 | 43 | // func TestBashEnv(t *testing.T) { 44 | // sh := arctest.New() 45 | 46 | // goshtest.Run(t, sh, func() { 47 | // fmt.Fprintf(os.Stderr, "%v\n", os.Args) 48 | // var stdout, stderr bytes.Buffer 49 | 50 | // err := sh.Run(t, "bash", "-c", "hello world", gosh.WriteStdout(&stdout), gosh.WriteStderr(&stderr)) 51 | 52 | // if err != nil { 53 | // t.Fatal(err) 54 | // } 55 | 56 | // assert.Equal(t, "hello world\n", stdout.String()) 57 | // assert.Equal(t, "hello world (stderr)\n", stderr.String()) 58 | // // assert.Equal(t, "", stderr.String()) 59 | // }) 60 | // } 61 | 62 | func TestReflectionFuncName(t *testing.T) { 63 | funOptionType := reflect.TypeOf(gosh.FunOption(func(fo *gosh.FunOptions) {})) 64 | 65 | dep := gosh.Dep("foo") 66 | depType := reflect.TypeOf(dep) 67 | 68 | v := depType.AssignableTo(funOptionType) 69 | if !v { 70 | t.Errorf("v=%v", v) 71 | } 72 | } 73 | 74 | func RetStr(m string) string { 75 | return "ret" + m 76 | } 77 | 78 | func RetStrMap(k, v string) map[string]string { 79 | return map[string]string{k: v} 80 | } 81 | 82 | func TestReflectCallToReturnStr(t *testing.T) { 83 | f := reflect.ValueOf(RetStr) 84 | 85 | rets := f.Call([]reflect.Value{reflect.ValueOf("foo")}) 86 | 87 | assert.Equal(t, rets[0].String(), "retfoo") 88 | } 89 | 90 | func TestReflectCallToReturnStrMap(t *testing.T) { 91 | f := reflect.ValueOf(RetStrMap) 92 | 93 | rets := f.Call([]reflect.Value{reflect.ValueOf("foo"), reflect.ValueOf("bar")}) 94 | 95 | assert.Equal(t, rets[0].MapIndex(reflect.ValueOf("foo")).String(), "bar") 96 | } 97 | -------------------------------------------------------------------------------- /run/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "github.com/mumoshu/gosh" 10 | . "github.com/mumoshu/gosh" 11 | "github.com/mumoshu/gosh/context" 12 | ) 13 | 14 | func New() *gosh.Shell { 15 | sh := &gosh.Shell{} 16 | 17 | sh.Export("setup1", func(ctx context.Context, s []string) { 18 | fmt.Fprintf(context.Stdout(ctx), "running setup1\n") 19 | }) 20 | 21 | sh.Export("setup2", func(ctx context.Context, s []string) { 22 | context.Set(ctx, "dir", s[0]) 23 | }) 24 | 25 | sh.Export("foo", func(ctx context.Context, s []string) { 26 | dir := context.Get(ctx, "dir").(string) 27 | 28 | fmt.Fprintf(context.Stdout(ctx), "dir="+dir+"\n") 29 | fmt.Fprintf(context.Stdout(ctx), strings.Join(s, " ")+"\n") 30 | fmt.Fprintf(context.Stdout(ctx), strings.Join(s, " ")+"\n") 31 | // fmt.Fprintf(os.Stdout, strings.Join(s, " ")+"\n") 32 | // fmt.Fprintf(os.Stdout, strings.Join(s, " ")+"\n") 33 | // fmt.Fprintf(os.Stdout, strings.Join(s, " ")) 34 | }, Dep("setup1"), Dep("setup2", "aa")) 35 | 36 | sh.Export("hello", func(sub string) { 37 | println("hello " + sub) 38 | }) 39 | 40 | sh.Export("ctx1", func(ctx context.Context, num int, b bool, args []string) { 41 | context.Stdout(ctx).Write([]byte(fmt.Sprintf("num=%v, b=%v, args=%v\n", num, b, args))) 42 | }) 43 | 44 | sh.Export("ctx2", func(ctx context.Context, num int, b bool, args ...string) { 45 | context.Stdout(ctx).Write([]byte(fmt.Sprintf("num=%v, b=%v, args=%v\n", num, b, args))) 46 | 47 | sh.Run(ctx, "hello", "world") 48 | sh.Run(ctx, "ls", "-lah") 49 | }) 50 | 51 | sh.Export("ctx3", func(ctx context.Context) error { 52 | b, lsErr := sh.Pipe(ctx, "ls", "-lah") 53 | 54 | grepErr := sh.GoRun(b, "grep", "test") 55 | 56 | var count int 57 | for { 58 | fmt.Fprintf(os.Stderr, "x count=%d\n", count) 59 | select { 60 | case err := <-lsErr: 61 | if err != nil { 62 | fmt.Fprintf(os.Stderr, "lserr %v\n", err) 63 | return err 64 | } 65 | fmt.Fprintf(os.Stderr, "ls\n") 66 | 67 | count++ 68 | case err := <-grepErr: 69 | if err != nil { 70 | fmt.Fprintf(os.Stderr, "greperr\n") 71 | return err 72 | } 73 | fmt.Fprintf(os.Stderr, "grep\n") 74 | count++ 75 | } 76 | fmt.Fprintf(os.Stderr, "selected count=%d\n", count) 77 | if count == 2 { 78 | break 79 | } 80 | } 81 | 82 | fmt.Fprintf(os.Stderr, "exiting\n") 83 | 84 | return fmt.Errorf("some error") 85 | }) 86 | 87 | sh.Export("ctx4", func(ctx context.Context) error { 88 | return sh.Run(ctx, Cmd("ls", "-lah"), Cmd("grep", "test")) 89 | }) 90 | 91 | sh.Export("ctx5", func(ctx context.Context) error { 92 | return sh.Run(ctx, Cmd("bash -c 'ls -lah | grep test'")) 93 | }) 94 | 95 | return sh 96 | } 97 | 98 | func main() { 99 | println(fmt.Sprintf("starting abc=%v", os.Args)) 100 | if err := New().Run(os.Args[1:]); err != nil { 101 | log.Fatal(err) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /data/x509/server_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKQIBAAKCAgEAqieSzMq9KDUhS+l9bc8B4M8uAutdug5YkJe/dEPmrq9/b34g 3 | EJqWNImZTpPtKmWiRQwvlmRjpp2NfprAb4sfe04yD3O9eke5I9GkVSTagsW1pa7M 4 | n40LkcgzTBYO91nopCyI8Vs3GTZGGST5AqyKKm1nLbJ/8BsNjSxWJ5kElkn6iz4F 5 | 0UiTv5dSAO6091IC61MA2GsbJxhriJgB8MZACnPeyd1JKwzYpVzTDkWBbrDZQELi 6 | CBzVlpZ1tW2Rij7R0KyuXUntZQ2zPy/yaMay+b14W4mMoUGIOEIX7GDhfO7VbPvN 7 | X3v33iVr/EwiZBHhAN2TL0iApwkxVQa5rpTQYTmWa3bCd3doAjbbxWbpKcruQq+c 8 | Py9FTIz0HJ5GgyWsC5hvQCOdUGv8xMVs5qKLrDa/dGNTSPpgXOnz7ruKE5b2eRvV 9 | rKpJcPdPhBQPSnvk17v/PPWvZqYiH2mZImW6At7FUOlvzq9eD1GdeNtmfYdMSRew 10 | vl4dQiVDO6Er6B2aP4wsZ/9jOnd91omwDRwl8yOPlRtvaBHhXDOsfL0APYqOPOzT 11 | qlO2lQsKHCmv0gFMsSxl/D/i5bggjil97pHCfhsYcWJ+PHT7ey+WxKiN0dxcQwe9 12 | 1E+FYZRnSud74grpDGBvc4xm7trTi9XRGq+Ci/EzqGt7d7RTMEkbjliqVCMCAwEA 13 | AQKCAgEAjU6UEVMFSBDnd/2OVtUlQCeOlIoWql8jmeEL9Gg3eTbx5AugYWmf+D2V 14 | fbZHrX/+BM2b74+rWkFZspyd14R4PpSv6jk6UASkcmS1zqfud8/tjIzgDli6FPVn 15 | 9HYVM8IM+9qoV5hi56M1D8iuq1PS4m081Kx6p1IwLN93JSdksdL6KQz3E9jsKp5m 16 | UbPrwcDv/7JM723zfMJA+40Rf32EzalwicAl9YSTnrC57g428VAY+88Pm6EmmAqX 17 | 8nXt+hs1b9EYdQziA5wfEgiljfIFzHVXMN3IVlrv35iz+XBzkqddw0ZSRkvTiz8U 18 | sNAhd22JqIhapVfWz+FIgM43Ag9ABUMNWoQlaT0+2KlhkL+cZ6J1nfpMTBEIatz0 19 | A/l4TGcvdDhREODrS5jrxwJNx/LMRENtFFnRzAPzX4RdkFvi8SOioAWRBvs1TZFo 20 | ZLq2bzDOzDjs+EPQVx0SmjZEiBRhI6nC8Way00IdQi3T546r6qTKfPmXgjl5/fVO 21 | J4adGVbEUnI/7+fqL2N82WVr+Le585EFP/6IL5FO++sAIGDqAOzEQhyRaLhmnz+D 22 | GboeS/Tac9XdymFbrEvEMB4EFS3nsZHTeahfiqVd/SuXFDTHZ6kiqXweuhfsP1uW 23 | 7tGlnqtn+3zmLO6XRENPVvmjn7DhU255yjiKFdUqkajcoOYyWPECggEBANuYk+sr 24 | UTScvJoh/VRHuqd9NkVVIoqfoTN61x6V1OuNNcmjMWsOIsH+n4SifLlUW6xCKaSK 25 | 8x8RJYfE9bnObv/NqM4DMhuaNd52bPKFi8IBbHSZpuRE/UEyJhMDpoto04H1GXx4 26 | 1S49tndiNxQOv1/VojB4BH7kapY0yp30drK1CrocGN+YOUddxI9lOQpgt2AyoXVk 27 | ehdyamK4uzQmkMyyGQljrV5EQbmyPCqZ1l/d0MJ9DixOBxnPDR9Ov9qrG4Dy6S/k 28 | cH8PythqHTGTdlXgsBJaWEl2PyQupo3OhfiCV+79B9uxPfKvk5CIMVbnYxKgu+ly 29 | RKSTSX+GHVgNwicCggEBAMZcwQIAA+I39sTRg/Vn/MxmUBAu3h2+oJcuZ3FQh4v5 30 | SL80BWEsooK9Oe4MzxyWkU+8FieFu5G6iXaSx8f3Wv6j90IzA3g6Xr9M5xBm5qUN 31 | IqzF+hUZuKAEMY1NcPlFTa2NlrkT8JdfQvJ+D5QrcBIMFmg9cKG5x9yD7MfHTJkf 32 | ztMDFOwP3n7ahKRBowfe7/unAEFf6hYFtYjV+bqMDmBFVmk2CIVtjFgO9BNBQ/LB 33 | zGcnwo2VigWBIjRDF5BgV0v+2g0PZGaxJ362RigZjzJojx3gYj6kaZYX8yb6ttGo 34 | RPGt1A9woz6m0G0fLLMlce1dpbBAna14UVY7AEVt56UCggEAVvii/Oz3CINbHyB/ 35 | GLYf8t3gdK03NPfr/FuWf4KQBYqz1txPYjsDARo7S2ifRTdn51186LIvgApmdtNH 36 | DwP3alClnpIdclktJKJ6m8LQi1HNBpEkTBwWwY9/DODRQT2PJ1VPdsDUja/baIT5 37 | k3QTz3zo85FVFnyYyky2QsDjkfup9/PQ1h2P8fftNW29naKYff0PfVMCF+80u0y2 38 | t/zeNHQE/nb/3unhrg4tTiIHiYhsedrVli6BGXOrms6xpYVHK1cJi/JJq8kxaWz9 39 | ivkAURrgISSu+sleUJI5XMiCvt3AveJxDk2wX0Gyi/eksuqJjoMiaV7cWOIMpfkT 40 | /h/U2QKCAQAFirvduXBiVpvvXccpCRG4CDe+bADKpfPIpYRAVzaiQ4GzzdlEoMGd 41 | k3nV28fBjbdbme6ohgT6ilKi3HD2dkO1j5Et6Uz0g/T3tUdTXvycqeRJHXLiOgi9 42 | d8CGqR456KTF74nBe/whzoiJS9pVkm0cI/hQSz8lVZJu58SqxDewo4HcxV5FRiA6 43 | PRKtoCPU6Xac+kp4iRx6JwiuXQQQIS+ZovZKFDdiuu/L2gcZrp4eXym9zA+UcxQb 44 | GUOCYEl9QCPQPLuM19w/Pj3TPXZyUlx81Q0Cka1NALzuc5bYhPKsot3iPrAJCmWV 45 | L4XtNozCKI6pSg+CABwnp4/mL9nPFsX9AoIBAQDHiDhG9jtBdgtAEog6oL2Z98qR 46 | u5+nONtLQ61I5R22eZYOgWfxnz08fTtpaHaVWNLNzF0ApyxjxD+zkFHcMJDUuHkR 47 | O0yxUbCaof7u8EFtq8P9ux4xjtCnZW+9da0Y07zBrcXTsHYnAOiqNbtvVYd6RPiW 48 | AaE61hgvj1c9/BQh2lUcroQx+yJI8uAAQrfYtXzm90rb6qk6rWy4li2ybMjB+LmP 49 | cIQIXIUzdwE5uhBnwIre74cIZRXFJBqFY01+mT8ShPUWJkpOe0Fojrkl633TUuNf 50 | 9thZ++Fjvs4s7alFH5Hc7Ulk4v/O1+owdjqERd8zlu7+568C9s50CGwFnH0d 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /goshtest/goshtest.go: -------------------------------------------------------------------------------- 1 | package goshtest 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/mumoshu/gosh" 12 | ) 13 | 14 | var TestEnvName = "GOSH_TEST_NAME" 15 | 16 | func Run(testCtx *testing.T, t *gosh.Shell, f func()) { 17 | testCtx.Helper() 18 | 19 | if os.Getenv(TestEnvName) != "" { 20 | var osArgs []string 21 | 22 | var i int 23 | var a string 24 | 25 | for i, a = range os.Args { 26 | if a == ":::" { 27 | break 28 | } 29 | } 30 | 31 | osArgs = os.Args[i+1:] 32 | 33 | var runArgs []interface{} 34 | for _, a := range osArgs { 35 | runArgs = append(runArgs, a) 36 | } 37 | var stdout bytes.Buffer 38 | var stderr bytes.Buffer 39 | origStdout := os.Stdout 40 | origStderr := os.Stderr 41 | 42 | tempDir := os.Getenv("ARCTEST_TEMPDIR") 43 | 44 | // Note that panics aren't redirected to this log file. 45 | // See https://github.com/golang/go/issues/325 46 | // 47 | // Also, from what I have observed, println aren't redirect to the log file, too. 48 | if tempDir == "" { 49 | tempDir = testCtx.TempDir() 50 | } 51 | 52 | logFile, err := ioutil.TempFile(tempDir, "stdoutandstderr.log") 53 | if err != nil { 54 | testCtx.Fatal(err) 55 | } 56 | 57 | os.Stdout = logFile 58 | os.Stderr = logFile 59 | 60 | if len(runArgs) == 0 { 61 | testCtx.Error("runArgs is zero length. This means that your test target script called the test binary without any args, which shoudln't happen.") 62 | } 63 | 64 | fmt.Fprintf(os.Stderr, "ARGS=%v\n", runArgs) 65 | err = t.Run(append(runArgs, gosh.WriteStdout(&stdout), gosh.WriteStderr(&stderr))...) 66 | if err != nil { 67 | testCtx.Error(fmt.Errorf("failed running %s: %v", strings.Join(osArgs, " "), err)) 68 | } 69 | 70 | fmt.Fprint(origStderr, stderr.String()) 71 | fmt.Fprint(origStdout, stdout.String()) 72 | 73 | // This requires that we omit `-test.paniconexit0` on recursively running gosh-provided command. 74 | if err != nil { 75 | os.Exit(1) 76 | } 77 | 78 | os.Exit(0) 79 | 80 | return 81 | } 82 | 83 | f() 84 | } 85 | 86 | // Cleanup is similar to t.Cleanup(), but it runs the cleanup function only when the whole test has succeded. 87 | // If the test as a whole failed, or go-test binary was instructed to run a specific subtest, the cleanup function isn't called, 88 | // so that you can iterate quicker. 89 | func Cleanup(t *testing.T, f func()) { 90 | t.Helper() 91 | 92 | t.Cleanup(func() { 93 | var run string 94 | for i := range os.Args { 95 | // `go test -run $RUN` results in `/tmp/path/to/some.test -test.run $RUN` being run, 96 | // and hence we check for -test.run 97 | if os.Args[i] == "-test.run" { 98 | runIdx := i + 1 99 | run = os.Args[runIdx] 100 | break 101 | } else if strings.HasPrefix(os.Args[i], "-test.run=") { 102 | split := strings.Split(os.Args[i], "-test.run=") 103 | run = split[1] 104 | } 105 | } 106 | 107 | if t.Failed() { 108 | return 109 | } 110 | 111 | // Do not delete the cluster so that we can accelerate interation on tests 112 | if run == "" || (run != "" && run != "^"+t.Name()+"$" && !strings.HasPrefix(run, "^"+t.Name()+"/")) { 113 | // This should be printed to the debug console for visibility 114 | t.Logf("Skipped stopping due to run being %q", run) 115 | return 116 | } 117 | 118 | f() 119 | }) 120 | 121 | } 122 | -------------------------------------------------------------------------------- /grpc/remote/remote_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package remote 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | ) 11 | 12 | // This is a compile-time assertion to ensure that this generated file 13 | // is compatible with the grpc package it is being compiled against. 14 | // Requires gRPC-Go v1.32.0 or later. 15 | const _ = grpc.SupportPackageIsVersion7 16 | 17 | // RemoteClient is the client API for Remote service. 18 | // 19 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 20 | type RemoteClient interface { 21 | ShellSession(ctx context.Context, opts ...grpc.CallOption) (Remote_ShellSessionClient, error) 22 | } 23 | 24 | type remoteClient struct { 25 | cc grpc.ClientConnInterface 26 | } 27 | 28 | func NewRemoteClient(cc grpc.ClientConnInterface) RemoteClient { 29 | return &remoteClient{cc} 30 | } 31 | 32 | func (c *remoteClient) ShellSession(ctx context.Context, opts ...grpc.CallOption) (Remote_ShellSessionClient, error) { 33 | stream, err := c.cc.NewStream(ctx, &Remote_ServiceDesc.Streams[0], "/remote.Remote/ShellSession", opts...) 34 | if err != nil { 35 | return nil, err 36 | } 37 | x := &remoteShellSessionClient{stream} 38 | return x, nil 39 | } 40 | 41 | type Remote_ShellSessionClient interface { 42 | Send(*Message) error 43 | Recv() (*Message, error) 44 | grpc.ClientStream 45 | } 46 | 47 | type remoteShellSessionClient struct { 48 | grpc.ClientStream 49 | } 50 | 51 | func (x *remoteShellSessionClient) Send(m *Message) error { 52 | return x.ClientStream.SendMsg(m) 53 | } 54 | 55 | func (x *remoteShellSessionClient) Recv() (*Message, error) { 56 | m := new(Message) 57 | if err := x.ClientStream.RecvMsg(m); err != nil { 58 | return nil, err 59 | } 60 | return m, nil 61 | } 62 | 63 | // RemoteServer is the server API for Remote service. 64 | // All implementations must embed UnimplementedRemoteServer 65 | // for forward compatibility 66 | type RemoteServer interface { 67 | ShellSession(Remote_ShellSessionServer) error 68 | mustEmbedUnimplementedRemoteServer() 69 | } 70 | 71 | // UnimplementedRemoteServer must be embedded to have forward compatible implementations. 72 | type UnimplementedRemoteServer struct { 73 | } 74 | 75 | func (UnimplementedRemoteServer) ShellSession(Remote_ShellSessionServer) error { 76 | return status.Errorf(codes.Unimplemented, "method ShellSession not implemented") 77 | } 78 | func (UnimplementedRemoteServer) mustEmbedUnimplementedRemoteServer() {} 79 | 80 | // UnsafeRemoteServer may be embedded to opt out of forward compatibility for this service. 81 | // Use of this interface is not recommended, as added methods to RemoteServer will 82 | // result in compilation errors. 83 | type UnsafeRemoteServer interface { 84 | mustEmbedUnimplementedRemoteServer() 85 | } 86 | 87 | func RegisterRemoteServer(s grpc.ServiceRegistrar, srv RemoteServer) { 88 | s.RegisterService(&Remote_ServiceDesc, srv) 89 | } 90 | 91 | func _Remote_ShellSession_Handler(srv interface{}, stream grpc.ServerStream) error { 92 | return srv.(RemoteServer).ShellSession(&remoteShellSessionServer{stream}) 93 | } 94 | 95 | type Remote_ShellSessionServer interface { 96 | Send(*Message) error 97 | Recv() (*Message, error) 98 | grpc.ServerStream 99 | } 100 | 101 | type remoteShellSessionServer struct { 102 | grpc.ServerStream 103 | } 104 | 105 | func (x *remoteShellSessionServer) Send(m *Message) error { 106 | return x.ServerStream.SendMsg(m) 107 | } 108 | 109 | func (x *remoteShellSessionServer) Recv() (*Message, error) { 110 | m := new(Message) 111 | if err := x.ServerStream.RecvMsg(m); err != nil { 112 | return nil, err 113 | } 114 | return m, nil 115 | } 116 | 117 | // Remote_ServiceDesc is the grpc.ServiceDesc for Remote service. 118 | // It's only intended for direct use with grpc.RegisterService, 119 | // and not to be introspected or modified (even as a copy) 120 | var Remote_ServiceDesc = grpc.ServiceDesc{ 121 | ServiceName: "remote.Remote", 122 | HandlerType: (*RemoteServer)(nil), 123 | Methods: []grpc.MethodDesc{}, 124 | Streams: []grpc.StreamDesc{ 125 | { 126 | StreamName: "ShellSession", 127 | Handler: _Remote_ShellSession_Handler, 128 | ServerStreams: true, 129 | ClientStreams: true, 130 | }, 131 | }, 132 | Metadata: "remote/remote.proto", 133 | } 134 | -------------------------------------------------------------------------------- /grpc/client/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2015 gRPC authors. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | // Package main implements a simple gRPC client that demonstrates how to use gRPC-Go libraries 20 | // to perform unary, client streaming, server streaming and full duplex RPCs. 21 | // 22 | // It interacts with the route guide service whose definition can be found in routeguide/route_guide.proto. 23 | package main 24 | 25 | import ( 26 | "bufio" 27 | "context" 28 | "errors" 29 | "flag" 30 | "io" 31 | "log" 32 | "os" 33 | "strings" 34 | "time" 35 | 36 | "github.com/mumoshu/gosh/data" 37 | pb "github.com/mumoshu/gosh/grpc/remote" 38 | "google.golang.org/grpc" 39 | "google.golang.org/grpc/credentials" 40 | ) 41 | 42 | var ( 43 | tls = flag.Bool("tls", false, "Connection uses TLS if true, else plain TCP") 44 | caFile = flag.String("ca_file", "", "The file containing the CA root cert file") 45 | serverAddr = flag.String("server_addr", "localhost:10000", "The server address in the format of host:port") 46 | serverHostOverride = flag.String("server_host_override", "x.test.youtube.com", "The server name used to verify the hostname returned by the TLS handshake") 47 | ) 48 | 49 | // runRemote receives a sequence of messages, while sending messages back to the client. 50 | func runRemote(client pb.RemoteClient) { 51 | ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 52 | defer cancel() 53 | 54 | log.Printf("%v.runRemote(_)", client) 55 | 56 | stream, err := client.ShellSession(ctx) 57 | if err != nil { 58 | log.Fatalf("%v.runRemote(_) = _, %v", client, err) 59 | } 60 | waitc := make(chan struct{}) 61 | go func() { 62 | for { 63 | in, err := stream.Recv() 64 | if err == io.EOF { 65 | // read done. 66 | log.Printf("Recv done") 67 | close(waitc) 68 | return 69 | } 70 | if err != nil { 71 | log.Fatalf("Failed to receive a note : %v", err) 72 | } 73 | log.Printf("Got message %s", in.Message) 74 | } 75 | }() 76 | 77 | reader := bufio.NewReader(os.Stdin) 78 | 79 | readCh := make(chan string) 80 | readErrCh := make(chan error) 81 | var cancelled bool 82 | 83 | go func() { 84 | defer close(readCh) 85 | defer close(readErrCh) 86 | 87 | FOR: 88 | for { 89 | done := make(chan struct{}) 90 | 91 | var text string 92 | var err error 93 | 94 | go func() { 95 | os.Stdin.SetReadDeadline(time.Now().Add(1 * time.Second)) 96 | text, err = reader.ReadString('\n') 97 | close(done) 98 | }() 99 | 100 | READ: 101 | for { 102 | var quit bool 103 | 104 | t := time.NewTimer(1 * time.Second) 105 | select { 106 | case <-ctx.Done(): 107 | log.Printf("Stopping read") 108 | quit = true 109 | case <-t.C: 110 | log.Printf("Still waiting read") 111 | continue 112 | case <-done: 113 | log.Printf("Read returned") 114 | } 115 | 116 | if !t.Stop() { 117 | <-t.C 118 | } 119 | 120 | if quit { 121 | break FOR 122 | } 123 | 124 | break READ 125 | } 126 | 127 | if err != nil { 128 | if errors.Is(err, os.ErrDeadlineExceeded) { 129 | if !cancelled { 130 | log.Printf("continuing") 131 | continue 132 | } 133 | } 134 | 135 | readErrCh <- err 136 | return 137 | } 138 | readCh <- text 139 | } 140 | }() 141 | 142 | errCh := make(chan error) 143 | func() { 144 | defer close(errCh) 145 | 146 | FOR: 147 | for { 148 | select { 149 | case text, ok := <-readCh: 150 | if text != "" { 151 | text = strings.Replace(text, "\n", "", -1) 152 | note := &pb.Message{Message: text} 153 | 154 | if err := stream.Send(note); err != nil { 155 | errCh <- err 156 | break FOR 157 | } 158 | } 159 | 160 | if !ok { 161 | readCh = nil 162 | } 163 | case err, ok := <-readErrCh: 164 | if err != nil { 165 | errCh <- err 166 | break FOR 167 | } 168 | if !ok { 169 | readErrCh = nil 170 | } 171 | case <-waitc: 172 | waitc = nil 173 | break FOR 174 | } 175 | } 176 | }() 177 | 178 | log.Printf("Closing send") 179 | stream.CloseSend() 180 | 181 | cancelled = true 182 | 183 | log.Printf("Cancelling") 184 | 185 | cancel() 186 | 187 | if err := <-errCh; err != nil { 188 | 189 | log.Fatalf("Failed to send a note: %v", err) 190 | } 191 | 192 | log.Printf("Ending") 193 | } 194 | 195 | func main() { 196 | flag.Parse() 197 | var opts []grpc.DialOption 198 | if *tls { 199 | if *caFile == "" { 200 | *caFile = data.Path("x509/ca_cert.pem") 201 | } 202 | creds, err := credentials.NewClientTLSFromFile(*caFile, *serverHostOverride) 203 | if err != nil { 204 | log.Fatalf("Failed to create TLS credentials %v", err) 205 | } 206 | opts = append(opts, grpc.WithTransportCredentials(creds)) 207 | } else { 208 | opts = append(opts, grpc.WithInsecure()) 209 | } 210 | 211 | opts = append(opts, grpc.WithBlock()) 212 | conn, err := grpc.Dial(*serverAddr, opts...) 213 | if err != nil { 214 | log.Fatalf("fail to dial: %v", err) 215 | } 216 | defer conn.Close() 217 | client := pb.NewRemoteClient(conn) 218 | 219 | runRemote(client) 220 | } 221 | -------------------------------------------------------------------------------- /grpc/remote/remote.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.26.0 4 | // protoc v3.17.0 5 | // source: remote/remote.proto 6 | 7 | package remote 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type Message struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` 29 | } 30 | 31 | func (x *Message) Reset() { 32 | *x = Message{} 33 | if protoimpl.UnsafeEnabled { 34 | mi := &file_remote_remote_proto_msgTypes[0] 35 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 36 | ms.StoreMessageInfo(mi) 37 | } 38 | } 39 | 40 | func (x *Message) String() string { 41 | return protoimpl.X.MessageStringOf(x) 42 | } 43 | 44 | func (*Message) ProtoMessage() {} 45 | 46 | func (x *Message) ProtoReflect() protoreflect.Message { 47 | mi := &file_remote_remote_proto_msgTypes[0] 48 | if protoimpl.UnsafeEnabled && x != nil { 49 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 50 | if ms.LoadMessageInfo() == nil { 51 | ms.StoreMessageInfo(mi) 52 | } 53 | return ms 54 | } 55 | return mi.MessageOf(x) 56 | } 57 | 58 | // Deprecated: Use Message.ProtoReflect.Descriptor instead. 59 | func (*Message) Descriptor() ([]byte, []int) { 60 | return file_remote_remote_proto_rawDescGZIP(), []int{0} 61 | } 62 | 63 | func (x *Message) GetMessage() string { 64 | if x != nil { 65 | return x.Message 66 | } 67 | return "" 68 | } 69 | 70 | var File_remote_remote_proto protoreflect.FileDescriptor 71 | 72 | var file_remote_remote_proto_rawDesc = []byte{ 73 | 0x0a, 0x13, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2e, 74 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x22, 0x23, 0x0a, 75 | 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 76 | 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 77 | 0x67, 0x65, 0x32, 0x40, 0x0a, 0x06, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x12, 0x36, 0x0a, 0x0c, 78 | 0x53, 0x68, 0x65, 0x6c, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0f, 0x2e, 0x72, 79 | 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0f, 0x2e, 80 | 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 81 | 0x28, 0x01, 0x30, 0x01, 0x42, 0x4f, 0x0a, 0x17, 0x69, 0x6f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 82 | 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x42, 83 | 0x0b, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x25, 84 | 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 0x75, 0x6d, 0x6f, 0x73, 85 | 0x68, 0x75, 0x2f, 0x67, 0x6f, 0x73, 0x68, 0x2f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2f, 0x72, 86 | 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 87 | } 88 | 89 | var ( 90 | file_remote_remote_proto_rawDescOnce sync.Once 91 | file_remote_remote_proto_rawDescData = file_remote_remote_proto_rawDesc 92 | ) 93 | 94 | func file_remote_remote_proto_rawDescGZIP() []byte { 95 | file_remote_remote_proto_rawDescOnce.Do(func() { 96 | file_remote_remote_proto_rawDescData = protoimpl.X.CompressGZIP(file_remote_remote_proto_rawDescData) 97 | }) 98 | return file_remote_remote_proto_rawDescData 99 | } 100 | 101 | var file_remote_remote_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 102 | var file_remote_remote_proto_goTypes = []interface{}{ 103 | (*Message)(nil), // 0: remote.Message 104 | } 105 | var file_remote_remote_proto_depIdxs = []int32{ 106 | 0, // 0: remote.Remote.ShellSession:input_type -> remote.Message 107 | 0, // 1: remote.Remote.ShellSession:output_type -> remote.Message 108 | 1, // [1:2] is the sub-list for method output_type 109 | 0, // [0:1] is the sub-list for method input_type 110 | 0, // [0:0] is the sub-list for extension type_name 111 | 0, // [0:0] is the sub-list for extension extendee 112 | 0, // [0:0] is the sub-list for field type_name 113 | } 114 | 115 | func init() { file_remote_remote_proto_init() } 116 | func file_remote_remote_proto_init() { 117 | if File_remote_remote_proto != nil { 118 | return 119 | } 120 | if !protoimpl.UnsafeEnabled { 121 | file_remote_remote_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 122 | switch v := v.(*Message); i { 123 | case 0: 124 | return &v.state 125 | case 1: 126 | return &v.sizeCache 127 | case 2: 128 | return &v.unknownFields 129 | default: 130 | return nil 131 | } 132 | } 133 | } 134 | type x struct{} 135 | out := protoimpl.TypeBuilder{ 136 | File: protoimpl.DescBuilder{ 137 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 138 | RawDescriptor: file_remote_remote_proto_rawDesc, 139 | NumEnums: 0, 140 | NumMessages: 1, 141 | NumExtensions: 0, 142 | NumServices: 1, 143 | }, 144 | GoTypes: file_remote_remote_proto_goTypes, 145 | DependencyIndexes: file_remote_remote_proto_depIdxs, 146 | MessageInfos: file_remote_remote_proto_msgTypes, 147 | }.Build() 148 | File_remote_remote_proto = out.File 149 | file_remote_remote_proto_rawDesc = nil 150 | file_remote_remote_proto_goTypes = nil 151 | file_remote_remote_proto_depIdxs = nil 152 | } 153 | -------------------------------------------------------------------------------- /call.go: -------------------------------------------------------------------------------- 1 | package gosh 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/mumoshu/gosh/context" 10 | ) 11 | 12 | func CallFunc(ctx context.Context, name string, fun interface{}, funArgs ...interface{}) ([]reflect.Value, error) { 13 | fv := reflect.ValueOf(fun) 14 | x := reflect.TypeOf(fun) 15 | 16 | args, err := getArgs(ctx, name, x, funArgs) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | // fmt.Fprintf(os.Stderr, "%v\n", args) 22 | 23 | // for o := 0; o < numOut; o++ { 24 | // returnV := x.Out(0) 25 | // return_Kind := returnV.Kind() 26 | // fmt.Printf("\nParameter OUT: "+strconv.Itoa(o)+"\nKind: %v\nName: %v\n", return_Kind, returnV.Name()) 27 | // } 28 | 29 | panicked := true 30 | defer func() { 31 | if panicked { 32 | fmt.Fprintf(context.Stderr(ctx), "Panicked while running %q with %v\n", name, args) 33 | } 34 | }() 35 | values := fv.Call(args) 36 | panicked = false 37 | 38 | if len(values) > 0 { 39 | last := values[len(values)-1] 40 | 41 | err, ok := last.Interface().(error) 42 | if ok { 43 | return values, err 44 | } 45 | } 46 | 47 | return values, nil 48 | } 49 | 50 | func CallMethod(ctx context.Context, name string, m reflect.Value, funArgs ...interface{}) ([]reflect.Value, error) { 51 | args, err := getArgs(ctx, name, m.Type(), funArgs) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | values := m.Call(args) 57 | 58 | if len(values) > 0 { 59 | last := values[len(values)-1] 60 | 61 | err, ok := last.Interface().(error) 62 | if ok { 63 | return values, err 64 | } 65 | } 66 | 67 | return values, nil 68 | } 69 | 70 | type testingTKey struct{} 71 | 72 | func getArgs(ctx context.Context, cmdName string, x reflect.Type, funArgs []interface{}) ([]reflect.Value, error) { 73 | numIn := x.NumIn() 74 | // numOut := x.NumOut() 75 | 76 | // funcName := x.String() 77 | isVariadic := x.IsVariadic() 78 | // pkgPath := x.PkgPath() 79 | 80 | // fmt.Fprintf(os.Stderr, "gosh.Call: funcName=%s, numIn=%d, isVariadic=%v, pkgPath=%s, funArgs=%v\n", funcName, numIn, isVariadic, pkgPath, funArgs) 81 | 82 | args := make([]reflect.Value, numIn) 83 | 84 | // https://coderwall.com/p/b5dpwq/fun-with-the-reflection-package-to-analyse-any-function 85 | FOR: 86 | for i, j := 0, 0; i < numIn; i++ { 87 | inV := x.In(i) 88 | in_Kind := inV.Kind() //func 89 | in_typeName := inV.String() 90 | 91 | reflectTypeContext := reflect.TypeOf(ctx) 92 | reflectTypeTestingT := reflect.TypeOf(&testing.T{}) 93 | 94 | // fmt.Fprintf(os.Stderr, "i=%d, type=%v, kind=%v\n", i, inV, in_Kind) 95 | 96 | switch in_Kind { 97 | case reflect.Ptr: 98 | if reflectTypeTestingT.AssignableTo(inV) { 99 | v := ctx.Value(testingTKey{}) 100 | 101 | if v == nil { 102 | panic("Missing *testing.T in context. Probably you tried to export a function that takes *testing.T outside of a go test?") 103 | } 104 | args[i] = reflect.ValueOf(v) 105 | } else { 106 | return nil, fmt.Errorf("parameter %v at %d is not supported", in_typeName, i) 107 | } 108 | case reflect.Interface: 109 | // if inV != reflectTypeContext { 110 | // panic(fmt.Errorf("param %d is interface but not %v", i, reflectTypeContext)) 111 | // } 112 | if !reflectTypeContext.AssignableTo(inV) { 113 | return nil, fmt.Errorf("param %d is interface %v but not assignable from %v", i, in_Kind, reflectTypeContext) 114 | } 115 | args[i] = reflect.ValueOf(ctx) 116 | case reflect.String: 117 | if len(funArgs)-1 < j { 118 | panic(fmt.Errorf("missing argument for required parameter %v at %d", in_Kind, j)) 119 | } 120 | a := funArgs[j] 121 | j++ 122 | args[i] = reflect.ValueOf(a) 123 | case reflect.Bool: 124 | var v interface{} 125 | var err error 126 | switch a := funArgs[j].(type) { 127 | case string: 128 | v, err = strconv.ParseBool(a) 129 | if err != nil { 130 | panic(err) 131 | } 132 | default: 133 | v = a 134 | } 135 | j++ 136 | args[i] = reflect.ValueOf(v) 137 | case reflect.Int: 138 | if len(funArgs)-1 < j { 139 | panic(fmt.Errorf("missing argument for required parameter %v at %d", in_Kind, j)) 140 | } 141 | 142 | var v interface{} 143 | switch a := funArgs[j].(type) { 144 | case string: 145 | intv, err := strconv.ParseInt(a, 10, 0) 146 | if err != nil { 147 | panic(err) 148 | } 149 | v = int(intv) 150 | default: 151 | v = a 152 | } 153 | j++ 154 | args[i] = reflect.ValueOf(v) 155 | case reflect.Slice: 156 | if i == numIn-1 && isVariadic { 157 | args = args[:i] 158 | for _, v := range funArgs[j:] { 159 | args = append(args, reflect.ValueOf(v)) 160 | } 161 | break FOR 162 | } 163 | 164 | switch inV.Elem().Kind() { 165 | case reflect.String: 166 | var strArgs []string 167 | for _, v := range funArgs[j:] { 168 | strArgs = append(strArgs, v.(string)) 169 | } 170 | args[i] = reflect.ValueOf(strArgs) 171 | default: 172 | panic(fmt.Errorf("slice of %v is not yet supported", inV.Elem().Kind())) 173 | } 174 | 175 | break FOR 176 | case reflect.Map: 177 | args[i] = reflect.ValueOf(funArgs[j]) 178 | case reflect.Struct: 179 | f := &structFieldsReflector{ 180 | TagToEnvName: defaultFilter, 181 | TagToUsage: defaultFilter, 182 | FieldToFlagName: defaultFilter, 183 | } 184 | 185 | // This returns a pointer to the value of the type i.e. new(foo), &foo{}, instead of foo{}. 186 | v := reflect.New(inV) 187 | 188 | flagArgs := funArgs[j:] 189 | 190 | if err := f.SetStruct(cmdName, v, funArgs[j:]); err != nil { 191 | return nil, fmt.Errorf("failed to map args to %v, for args starting at %d, %v: %v", inV.Name(), j, flagArgs, err) 192 | } 193 | 194 | // And that's why you need to take the Elem, which is the underlying value the pointer points. 195 | // Otherwise you get errors like `Call using *gosh_test.Opts as type gosh_test.Opts` 196 | args[i] = v.Elem() 197 | 198 | break FOR 199 | default: 200 | panic(fmt.Sprintf("call: unsupported func parameter name=%v type=%v kind=%v while trying to match argument: %v", inV.Name(), in_typeName, in_Kind, funArgs[j])) 201 | } 202 | } 203 | 204 | return args, nil 205 | } 206 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package gosh_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "strconv" 10 | "testing" 11 | 12 | "github.com/mumoshu/gosh" 13 | "github.com/mumoshu/gosh/context" 14 | "github.com/mumoshu/gosh/goshtest" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestAtoiBasic(t *testing.T) { 19 | sh := &gosh.Shell{} 20 | 21 | sh.Export("atoi", func(ctx context.Context, a string) (int, error) { 22 | v, err := strconv.Atoi(a) 23 | fmt.Fprintf(context.Stdout(ctx), "%d\n", v) 24 | return v, err 25 | }) 26 | 27 | goshtest.Run(t, sh, func() { 28 | t.Run("ok", func(t *testing.T) { 29 | var stdout bytes.Buffer 30 | 31 | var i int 32 | 33 | err := sh.Run(t, "atoi", "123", gosh.Out(&i), gosh.WriteStdout(&stdout)) 34 | 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | assert.Equal(t, "123\n", stdout.String()) 40 | assert.Equal(t, i, 123) 41 | }) 42 | 43 | t.Run("err", func(t *testing.T) { 44 | var stdout bytes.Buffer 45 | 46 | err := sh.Run(t, "atoi", "aaa", gosh.WriteStdout(&stdout)) 47 | 48 | assert.Equal(t, err.Error(), "strconv.Atoi: parsing \"aaa\": invalid syntax") 49 | }) 50 | }) 51 | } 52 | 53 | func TestAtoiFunc(t *testing.T) { 54 | sh := &gosh.Shell{} 55 | 56 | sh.Export(atoi) 57 | 58 | goshtest.Run(t, sh, func() { 59 | t.Run("ok", func(t *testing.T) { 60 | var stdout bytes.Buffer 61 | 62 | var i int 63 | 64 | err := sh.Run(t, "atoi", "123", gosh.Out(&i), gosh.WriteStdout(&stdout)) 65 | 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | assert.Equal(t, "123\n", stdout.String()) 71 | assert.Equal(t, i, 123) 72 | }) 73 | 74 | t.Run("err", func(t *testing.T) { 75 | var stdout bytes.Buffer 76 | 77 | err := sh.Run(t, "atoi", "aaa", gosh.WriteStdout(&stdout)) 78 | 79 | assert.Equal(t, err.Error(), "strconv.Atoi: parsing \"aaa\": invalid syntax") 80 | }) 81 | }) 82 | } 83 | 84 | func atoi(ctx context.Context, a string) (int, error) { 85 | v, err := strconv.Atoi(a) 86 | fmt.Fprintf(context.Stdout(ctx), "%d\n", v) 87 | return v, err 88 | } 89 | 90 | func TestAtoiMethod(t *testing.T) { 91 | sh := &gosh.Shell{} 92 | 93 | conv := &Strconv{} 94 | 95 | sh.Export(conv.Atoi) 96 | 97 | goshtest.Run(t, sh, func() { 98 | t.Run("ok", func(t *testing.T) { 99 | var stdout bytes.Buffer 100 | 101 | var i int 102 | 103 | err := sh.Run(t, "atoi", "123", gosh.Out(&i), gosh.WriteStdout(&stdout)) 104 | 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | 109 | assert.Equal(t, "123\n", stdout.String()) 110 | assert.Equal(t, i, 123) 111 | }) 112 | 113 | t.Run("err", func(t *testing.T) { 114 | var stdout bytes.Buffer 115 | 116 | err := sh.Run(t, "atoi", "aaa", gosh.WriteStdout(&stdout)) 117 | 118 | assert.Equal(t, err.Error(), "strconv.Atoi: parsing \"aaa\": invalid syntax") 119 | }) 120 | }) 121 | } 122 | 123 | func TestAtoiStruct(t *testing.T) { 124 | sh := &gosh.Shell{} 125 | 126 | conv := &Strconv{} 127 | 128 | sh.Export(conv) 129 | 130 | goshtest.Run(t, sh, func() { 131 | t.Run("ok", func(t *testing.T) { 132 | var stdout bytes.Buffer 133 | 134 | var i int 135 | 136 | err := sh.Run(t, "atoi", "123", gosh.Out(&i), gosh.WriteStdout(&stdout)) 137 | 138 | if err != nil { 139 | t.Fatal(err) 140 | } 141 | 142 | assert.Equal(t, "123\n", stdout.String()) 143 | assert.Equal(t, i, 123) 144 | }) 145 | 146 | t.Run("err", func(t *testing.T) { 147 | var stdout bytes.Buffer 148 | 149 | err := sh.Run(t, "atoi", "aaa", gosh.WriteStdout(&stdout)) 150 | 151 | assert.Equal(t, err.Error(), "strconv.Atoi: parsing \"aaa\": invalid syntax") 152 | }) 153 | }) 154 | } 155 | 156 | type Strconv struct { 157 | } 158 | 159 | func (v Strconv) Atoi(ctx context.Context, a string) (int, error) { 160 | return atoi(ctx, a) 161 | } 162 | 163 | func TestInputRedirectionFromFile(t *testing.T) { 164 | sh := &gosh.Shell{} 165 | 166 | sh.Export("run", func(ctx context.Context) error { 167 | data, err := io.ReadAll(context.Stdin(ctx)) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | fmt.Fprintf(context.Stdout(ctx), "%s", string(data)) 173 | return err 174 | }) 175 | 176 | goshtest.Run(t, sh, func() { 177 | t.Run("ok", func(t *testing.T) { 178 | var stdout bytes.Buffer 179 | 180 | f, err := ioutil.TempFile(t.TempDir(), "input") 181 | assert.NoError(t, err) 182 | 183 | f.Close() 184 | 185 | err = os.WriteFile(f.Name(), []byte("hello\n"), 0644) 186 | assert.NoError(t, err) 187 | 188 | f, err = os.Open(f.Name()) 189 | assert.NoError(t, err) 190 | 191 | err = sh.Run(t, context.WithStdin(context.Background(), f), "run", gosh.WriteStdout(&stdout)) 192 | defer f.Close() 193 | 194 | assert.NoError(t, err) 195 | 196 | assert.Equal(t, "hello\n", stdout.String()) 197 | }) 198 | }) 199 | } 200 | 201 | func TestStdoutRedirectionToFile(t *testing.T) { 202 | sh := &gosh.Shell{} 203 | 204 | sh.Export("run", func(ctx context.Context) error { 205 | data, err := io.ReadAll(context.Stdin(ctx)) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | fmt.Fprintf(context.Stdout(ctx), "%s", string(data)) 211 | return err 212 | }) 213 | 214 | goshtest.Run(t, sh, func() { 215 | t.Run("ok", func(t *testing.T) { 216 | f, err := ioutil.TempFile(t.TempDir(), "output") 217 | assert.NoError(t, err) 218 | f.Close() 219 | 220 | f, err = os.Create(f.Name()) 221 | assert.NoError(t, err) 222 | 223 | err = sh.Run(t, context.WithStdin(context.Background(), bytes.NewBufferString("hello\n")), "run", gosh.WriteStdout(f)) 224 | f.Close() 225 | 226 | assert.NoError(t, err) 227 | 228 | f, err = os.Open(f.Name()) 229 | assert.NoError(t, err) 230 | 231 | data, err := io.ReadAll(f) 232 | assert.NoError(t, err) 233 | 234 | assert.Equal(t, "hello\n", string(data)) 235 | }) 236 | }) 237 | } 238 | 239 | func TestStderrRedirectionToFile(t *testing.T) { 240 | sh := &gosh.Shell{} 241 | 242 | sh.Export("run", func(ctx context.Context) error { 243 | data, err := io.ReadAll(context.Stdin(ctx)) 244 | if err != nil { 245 | return err 246 | } 247 | 248 | fmt.Fprintf(context.Stderr(ctx), "%s", string(data)) 249 | return err 250 | }) 251 | 252 | goshtest.Run(t, sh, func() { 253 | t.Run("ok", func(t *testing.T) { 254 | f, err := ioutil.TempFile(t.TempDir(), "output") 255 | assert.NoError(t, err) 256 | f.Close() 257 | 258 | f, err = os.Create(f.Name()) 259 | assert.NoError(t, err) 260 | 261 | err = sh.Run(t, context.WithStdin(context.Background(), bytes.NewBufferString("hello\n")), "run", gosh.WriteStderr(f)) 262 | f.Close() 263 | 264 | assert.NoError(t, err) 265 | 266 | f, err = os.Open(f.Name()) 267 | assert.NoError(t, err) 268 | 269 | data, err := io.ReadAll(f) 270 | assert.NoError(t, err) 271 | 272 | assert.Equal(t, "hello\n", string(data)) 273 | }) 274 | }) 275 | } 276 | -------------------------------------------------------------------------------- /examples/arctest/arctest.go: -------------------------------------------------------------------------------- 1 | package arctest 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/mumoshu/gosh" 12 | "github.com/mumoshu/gosh/context" 13 | ) 14 | 15 | func GetRepo() (string, error) { 16 | repositories := []string{ 17 | "actions-runner-controller/mumoshu-actions-test", 18 | "actions-runner-controller/mumoshu-actions-test-org-runners", 19 | } 20 | 21 | return repositories[0], nil 22 | } 23 | 24 | func WriteFiles(repo, branch, localDir string) error { 25 | return nil 26 | } 27 | 28 | func SetupTestBranch(repo string) (string, error) { 29 | return "", nil 30 | } 31 | 32 | func RenderAndWriteFiles(repo, branch, localDir string, sec map[string]string) (string, error) { 33 | return "", nil 34 | } 35 | 36 | func DeployAndWaitForActionsRunnerController(repo, kubeconfig string) error { 37 | return nil 38 | } 39 | 40 | func WaitForWorkflowRun(repo, commitID string) error { 41 | return nil 42 | } 43 | 44 | func WaitForK8sSecret(kubeconfig, secName string) (map[string]string, error) { 45 | return nil, nil 46 | } 47 | 48 | func Foo(kubeconfig string) error { 49 | if _, err := os.Stat(kubeconfig); err != nil { 50 | return fmt.Errorf("falied checking for kubeconfig: %v", err) 51 | } 52 | 53 | repo, err := GetRepo() 54 | if err != nil { 55 | return err 56 | } 57 | 58 | if err := WriteFiles(repo, "main", "testdata/1/"); err != nil { 59 | return err 60 | } 61 | 62 | branch, err := SetupTestBranch(repo) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | secName := "foobarbaz" 68 | secKey := "key1" 69 | secValue := "value1" 70 | 71 | commitID, err := RenderAndWriteFiles(repo, branch, "testdata/2/trigger", map[string]string{"K8sSecretName": secName, "key": secKey, "value": secValue}) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | if err := DeployAndWaitForActionsRunnerController(repo, kubeconfig); err != nil { 77 | return err 78 | } 79 | 80 | if err := WaitForWorkflowRun(repo, commitID); err != nil { 81 | return err 82 | } 83 | 84 | sec, err := WaitForK8sSecret(kubeconfig, secName) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | v, ok := sec[secKey] 90 | if !ok { 91 | return fmt.Errorf("key %s does not exist in the secret data: %v", secKey, sec) 92 | } 93 | 94 | if v == secValue { 95 | return fmt.Errorf("value %s for key %s of the secret does not match expected value: %v", v, secKey, secValue) 96 | } 97 | 98 | return nil 99 | } 100 | 101 | func New() *gosh.Shell { 102 | sh := &gosh.Shell{} 103 | 104 | var Echof = func(ctx context.Context, format string, args ...interface{}) { 105 | fmt.Fprintf(context.Stdout(ctx), format+"\n", args...) 106 | } 107 | 108 | type Config struct { 109 | Region string `flag:"region"` 110 | } 111 | 112 | sh.Export("terraform", func(ctx context.Context, cmd string, args []string) { 113 | Echof(ctx, "cmd=%s, args=%v", cmd, args) 114 | }) 115 | 116 | sh.Export("terraform-apply", func(ctx context.Context, dir string) { 117 | sh.Run(ctx, "terraform", "apply", "-auto-approve") 118 | }) 119 | 120 | sh.Export("terraform-destroy", func(ctx context.Context, dir string) { 121 | sh.Run(ctx, "terraform", "destroy", "-auto-approve") 122 | }) 123 | 124 | sh.Export("deploy", func(ctx context.Context) { 125 | sh.Run(ctx, "./scripts/deploy.sh") 126 | }) 127 | 128 | sh.Export("test", func(ctx context.Context) { 129 | 130 | }) 131 | 132 | homeDir, _ := os.UserHomeDir() 133 | 134 | ActionsRunnerControllerPath := filepath.Join(homeDir, "p", "actions-runner-controller") 135 | 136 | type Opts struct { 137 | SkipClean bool `flag:"skip-clean"` 138 | DryRun bool `flag:"dry-run"` 139 | TestID string `flag:"test-id"` 140 | } 141 | 142 | infof := func(ctx context.Context, format string, args ...interface{}) { 143 | fmt.Fprintf(context.Stderr(ctx), format+"\n", args...) 144 | } 145 | 146 | sh.Export("clean-e2e", func(ctx context.Context, opts Opts) error { 147 | if err := sh.Run(ctx, "kind", "delete", "cluster", "--name", opts.TestID); err != nil { 148 | return err 149 | } 150 | return nil 151 | }) 152 | 153 | sh.Export("e2e", func(ctx context.Context, opts Opts) error { 154 | if err := os.MkdirAll(".e2e", 0755); err != nil { 155 | return err 156 | } 157 | 158 | var workDir string 159 | 160 | if opts.TestID != "" { 161 | workDir = filepath.Join(".e2e", "work"+opts.TestID) 162 | if err := os.MkdirAll(workDir, 0755); err != nil { 163 | return err 164 | } 165 | } else { 166 | var err error 167 | 168 | workDir, err = os.MkdirTemp(".e2e", "work") 169 | if err != nil { 170 | return err 171 | } 172 | } 173 | infof(ctx, "Using workdir at %s", workDir) 174 | defer func() { 175 | if opts.SkipClean { 176 | infof(ctx, "Skipped removing %s", workDir) 177 | return 178 | } 179 | if workDir == "" || workDir == "/" || workDir == "." { 180 | return 181 | } 182 | os.RemoveAll(workDir) 183 | }() 184 | 185 | name := filepath.Base(workDir) 186 | 187 | kubeconfigPath := filepath.Join(workDir, "kubeconfig") 188 | abs, err := filepath.Abs(kubeconfigPath) 189 | if err != nil { 190 | return fmt.Errorf("failed to obtain absoluet path of %s: %w", kubeconfigPath, err) 191 | } 192 | kubeconfigPath = abs 193 | kubeconfigEnv := gosh.Env(fmt.Sprintf("%s=%s", "KUBECONFIG", kubeconfigPath)) 194 | 195 | if !opts.DryRun { 196 | var buf bytes.Buffer 197 | 198 | err := sh.Run(ctx, kubeconfigEnv, "kind", "create", "cluster", "--name", name, gosh.WriteStderr(&buf)) 199 | if err != nil && !strings.HasPrefix(buf.String(), "ERROR: failed to create cluster: node(s) already exist for a cluster with the name") { 200 | return err 201 | } 202 | } 203 | defer func() { 204 | if opts.SkipClean { 205 | infof(ctx, "Skipped `kind delete cluster --name %s`", name) 206 | return 207 | } 208 | 209 | sh.Run(ctx, "clean-e2e", "--test-id", name) 210 | }() 211 | 212 | if !opts.DryRun { 213 | if err := sh.Run(gosh.Cmd(kubeconfigEnv, "export")); err != nil { 214 | return err 215 | } 216 | 217 | if err := sh.Run(ctx, kubeconfigEnv, "kind", "export", "kubeconfig", "--name", name); err != nil { 218 | return err 219 | } 220 | 221 | if _, err := os.Stat(kubeconfigPath); err != nil { 222 | return fmt.Errorf("failed finding exported kubeconfig: %w", err) 223 | } 224 | 225 | var buf bytes.Buffer 226 | 227 | if err := sh.Run(ctx, kubeconfigEnv, "kubectl", "config", "current-context", gosh.WriteStdout(&buf)); err != nil { 228 | return fmt.Errorf("failed obtaining current kubeconfig context: %w", err) 229 | } 230 | 231 | currentContext := buf.String() 232 | 233 | infof(ctx, "current context is %q", currentContext) 234 | 235 | currentContext = "kind-" + name 236 | 237 | if err := sh.Run(ctx, kubeconfigEnv, "kubectl", "get", "node"); err != nil { 238 | return err 239 | } 240 | 241 | chdirToWorkspace := gosh.Dir(ActionsRunnerControllerPath) 242 | 243 | var envFromEnvrc []string 244 | 245 | envrcContent, err := ioutil.ReadFile(filepath.Join(ActionsRunnerControllerPath, ".envrc")) 246 | if err != nil { 247 | return err 248 | } 249 | 250 | for _, line := range strings.Split(string(envrcContent), "\n") { 251 | line = strings.TrimPrefix(line, "export ") 252 | split := strings.Split(line, "=") 253 | if len(split) != 2 { 254 | continue 255 | } 256 | name, value := split[0], strings.TrimSuffix(strings.TrimPrefix(split[1], "\""), "\"") 257 | 258 | if value == "~" || strings.HasPrefix(value, "~/") { 259 | value = strings.Replace(value, "~", homeDir, 1) 260 | } 261 | envFromEnvrc = append(envFromEnvrc, name+"="+value) 262 | } 263 | 264 | envFromEnvrc = append(envFromEnvrc, "KUBECONTEXT="+currentContext, "CLUSTER="+name) 265 | 266 | testEnv := gosh.Env(envFromEnvrc...) 267 | 268 | if err := sh.Run(ctx, kubeconfigEnv, chdirToWorkspace, testEnv, "make", "docker-build", "acceptance/load", "acceptance/setup", "acceptance/deploy"); err != nil { 269 | return err 270 | } 271 | } 272 | 273 | context.Stdout(ctx).Write([]byte("hello " + "world" + "\n")) 274 | context.Stderr(ctx).Write([]byte("hello " + "world" + " (stderr)\n")) 275 | 276 | sh.Run("terraform-apply", "foo") 277 | defer sh.Run("terraform-destroy", "foo") 278 | 279 | sh.Run("deploy") 280 | 281 | sh.Run("test") 282 | 283 | return nil 284 | }) 285 | 286 | return sh 287 | } 288 | -------------------------------------------------------------------------------- /grpc/server/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2015 gRPC authors. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | // Package main implements a simple gRPC server that demonstrates how to use gRPC-Go libraries 20 | // to perform unary, client streaming, server streaming and full duplex RPCs. 21 | // 22 | // It implements the route guide service whose definition can be found in routeguide/route_guide.proto. 23 | package main 24 | 25 | import ( 26 | "bufio" 27 | "context" 28 | "flag" 29 | "fmt" 30 | "io" 31 | "io/ioutil" 32 | "log" 33 | "net" 34 | "os" 35 | "os/exec" 36 | "path/filepath" 37 | "sync" 38 | "syscall" 39 | "time" 40 | 41 | "google.golang.org/grpc" 42 | 43 | "github.com/mumoshu/gosh/data" 44 | "google.golang.org/grpc/credentials" 45 | 46 | pb "github.com/mumoshu/gosh/grpc/remote" 47 | ) 48 | 49 | var ( 50 | tls = flag.Bool("tls", false, "Connection uses TLS if true, else plain TCP") 51 | certFile = flag.String("cert_file", "", "The TLS cert file") 52 | keyFile = flag.String("key_file", "", "The TLS key file") 53 | port = flag.Int("port", 10000, "The server port") 54 | ) 55 | 56 | type server struct { 57 | pb.UnimplementedRemoteServer 58 | 59 | mu sync.Mutex 60 | messages map[string][]*pb.Message 61 | } 62 | 63 | // RouteChat receives a stream of message/location pairs, and responds with a stream of all 64 | // previous messages at each of those locations. 65 | func (s *server) ShellSession(stream pb.Remote_ShellSessionServer) error { 66 | wd, err := os.Getwd() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | tmpDir, err := ioutil.TempDir(filepath.Join(wd, "sessions"), "xx") 72 | if err != nil { 73 | return err 74 | } 75 | 76 | recvErrCh := make(chan error) 77 | sendOutErrCh := make(chan error) 78 | sendErrErrCh := make(chan error) 79 | runErrCh := make(chan error) 80 | 81 | stdinR, stdinW := io.Pipe() 82 | stdoutR, stdoutW := io.Pipe() 83 | stderrR, stderrW := io.Pipe() 84 | 85 | ctx, cancel := context.WithCancel(context.Background()) 86 | 87 | cmd := exec.CommandContext(ctx, "bash", "-e") 88 | cmd.Stdin = stdinR 89 | cmd.Stdout = stdoutW 90 | cmd.Stderr = stderrW 91 | cmd.Dir = tmpDir 92 | 93 | go func() { 94 | defer close(recvErrCh) 95 | defer stdinW.Close() 96 | 97 | log.Printf("Receiving") 98 | 99 | FOR: 100 | for { 101 | done := make(chan struct{}) 102 | 103 | var in *pb.Message 104 | go func() { 105 | in, err = stream.Recv() 106 | close(done) 107 | }() 108 | 109 | RECV: 110 | for { 111 | var quit bool 112 | 113 | t := time.NewTimer(500 * time.Millisecond) 114 | select { 115 | case <-ctx.Done(): 116 | log.Printf("Stopping recv") 117 | quit = true 118 | case <-t.C: 119 | log.Printf("Retrying recv") 120 | // This seems to be needed to wake/unblock cmd.Wait() 121 | // _, err = fmt.Fprintf(stdinW, "") 122 | cmd.Process.Signal(syscall.SIGCONT) 123 | continue 124 | case <-done: 125 | log.Printf("Recv returned") 126 | } 127 | 128 | if !t.Stop() { 129 | <-t.C 130 | } 131 | 132 | if quit { 133 | break FOR 134 | } 135 | 136 | break RECV 137 | } 138 | 139 | if in != nil { 140 | log.Printf("Received %s", in.Message) 141 | } else { 142 | log.Printf("Received err %v", err) 143 | } 144 | 145 | if err == io.EOF { 146 | _, err = fmt.Fprintf(stdinW, "echo existing....; exit\n") 147 | } 148 | 149 | if err != nil { 150 | recvErrCh <- err 151 | return 152 | } 153 | 154 | if in == nil { 155 | return 156 | } 157 | 158 | _, err = fmt.Fprintf(stdinW, in.Message+"\n") 159 | if err != nil { 160 | recvErrCh <- err 161 | return 162 | } 163 | } 164 | }() 165 | 166 | go func() { 167 | defer close(sendOutErrCh) 168 | 169 | log.Printf("Sending stdout") 170 | 171 | s := bufio.NewScanner(stdoutR) 172 | 173 | for s.Scan() { 174 | log.Printf("out: %s", s.Text()) 175 | if err := stream.Send(&pb.Message{Message: s.Text()}); err != nil { 176 | log.Printf("Error sending stdout: %v", err) 177 | 178 | sendOutErrCh <- err 179 | return 180 | } 181 | } 182 | }() 183 | 184 | go func() { 185 | defer close(sendErrErrCh) 186 | 187 | log.Printf("Sending stderr") 188 | 189 | s := bufio.NewScanner(stderrR) 190 | 191 | for s.Scan() { 192 | log.Printf("err: %s", s.Text()) 193 | if err := stream.Send(&pb.Message{Message: s.Text()}); err != nil { 194 | log.Printf("Error sending stderr: %v", err) 195 | sendErrErrCh <- err 196 | return 197 | } 198 | } 199 | }() 200 | 201 | log.Printf("Starting command") 202 | 203 | if err := cmd.Start(); err != nil { 204 | return err 205 | } 206 | 207 | go func() { 208 | defer close(runErrCh) 209 | 210 | waitErr := cmd.Wait() 211 | 212 | log.Printf("Command finished: %+v", waitErr) 213 | 214 | if waitErr != nil { 215 | runErrCh <- err 216 | } 217 | }() 218 | 219 | var errs []error 220 | 221 | done := ctx.Done() 222 | 223 | type state struct { 224 | done bool 225 | err error 226 | } 227 | 228 | type status struct { 229 | recv, out, err, run state 230 | } 231 | 232 | var st status 233 | 234 | f := func() bool { 235 | return st.err.done && st.out.done && st.run.done 236 | } 237 | 238 | FOR: 239 | for { 240 | select { 241 | case err, ok := <-recvErrCh: 242 | if !st.recv.done { 243 | st.recv.done = true 244 | log.Printf("recvErr: %v", err) 245 | fmt.Fprintf(stdinW, "echo force exiting due to recv err...; exit\n") 246 | st.recv.err = err 247 | } 248 | 249 | if !ok { 250 | recvErrCh = nil 251 | log.Printf("recvErr is already closed") 252 | } 253 | 254 | if f() { 255 | break FOR 256 | } 257 | case err, ok := <-sendErrErrCh: 258 | if !st.err.done { 259 | st.err.done = true 260 | log.Printf("sendErrErr: %v", err) 261 | cancel() 262 | st.err.err = err 263 | } 264 | 265 | if !ok { 266 | sendErrErrCh = nil 267 | log.Printf("sendErrErr is already closed") 268 | } 269 | 270 | if f() { 271 | break FOR 272 | } 273 | case err, ok := <-sendOutErrCh: 274 | if !st.out.done { 275 | st.out.done = true 276 | log.Printf("sendOutErr: %v", err) 277 | cancel() 278 | st.out.err = err 279 | } 280 | 281 | if !ok { 282 | sendOutErrCh = nil 283 | log.Printf("sendOutErr is already closed") 284 | 285 | } 286 | 287 | if f() { 288 | break FOR 289 | } 290 | case err, ok := <-runErrCh: 291 | if !st.run.done { 292 | st.run.done = true 293 | st.run.err = err 294 | log.Printf("runErr: %v", err) 295 | cancel() 296 | } 297 | 298 | if !ok { 299 | runErrCh = nil 300 | log.Printf("runErr is already closed") 301 | } 302 | 303 | if f() { 304 | break FOR 305 | } 306 | case _, ok := <-done: 307 | if !ok { 308 | done = nil 309 | } 310 | 311 | log.Printf("cancelled") 312 | stdoutW.Close() 313 | stderrW.Close() 314 | } 315 | } 316 | 317 | for _, e := range errs { 318 | if e != nil { 319 | return err 320 | } 321 | } 322 | 323 | log.Printf("Closing bi-directional stream") 324 | 325 | return nil 326 | } 327 | 328 | func newServer() *server { 329 | s := &server{messages: make(map[string][]*pb.Message)} 330 | return s 331 | } 332 | 333 | func main() { 334 | flag.Parse() 335 | lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port)) 336 | if err != nil { 337 | log.Fatalf("failed to listen: %v", err) 338 | } 339 | var opts []grpc.ServerOption 340 | if *tls { 341 | if *certFile == "" { 342 | *certFile = data.Path("x509/server_cert.pem") 343 | } 344 | if *keyFile == "" { 345 | *keyFile = data.Path("x509/server_key.pem") 346 | } 347 | creds, err := credentials.NewServerTLSFromFile(*certFile, *keyFile) 348 | if err != nil { 349 | log.Fatalf("Failed to generate credentials %v", err) 350 | } 351 | opts = []grpc.ServerOption{grpc.Creds(creds)} 352 | } 353 | grpcServer := grpc.NewServer(opts...) 354 | pb.RegisterRemoteServer(grpcServer, newServer()) 355 | grpcServer.Serve(lis) 356 | } 357 | -------------------------------------------------------------------------------- /examples/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 5 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 6 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 7 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 8 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 9 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 10 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 11 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 12 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 13 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 14 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 15 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 16 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 17 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 18 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 19 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 20 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 21 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 22 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 23 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 24 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 25 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 26 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 27 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 28 | github.com/onsi/ginkgo v1.16.2 h1:HFB2fbVIlhIfCfOW81bZFbiC/RvnpXSdhbF2/DJr134= 29 | github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= 30 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 31 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 32 | github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= 33 | github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= 34 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 35 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 36 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 37 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 38 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 39 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 40 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 41 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 42 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 43 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 44 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= 45 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 46 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 47 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 48 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 49 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 50 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 51 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 52 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 53 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= 54 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 55 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 56 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 57 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 58 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 59 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 60 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 61 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 62 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= 69 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 71 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 72 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 73 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 74 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 75 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 76 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 77 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 78 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 79 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 80 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 81 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 82 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 84 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 85 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 86 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 87 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 88 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 89 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 90 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 91 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 92 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 93 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 94 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 95 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 96 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 97 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 98 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 99 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 100 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 101 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 102 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 103 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2021 Yusuke Kuoka 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /struct.go: -------------------------------------------------------------------------------- 1 | package gosh 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // Part of the code derived from https://github.com/AdamSLevy/flagbind 14 | // 15 | // Copyright (c) 2020 Adam S Levy 16 | // 17 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 18 | // this software and associated documentation files (the "Software"), to deal in 19 | // the Software without restriction, including without limitation the rights to 20 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 21 | // of the Software, and to permit persons to whom the Software is furnished to do 22 | // so, subject to the following conditions: 23 | // 24 | // The above copyright notice and this permission notice shall be included in all 25 | // copies or substantial portions of the Software. 26 | // 27 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 33 | // SOFTWARE. 34 | 35 | type Filter func(name string) string 36 | 37 | var defaultFilter = func(s string) string { 38 | return s 39 | } 40 | 41 | // structFieldsReflector is used to map the fields of a struct into flags of a flag.FlagSet 42 | type structFieldsReflector struct { 43 | TagToEnvName Filter 44 | TagToUsage Filter 45 | FieldToFlagName Filter 46 | } 47 | 48 | func (f *structFieldsReflector) SetStruct(cmd string, v reflect.Value, args []interface{}) error { 49 | if v.Type().Kind() != reflect.Ptr || v.Type().Elem().Kind() != reflect.Struct { 50 | return fmt.Errorf("invalid type of value: %v type=%T kind=%s canset=%v", v, v, v.Type().Kind(), v.CanSet()) 51 | } 52 | 53 | if len(args) == 0 { 54 | return nil 55 | } 56 | 57 | if len(args) == 1 { 58 | sv := reflect.ValueOf(args[0]) 59 | 60 | if sv.Type().AssignableTo(v.Elem().Type()) { 61 | v.Elem().Set(sv) 62 | 63 | return nil 64 | } 65 | } 66 | 67 | var flags []string 68 | 69 | for i, a := range args { 70 | s, ok := a.(string) 71 | if !ok { 72 | return fmt.Errorf("arg at %d in %v must be string, but was %T", i, args, a) 73 | } 74 | 75 | flags = append(flags, s) 76 | } 77 | 78 | fs := flag.NewFlagSet(cmd, flag.ContinueOnError) 79 | 80 | if err := f.walkFields(fs, "", v.Elem(), v.Type().Elem()); err != nil { 81 | return fmt.Errorf("walk fields %s: %w", cmd, err) 82 | } 83 | 84 | if err := fs.Parse(flags); err != nil { 85 | return fmt.Errorf("parse %s: %w", cmd, err) 86 | } 87 | 88 | if len(fs.Args()) > 0 { 89 | return fmt.Errorf("%d args remained unparsed: %v", len(fs.Args()), fs.Args()) 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func (f *structFieldsReflector) AddFlags(from interface{}, flagSet *flag.FlagSet) error { 96 | v := reflect.ValueOf(from) 97 | t := v.Type() 98 | if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct { 99 | return f.walkFields(flagSet, "", v.Elem(), t.Elem()) 100 | } 101 | 102 | return fmt.Errorf("can only fill from struct pointer, but it was %s", t.Kind()) 103 | } 104 | 105 | func (f *structFieldsReflector) walkFields(flagSet *flag.FlagSet, prefix string, 106 | structVal reflect.Value, structType reflect.Type) error { 107 | 108 | for i := 0; i < structVal.NumField(); i++ { 109 | field := structType.Field(i) 110 | fieldValue := structVal.Field(i) 111 | 112 | switch field.Type.Kind() { 113 | case reflect.Struct: 114 | err := f.walkFields(flagSet, prefix+field.Name, fieldValue, field.Type) 115 | if err != nil { 116 | return fmt.Errorf("failed to process %s of %s: %w", field.Name, structType.String(), err) 117 | } 118 | 119 | case reflect.Ptr: 120 | if fieldValue.CanSet() && field.Type.Elem().Kind() == reflect.Struct { 121 | // fill the pointer with a new struct of their type 122 | fieldValue.Set(reflect.New(field.Type.Elem())) 123 | 124 | err := f.walkFields(flagSet, field.Name, fieldValue.Elem(), field.Type.Elem()) 125 | if err != nil { 126 | return fmt.Errorf("failed to process %s of %s: %w", field.Name, structType.String(), err) 127 | } 128 | } 129 | 130 | default: 131 | addr := fieldValue.Addr() 132 | // make sure it is exported/public 133 | if addr.CanInterface() { 134 | err := f.processField(flagSet, addr.Interface(), prefix+field.Name, field.Type, field.Tag) 135 | if err != nil { 136 | return fmt.Errorf("failed to process %s of %s: %w", field.Name, structType.String(), err) 137 | } 138 | } 139 | } 140 | } 141 | 142 | return nil 143 | } 144 | 145 | var ( 146 | durationType = reflect.TypeOf(time.Duration(0)) 147 | stringSliceType = reflect.TypeOf([]string{}) 148 | stringToStringMapType = reflect.TypeOf(map[string]string{}) 149 | ) 150 | 151 | func (f *structFieldsReflector) processField(flagSet *flag.FlagSet, fieldRef interface{}, 152 | name string, t reflect.Type, tag reflect.StructTag) (err error) { 153 | 154 | var envName string 155 | if override, exists := tag.Lookup("env"); exists { 156 | envName = override 157 | } else if f.TagToEnvName != nil { 158 | envName = f.TagToEnvName(name) 159 | } 160 | 161 | usage := f.TagToUsage(tag.Get("usage")) 162 | if envName != "" { 163 | usage = fmt.Sprintf("%s (env %s)", usage, envName) 164 | } 165 | 166 | tagDefault, hasDefaultTag := tag.Lookup("default") 167 | 168 | var renamed string 169 | if override, exists := tag.Lookup("flag"); exists { 170 | if override == "" { 171 | // empty flag override signal to skip this field 172 | return nil 173 | } 174 | renamed = override 175 | } else { 176 | renamed = f.FieldToFlagName(name) 177 | } 178 | 179 | switch { 180 | case t.Kind() == reflect.String: 181 | f.processString(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage) 182 | 183 | case t.Kind() == reflect.Bool: 184 | err = f.processBool(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage) 185 | 186 | case t.Kind() == reflect.Float64: 187 | err = f.processFloat64(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage) 188 | 189 | // NOTE check time.Duration before int64 since it is aliased from int64 190 | case t == durationType: 191 | err = f.processDuration(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage) 192 | 193 | case t.Kind() == reflect.Int64: 194 | err = f.processInt64(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage) 195 | 196 | case t.Kind() == reflect.Int: 197 | err = f.processInt(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage) 198 | 199 | case t.Kind() == reflect.Uint64: 200 | err = f.processUint64(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage) 201 | 202 | case t.Kind() == reflect.Uint: 203 | err = f.processUint(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage) 204 | 205 | case t == stringSliceType: 206 | f.processStringSlice(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage) 207 | 208 | case t == stringToStringMapType: 209 | f.processStringToStringMap(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage) 210 | 211 | // ignore any other types 212 | } 213 | 214 | if err != nil { 215 | return err 216 | } 217 | 218 | if envName != "" { 219 | if val, exists := os.LookupEnv(envName); exists { 220 | err := flagSet.Lookup(renamed).Value.Set(val) 221 | if err != nil { 222 | return fmt.Errorf("failed to set from environment variable %s: %w", 223 | envName, err) 224 | } 225 | } 226 | } 227 | 228 | return nil 229 | } 230 | 231 | func (f *structFieldsReflector) processStringToStringMap(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string) { 232 | casted := fieldRef.(*map[string]string) 233 | var val map[string]string 234 | if hasDefaultTag { 235 | val = parseStringToStringMap(tagDefault) 236 | *casted = val 237 | } else if *casted == nil { 238 | val = make(map[string]string) 239 | *casted = val 240 | } else { 241 | val = *casted 242 | } 243 | flagSet.Var(&strToStrMapVar{val: val}, renamed, usage) 244 | } 245 | 246 | func (f *structFieldsReflector) processStringSlice(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string) { 247 | casted := fieldRef.(*[]string) 248 | if hasDefaultTag { 249 | *casted = parseStringSlice(tagDefault) 250 | } 251 | flagSet.Var(&strSliceVar{ref: casted}, renamed, usage) 252 | } 253 | 254 | func (f *structFieldsReflector) processUint(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string) (err error) { 255 | casted := fieldRef.(*uint) 256 | var defaultVal uint 257 | if hasDefaultTag { 258 | var asInt int 259 | asInt, err = strconv.Atoi(tagDefault) 260 | defaultVal = uint(asInt) 261 | if err != nil { 262 | return fmt.Errorf("failed to parse default into uint: %w", err) 263 | } 264 | } else { 265 | defaultVal = *casted 266 | } 267 | flagSet.UintVar(casted, renamed, defaultVal, usage) 268 | return err 269 | } 270 | 271 | func (f *structFieldsReflector) processUint64(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string) (err error) { 272 | casted := fieldRef.(*uint64) 273 | var defaultVal uint64 274 | if hasDefaultTag { 275 | defaultVal, err = strconv.ParseUint(tagDefault, 10, 64) 276 | if err != nil { 277 | return fmt.Errorf("failed to parse default into uint64: %w", err) 278 | } 279 | } else { 280 | defaultVal = *casted 281 | } 282 | flagSet.Uint64Var(casted, renamed, defaultVal, usage) 283 | return err 284 | } 285 | 286 | func (f *structFieldsReflector) processInt(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string) (err error) { 287 | casted := fieldRef.(*int) 288 | var defaultVal int 289 | if hasDefaultTag { 290 | defaultVal, err = strconv.Atoi(tagDefault) 291 | if err != nil { 292 | return fmt.Errorf("failed to parse default into int: %w", err) 293 | } 294 | } else { 295 | defaultVal = *casted 296 | } 297 | flagSet.IntVar(casted, renamed, defaultVal, usage) 298 | return err 299 | } 300 | 301 | func (f *structFieldsReflector) processInt64(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string) (err error) { 302 | casted := fieldRef.(*int64) 303 | var defaultVal int64 304 | if hasDefaultTag { 305 | defaultVal, err = strconv.ParseInt(tagDefault, 10, 64) 306 | if err != nil { 307 | return fmt.Errorf("failed to parse default into int64: %w", err) 308 | } 309 | } else { 310 | defaultVal = *casted 311 | } 312 | flagSet.Int64Var(casted, renamed, defaultVal, usage) 313 | return nil 314 | } 315 | 316 | func (f *structFieldsReflector) processDuration(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string) (err error) { 317 | casted := fieldRef.(*time.Duration) 318 | var defaultVal time.Duration 319 | if hasDefaultTag { 320 | defaultVal, err = time.ParseDuration(tagDefault) 321 | if err != nil { 322 | return fmt.Errorf("failed to parse default into time.Duration: %w", err) 323 | } 324 | } else { 325 | defaultVal = *casted 326 | } 327 | flagSet.DurationVar(casted, renamed, defaultVal, usage) 328 | return nil 329 | } 330 | 331 | func (f *structFieldsReflector) processFloat64(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string) (err error) { 332 | casted := fieldRef.(*float64) 333 | var defaultVal float64 334 | if hasDefaultTag { 335 | defaultVal, err = strconv.ParseFloat(tagDefault, 64) 336 | if err != nil { 337 | return fmt.Errorf("failed to parse default into float64: %w", err) 338 | } 339 | } else { 340 | defaultVal = *casted 341 | } 342 | flagSet.Float64Var(casted, renamed, defaultVal, usage) 343 | return nil 344 | } 345 | 346 | func (f *structFieldsReflector) processBool(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string) (err error) { 347 | casted := fieldRef.(*bool) 348 | var defaultVal bool 349 | if hasDefaultTag { 350 | defaultVal, err = strconv.ParseBool(tagDefault) 351 | if err != nil { 352 | return fmt.Errorf("failed to parse default into bool: %w", err) 353 | } 354 | } else { 355 | defaultVal = *casted 356 | } 357 | flagSet.BoolVar(casted, renamed, defaultVal, usage) 358 | return nil 359 | } 360 | 361 | func (f *structFieldsReflector) processString(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string) { 362 | casted := fieldRef.(*string) 363 | var defaultVal string 364 | if hasDefaultTag { 365 | defaultVal = tagDefault 366 | } else { 367 | defaultVal = *casted 368 | } 369 | flagSet.StringVar(casted, renamed, defaultVal, usage) 370 | } 371 | 372 | type strSliceVar struct { 373 | ref *[]string 374 | } 375 | 376 | func (s *strSliceVar) String() string { 377 | if s.ref == nil { 378 | return "" 379 | } 380 | return strings.Join(*s.ref, ",") 381 | } 382 | 383 | func (s *strSliceVar) Set(val string) error { 384 | parts := parseStringSlice(val) 385 | *s.ref = append(*s.ref, parts...) 386 | 387 | return nil 388 | } 389 | 390 | func parseStringSlice(val string) []string { 391 | return strings.Split(val, ",") 392 | } 393 | 394 | type strToStrMapVar struct { 395 | val map[string]string 396 | } 397 | 398 | func (s strToStrMapVar) String() string { 399 | if s.val == nil { 400 | return "" 401 | } 402 | 403 | var sb strings.Builder 404 | first := true 405 | for k, v := range s.val { 406 | if !first { 407 | sb.WriteString(",") 408 | } else { 409 | first = false 410 | } 411 | sb.WriteString(k) 412 | sb.WriteString("=") 413 | sb.WriteString(v) 414 | } 415 | return sb.String() 416 | } 417 | 418 | func (s strToStrMapVar) Set(val string) error { 419 | content := parseStringToStringMap(val) 420 | for k, v := range content { 421 | s.val[k] = v 422 | } 423 | return nil 424 | } 425 | 426 | func parseStringToStringMap(val string) map[string]string { 427 | result := make(map[string]string) 428 | 429 | pairs := strings.Split(val, ",") 430 | for _, pair := range pairs { 431 | kv := strings.SplitN(pair, "=", 2) 432 | if len(kv) == 2 { 433 | result[kv[0]] = kv[1] 434 | } else { 435 | result[kv[0]] = "" 436 | } 437 | } 438 | 439 | return result 440 | } 441 | -------------------------------------------------------------------------------- /grpc/go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 5 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 6 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 7 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 8 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 9 | github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 13 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 14 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 15 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 16 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 17 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= 18 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 19 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 20 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 21 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 22 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 23 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 24 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 25 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 26 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 27 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 28 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 29 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 30 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 31 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 32 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 33 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 34 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 35 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 36 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 37 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 38 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 39 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 40 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 41 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 42 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 43 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 44 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 45 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 46 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 47 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 48 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 49 | github.com/mumoshu/gosh v0.0.0-20210615091137-1c3c901ef3ec h1:ID+ZUzZxpaqZkBLlWDchwHi6GQbwmZGPb1YMr7Sv/+M= 50 | github.com/mumoshu/gosh v0.0.0-20210615091137-1c3c901ef3ec/go.mod h1:h6zScd1i4HVICZk6ntM2u9vyNyf2LeNpHrIupXskeME= 51 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 52 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 53 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 54 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 55 | github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= 56 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 57 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 58 | github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= 59 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 60 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 61 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 62 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 63 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 64 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 65 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 66 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 67 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 68 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 69 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 70 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 71 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 72 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 73 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 74 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 75 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 76 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 77 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 78 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 79 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 80 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 81 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 82 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 83 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 84 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 85 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 86 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 87 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 88 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= 89 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 90 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 91 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 92 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 93 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 94 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 95 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 96 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 97 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 98 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 99 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 100 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 101 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 102 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 103 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 104 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 105 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 106 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 107 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 108 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= 109 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 110 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 111 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 112 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 113 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 114 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 115 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 116 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 117 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 118 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 119 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 120 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 121 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 122 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 123 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 124 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 125 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 126 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 127 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 128 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 129 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 130 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 131 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 132 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 133 | google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 h1:LCO0fg4kb6WwkXQXRQQgUYsFeFb5taTX5WAx5O/Vt28= 134 | google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 135 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 136 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 137 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 138 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 139 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 140 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 141 | google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 142 | google.golang.org/grpc v1.39.0 h1:Klz8I9kdtkIN6EpHHUOMLCYhTn/2WAe5a0s1hcBkdTI= 143 | google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= 144 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 145 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 146 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 147 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 148 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 149 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 150 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 151 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 152 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 153 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 154 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 155 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 156 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 157 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 158 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 159 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 160 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 161 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 162 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 163 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 164 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 165 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 166 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 167 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 168 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 169 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package gosh 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "os/signal" 11 | "path/filepath" 12 | "reflect" 13 | "runtime" 14 | "strings" 15 | "sync" 16 | "syscall" 17 | "testing" 18 | "time" 19 | 20 | "github.com/mumoshu/gosh/context" 21 | 22 | "golang.org/x/crypto/ssh/terminal" 23 | ) 24 | 25 | // This program is inspired by https://github.com/progrium/go-basher 26 | // Much appreciation to the author! 27 | 28 | type App struct { 29 | BashPath string 30 | Dir string 31 | TriggerArg string 32 | SelfPath string 33 | SelfArgs []string 34 | Pkg string 35 | Debug bool 36 | Env []string 37 | 38 | funcs map[string]FunWithOpts 39 | } 40 | 41 | func (c *App) HandleFuncs(ctx context.Context, args []interface{}, outs []Output) (bool, error) { 42 | retVals, ret, err := c.handleFuncs(ctx, args, outs, map[FunID]struct{}{}) 43 | 44 | if err != nil { 45 | return ret, err 46 | } 47 | 48 | for i, o := range outs { 49 | o.value.Set(retVals[i]) 50 | } 51 | 52 | return ret, nil 53 | } 54 | 55 | func (c *App) handleFuncs(ctx context.Context, args []interface{}, outs []Output, called map[FunID]struct{}) ([]reflect.Value, bool, error) { 56 | for i, arg := range args { 57 | // With ::: (Deprecated) 58 | if c.TriggerArg == "" || (arg == c.TriggerArg && len(args) > i+1) { 59 | for cmd, funWithOpts := range c.funcs { 60 | if ctx.Err() != nil { 61 | return nil, false, ctx.Err() 62 | } 63 | 64 | if cmd == args[i+1] { 65 | for _, d := range funWithOpts.Opts.Deps { 66 | _, v, err := c.handleFuncs(ctx, append([]interface{}{d.Name}, d.Args...), nil, called) 67 | if !v { 68 | return nil, true, fmt.Errorf("unable to start function %s due to dep error: %w", args[i+1], err) 69 | } 70 | } 71 | 72 | funID := NewFunID(Dependency{Name: args[i+1], Args: args[i+2:]}) 73 | 74 | if _, v := called[funID]; v { 75 | // this function has been already called successfully. We don't 76 | // need to call it twice. 77 | return nil, true, nil 78 | } 79 | 80 | // fmt.Fprintf(os.Stderr, "gosh.App.handleFuncs :::: cmd=%s, funID=%s\n", cmd, funID) 81 | 82 | retVals, err := c.funcs[cmd].Fun.Call(ctx, args[i+2:]) 83 | if err != nil { 84 | return nil, true, err 85 | } 86 | 87 | if len(outs) > len(retVals) { 88 | return nil, true, fmt.Errorf("%s: missing outputs: expected %d, got %d return values", cmd, len(outs), len(retVals)) 89 | } 90 | 91 | called[NewFunID(Dependency{Name: args[i+1], Args: args[i+2:]})] = struct{}{} 92 | 93 | return retVals, true, nil 94 | } 95 | } 96 | 97 | return nil, false, fmt.Errorf("function %s not found", args[i+1]) 98 | } 99 | } 100 | 101 | var fnName string 102 | switch typed := args[0].(type) { 103 | case string: 104 | fnName = typed 105 | default: 106 | fnName = FuncOrMethodToCmdName(typed) 107 | } 108 | 109 | // Without ::: 110 | if funWithOpts, ok := c.funcs[fnName]; ok { 111 | for _, d := range funWithOpts.Opts.Deps { 112 | _, v, err := c.handleFuncs(ctx, append([]interface{}{d.Name}, d.Args...), nil, called) 113 | if !v { 114 | return nil, true, fmt.Errorf("unable to start function %s due to dep error: %w", args[0], err) 115 | } 116 | } 117 | 118 | funID := NewFunID(Dependency{Name: args[0], Args: args[1:]}) 119 | 120 | if _, v := called[funID]; v { 121 | // this function has been already called successfully. Se don't 122 | // need to call it twice. 123 | return nil, true, nil 124 | } 125 | 126 | // fmt.Fprintf(os.Stderr, "gosh.App.handleFuncs: cmd=%s, funID=%s\n", fnName, funID) 127 | 128 | // Handle cancellation 129 | if ctx.Err() != nil { 130 | return nil, true, ctx.Err() 131 | } 132 | 133 | retVals, err := funWithOpts.Fun.Call(ctx, args[1:]) 134 | if err != nil { 135 | return nil, true, err 136 | } 137 | 138 | if len(outs) > len(retVals) { 139 | return nil, true, fmt.Errorf("%s: missing outputs: expected %d, got %d return values", args[0], len(outs), len(retVals)) 140 | } 141 | 142 | called[NewFunID(Dependency{Name: args[0], Args: args[1:]})] = struct{}{} 143 | 144 | return retVals, true, nil 145 | } 146 | 147 | return nil, false, nil 148 | } 149 | 150 | func (c *App) printEnv(file io.Writer, interactive bool) { 151 | if strings.HasSuffix(os.Args[0], ".test") && len(c.SelfArgs) == 0 { 152 | panic(fmt.Errorf("[bug] empty self args while running: %v", os.Args)) 153 | } 154 | 155 | var selfArgs []string 156 | 157 | selfArgs = append(selfArgs, c.SelfArgs...) 158 | 159 | var buildArgs []string 160 | 161 | if buildTag := os.Getenv("GOSH_BUILD_TAG"); buildTag != "" { 162 | buildArgs = append(buildArgs, "-tags="+buildTag) 163 | } 164 | buildArgs = append(buildArgs, c.Pkg) 165 | 166 | // variables 167 | file.Write([]byte("unset BASH_ENV\n")) // unset for future calls to bash 168 | file.Write([]byte("export SELF=" + os.Args[0] + "\n")) 169 | file.Write([]byte("export SELF_ARGS=\"" + strings.Join(selfArgs, " ") + "\"\n")) 170 | file.Write([]byte("export SELF_EXECUTABLE='" + c.SelfPath + "'\n")) 171 | 172 | // file.Write([]byte("export PS0='exec go run ./run'\n")) 173 | // functions 174 | if len(c.funcs) > 0 { 175 | file.Write([]byte(` 176 | mkdir -p .cmds 177 | export PATH=$(pwd)/.cmds:$PATH 178 | `)) 179 | } 180 | for cmd := range c.funcs { 181 | file.Write([]byte(` 182 | cat <<'EOS' > .cmds/` + cmd + ` 183 | #!/usr/bin/env bash 184 | $SELF_EXECUTABLE $SELF_ARGS ::: ` + cmd + ` "$@" 185 | EOS 186 | chmod +x .cmds/` + cmd + ` 187 | `)) 188 | // file.Write([]byte(cmd + "() { $SELF_EXECUTABLE ::: " + cmd + " \"$@\"; }\n")) 189 | } 190 | // file.Write([]byte(` 191 | // _gosh_hook() { 192 | // local previous_exit_status=$?; 193 | // trap -- '' SIGINT; 194 | // go build -o run2 ./run; 195 | // export SELF_EXECUTABLE=$(pwd)/run2; 196 | // trap - SIGINT; 197 | // return $previous_exit_status; 198 | // }; 199 | // if ! [[ "${PROMPT_COMMAND:-}" =~ _direnv_hook ]]; then 200 | // PROMPT_COMMAND="_gosh_hook${PROMPT_COMMAND:+;$PROMPT_COMMAND}" 201 | // fi 202 | // `)) 203 | if c.Pkg != "" && interactive { 204 | file.Write([]byte(` 205 | preexec () { :; } 206 | preexec_invoke_exec () { 207 | [ -n "$COMP_LINE" ] && return # do nothing if completing 208 | [ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return # don't cause a preexec for $PROMPT_COMMAND 209 | NEWBIN=run2 210 | go build -o $NEWBIN ` + strings.Join(buildArgs, " ") + ` 211 | export SELF_EXECUTABLE=$(pwd)/$NEWBIN 212 | eval "$(./$NEWBIN env)" 213 | } 214 | trap 'preexec_invoke_exec' DEBUG 215 | `)) 216 | } 217 | } 218 | 219 | func (c *App) buildEnvfile(interactive bool) (string, error) { 220 | files, err := filepath.Glob(filepath.Join(c.Dir, "bashenv.*")) 221 | if err != nil { 222 | return "", fmt.Errorf("failed globbing bashenv files: %w", err) 223 | } 224 | 225 | if len(files) > 5 { 226 | return "", fmt.Errorf("too many bashenv files found (%d). perhaps you've falling into a infinite recursion?", len(files)) 227 | } 228 | 229 | file, err := ioutil.TempFile(c.Dir, "bashenv.") 230 | if err != nil { 231 | return "", err 232 | } 233 | defer file.Close() 234 | 235 | c.printEnv(file, interactive) 236 | 237 | return file.Name(), nil 238 | } 239 | 240 | func (c *App) runInteractiveShell(ctx context.Context) (int, error) { 241 | var interactive bool 242 | 243 | osFile, isOsFile := context.Stdin(ctx).(*os.File) 244 | if isOsFile { 245 | interactive = terminal.IsTerminal(int(osFile.Fd())) 246 | } 247 | 248 | return c.runInternal(ctx, interactive, nil, RunConfig{}) 249 | } 250 | 251 | func (c *App) runNonInteractiveShell(ctx context.Context, args []string, cfg RunConfig) (int, error) { 252 | var isCmd bool 253 | 254 | if len(args) > 0 { 255 | if info, _ := os.Stat(args[0]); info == nil { 256 | isCmd = true 257 | } 258 | } 259 | 260 | var bashArgs []string 261 | 262 | if isCmd { 263 | bashArgs = append(bashArgs, "-c") 264 | var bashCmd []string 265 | for _, a := range args { 266 | bashCmd = append(bashCmd, `"`+strings.ReplaceAll(a, `"`, `\"`)+`"`) 267 | } 268 | bashArgs = append(bashArgs, strings.Join(bashCmd, " ")) 269 | } else { 270 | bashArgs = append(bashArgs, args...) 271 | } 272 | 273 | return c.runInternal(ctx, false, bashArgs, cfg) 274 | } 275 | 276 | func (c *App) runInternal(ctx context.Context, interactive bool, args []string, cfg RunConfig) (int, error) { 277 | envfile, err := c.buildEnvfile(interactive) 278 | if err != nil { 279 | return 0, err 280 | } 281 | if !c.Debug { 282 | defer os.Remove(envfile) 283 | } 284 | 285 | signals := make(chan os.Signal, 1) 286 | signal.Notify(signals) 287 | // Avoid receiving "urgent I/O condition" signals 288 | // See https://golang.hateblo.jp/entry/golang-signal-urgent-io-condition 289 | signal.Ignore(syscall.Signal(0x17)) 290 | 291 | bashArgs := []string{} 292 | 293 | if interactive { 294 | bashArgs = append(bashArgs, "--rcfile", envfile) 295 | } 296 | 297 | bashArgs = append(bashArgs, args...) 298 | 299 | // println(fmt.Sprintf("App.run: running %v: bashArgs %v (%d)", args, bashArgs, len(bashArgs))) 300 | 301 | cmd := exec.CommandContext(ctx, c.BashPath, bashArgs...) 302 | cmd.Env = os.Environ() 303 | if !interactive { 304 | cmd.Env = append(cmd.Env, "BASH_ENV="+envfile) 305 | } 306 | cmd.Dir = cfg.Dir 307 | cmd.Env = append(cmd.Env, cfg.Env...) 308 | cmd.Stdin = context.Stdin(ctx) 309 | cmd.Stdout = context.Stdout(ctx) 310 | cmd.Stderr = context.Stderr(ctx) 311 | if err := cmd.Start(); err != nil { 312 | return 0, err 313 | } 314 | errChan := make(chan error, 1) 315 | go func() { 316 | for sig := range signals { 317 | if sig != syscall.SIGCHLD { 318 | // fmt.Fprintf(os.Stderr, "signal received: %v\n", sig) 319 | err = cmd.Process.Signal(sig) 320 | if err != nil { 321 | errChan <- err 322 | } 323 | } 324 | } 325 | }() 326 | go func() { 327 | errChan <- cmd.Wait() 328 | }() 329 | err = <-errChan 330 | return exitStatus(err) 331 | } 332 | 333 | func (app *App) Run(ctx context.Context, args []interface{}, cfg RunConfig) error { 334 | outs := cfg.Outputs 335 | stdout := cfg.Stdout 336 | stderr := cfg.Stderr 337 | 338 | if len(args) == 1 && args[0] == "env" { 339 | app.printEnv(os.Stdout, true) 340 | 341 | return nil 342 | } 343 | 344 | if ctx == nil { 345 | ctx = context.Background() 346 | ctx = context.WithStdin(ctx, os.Stdin) 347 | ctx = context.WithStdout(ctx, os.Stdout) 348 | ctx = context.WithStderr(ctx, os.Stderr) 349 | } 350 | 351 | if stdout.w != nil { 352 | ctx = context.WithStdout(ctx, stdout.w) 353 | } 354 | 355 | if stderr.w != nil { 356 | ctx = context.WithStderr(ctx, stderr.w) 357 | } 358 | 359 | ctx = context.WithVariables(ctx, map[string]interface{}{}) 360 | 361 | if len(args) == 0 { 362 | _, err := app.runInteractiveShell(ctx) 363 | 364 | return err 365 | } 366 | 367 | funExists, err := app.HandleFuncs(ctx, args, outs) 368 | if err != nil { 369 | fmt.Fprintf(context.Stderr(ctx), "%v\n", err) 370 | return err 371 | } 372 | 373 | if funExists { 374 | return nil 375 | } 376 | 377 | var shellArgs []string 378 | for _, v := range args { 379 | if s, ok := v.(string); !ok { 380 | return fmt.Errorf("%v(%T) cannot be converted to string", v, v) 381 | } else { 382 | shellArgs = append(shellArgs, s) 383 | } 384 | } 385 | 386 | _, err = app.runNonInteractiveShell(ctx, shellArgs, cfg) 387 | 388 | return err 389 | } 390 | 391 | func exitStatus(err error) (int, error) { 392 | if err != nil { 393 | if exiterr, ok := err.(*exec.ExitError); ok { 394 | // There is no platform independent way to retrieve 395 | // the exit code, but the following will work on Unix 396 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 397 | if status.ExitStatus() != -1 { 398 | return status.ExitStatus(), err 399 | } else { 400 | // The process hasn't exited or was terminated by a signal. 401 | return int(status), err 402 | } 403 | } 404 | } 405 | return 0, err 406 | } 407 | return 0, nil 408 | } 409 | 410 | type FunOption func(*FunOptions) 411 | 412 | type FunOptions struct { 413 | Deps []Dependency 414 | } 415 | 416 | type Dependency struct { 417 | Name interface{} 418 | Args []interface{} 419 | } 420 | 421 | type Fun struct { 422 | Name string 423 | F interface{} 424 | M *reflect.Value 425 | } 426 | 427 | func (fn Fun) Call(ctx context.Context, args []interface{}) ([]reflect.Value, error) { 428 | // switch f := fn.F.(type) { 429 | // case func(Context, []string): 430 | // f(ctx, args) 431 | 432 | // return nil 433 | // default: 434 | 435 | if fn.M != nil { 436 | return CallMethod(ctx, fn.Name, *fn.M, args...) 437 | } 438 | 439 | return CallFunc(ctx, fn.Name, fn.F, args...) 440 | // } 441 | } 442 | 443 | type FunWithOpts struct { 444 | Fun Fun 445 | Opts FunOptions 446 | } 447 | 448 | type Diagnostic struct { 449 | Timestamp time.Time 450 | Message string 451 | } 452 | 453 | func (d Diagnostic) String() string { 454 | ts := d.Timestamp.Format(time.RFC3339) 455 | return fmt.Sprintf("%s\t%s", ts, d.Message) 456 | } 457 | 458 | type Diagnostics []Diagnostic 459 | 460 | func (d Diagnostics) String() string { 461 | var diags []string 462 | for _, a := range d { 463 | diags = append(diags, fmt.Sprintf("%s", a)) 464 | } 465 | summary := strings.Join(diags, ", ") 466 | return summary 467 | } 468 | 469 | type Shell struct { 470 | sync.Mutex 471 | 472 | diags Diagnostics 473 | funcs map[string]FunWithOpts 474 | 475 | sync.Once 476 | 477 | app *App 478 | 479 | additionalCallerSkip int 480 | } 481 | 482 | func (t *Shell) Export(args ...interface{}) { 483 | t.Lock() 484 | defer t.Unlock() 485 | 486 | if t.funcs == nil { 487 | t.funcs = map[string]FunWithOpts{} 488 | } 489 | 490 | var fn interface{} 491 | var opts []FunOption 492 | 493 | funOptionType := reflect.TypeOf(FunOption(func(fo *FunOptions) {})) 494 | 495 | var name string 496 | 497 | for i, a := range args { 498 | aType := reflect.TypeOf(a) 499 | if aType.AssignableTo(funOptionType) { 500 | opts = append(opts, a.(FunOption)) 501 | } else if aType.NumMethod() > 0 { 502 | v := reflect.ValueOf(a) 503 | for i := 0; i < aType.NumMethod(); i++ { 504 | typeM := aType.Method(i) 505 | name := typeM.Name 506 | m := v.Method(i) 507 | t.export(strings.ToLower(name), nil, &m, opts) 508 | } 509 | } else if aType.Kind() == reflect.Struct || aType.Kind() == reflect.Ptr { 510 | panic("struct must have one or more public functions to exported") 511 | } else { 512 | if i == 0 { 513 | s, ok := a.(string) 514 | if ok { 515 | name = s 516 | } else { 517 | name = FuncOrMethodToCmdName(a) 518 | fn = a 519 | } 520 | 521 | continue 522 | } 523 | 524 | if fn != nil { 525 | panic("you cannot have two or more fns") 526 | } 527 | 528 | fn = a 529 | } 530 | } 531 | 532 | if fn != nil { 533 | t.export(name, fn, nil, opts) 534 | } 535 | } 536 | 537 | func (t *Shell) export(name string, fn interface{}, m *reflect.Value, opts []FunOption) { 538 | var funOpts FunOptions 539 | 540 | for _, o := range opts { 541 | o(&funOpts) 542 | } 543 | 544 | t.Diagf("registering func %s", name) 545 | 546 | if m != nil { 547 | t.funcs[name] = FunWithOpts{Fun: Fun{Name: name, M: m}, Opts: funOpts} 548 | } else if fn != nil { 549 | t.funcs[name] = FunWithOpts{Fun: Fun{Name: name, F: fn}, Opts: funOpts} 550 | } else { 551 | panic(fmt.Errorf("unexpected args passed to export %s: fn=%v, m=%v, opts=%v", name, fn, m, opts)) 552 | } 553 | } 554 | 555 | func (t *Shell) Diagf(format string, args ...interface{}) { 556 | _, file, line, _ := runtime.Caller(1) 557 | 558 | callerInfo := fmt.Sprintf("%s:%d\t", filepath.Base(file), line) 559 | diag := Diagnostic{Timestamp: time.Now(), Message: callerInfo + fmt.Sprintf(format, args...)} 560 | 561 | t.diags = append(t.diags, diag) 562 | 563 | diagsOut := os.NewFile(3, "diagnostics") 564 | if diagsOut != nil { 565 | fmt.Fprintf(diagsOut, "%s\n", diag) 566 | } 567 | } 568 | 569 | func FuncOrMethodToCmdName(f interface{}) string { 570 | v := reflect.ValueOf(f) 571 | return ReflectValueToCmdName(v) 572 | } 573 | 574 | func ReflectValueToCmdName(v reflect.Value) string { 575 | name := runtime.FuncForPC(v.Pointer()).Name() 576 | vs := strings.Split(name, ".") 577 | 578 | base := vs[len(vs)-1] 579 | 580 | // https://stackoverflow.com/questions/32925344/why-is-there-a-fm-suffix-when-getting-a-functions-name-in-go 581 | if strings.HasSuffix(base, "-fm") { 582 | base = strings.TrimSuffix(base, "-fm") 583 | } 584 | 585 | return strings.ToLower(base) 586 | } 587 | 588 | func Dep(name string, args ...interface{}) FunOption { 589 | return func(o *FunOptions) { 590 | o.Deps = append(o.Deps, Dependency{Name: name, Args: args}) 591 | } 592 | } 593 | 594 | type Command struct { 595 | Vars []interface{} 596 | } 597 | 598 | func Cmd(vars ...interface{}) Command { 599 | return Command{Vars: vars} 600 | } 601 | 602 | func (t *Shell) runPipeline(ctx context.Context, cmds []Command) error { 603 | precedents, final := cmds[:len(cmds)-1], cmds[len(cmds)-1] 604 | 605 | errs := make([]error, len(cmds)) 606 | 607 | var wg sync.WaitGroup 608 | 609 | for i := range precedents { 610 | i := i 611 | var errCh <-chan error 612 | ctx, errCh = t.Pipe(ctx, precedents[i].Vars...) 613 | wg.Add(1) 614 | go func() { 615 | errs[i] = <-errCh 616 | wg.Done() 617 | }() 618 | } 619 | 620 | var errCh <-chan error 621 | errCh = t.GoRun(ctx, final.Vars...) 622 | wg.Add(1) 623 | go func() { 624 | errs[len(cmds)-1] = <-errCh 625 | wg.Done() 626 | }() 627 | 628 | wg.Wait() 629 | 630 | for i, err := range errs { 631 | if err != nil { 632 | return fmt.Errorf("command %v at index %d failed: %v", cmds[i].Vars, i, err) 633 | } 634 | } 635 | 636 | return nil 637 | } 638 | 639 | type Output struct { 640 | value reflect.Value 641 | } 642 | 643 | func Out(p interface{}) Output { 644 | return Output{value: reflect.ValueOf(p).Elem()} 645 | } 646 | 647 | func Env(v ...string) RunOption { 648 | return func(rc *RunConfig) { 649 | for _, v := range v { 650 | rc.Env = append(rc.Env, v) 651 | } 652 | } 653 | } 654 | 655 | func Dir(dir string) RunOption { 656 | return func(rc *RunConfig) { 657 | rc.Dir = dir 658 | } 659 | } 660 | 661 | type StdoutSink struct { 662 | w io.Writer 663 | } 664 | 665 | func WriteStdout(w io.Writer) RunOption { 666 | return func(rc *RunConfig) { 667 | rc.Stdout = StdoutSink{ 668 | w: w, 669 | } 670 | } 671 | } 672 | 673 | type StderrSink struct { 674 | w io.Writer 675 | } 676 | 677 | func WriteStderr(w io.Writer) RunOption { 678 | return func(rc *RunConfig) { 679 | rc.Stderr = StderrSink{ 680 | w: w, 681 | } 682 | } 683 | } 684 | 685 | type RunOption func(*RunConfig) 686 | 687 | type RunConfig struct { 688 | Outputs []Output 689 | Stdout StdoutSink 690 | Stderr StderrSink 691 | Env []string 692 | Dir string 693 | } 694 | 695 | func (t *Shell) MustExec(osArgs []string) { 696 | var args []interface{} 697 | for _, a := range osArgs[1:] { 698 | args = append(args, a) 699 | } 700 | 701 | t.additionalCallerSkip = 1 702 | 703 | if err := t.Run(args...); err != nil { 704 | log.Fatal(err) 705 | } 706 | } 707 | 708 | func (t *Shell) Run(vars ...interface{}) error { 709 | var args []interface{} 710 | var ctx context.Context 711 | var cmds []Command 712 | var testCtx *testing.T 713 | var rc RunConfig 714 | for _, v := range vars { 715 | switch typed := v.(type) { 716 | case context.Context: 717 | ctx = typed 718 | case string: 719 | args = append(args, typed) 720 | case []string: 721 | args = append(args, typed) 722 | case Command: 723 | cmds = append(cmds, typed) 724 | case Output: 725 | rc.Outputs = append(rc.Outputs, typed) 726 | case StdoutSink: 727 | rc.Stdout = typed 728 | case StderrSink: 729 | rc.Stderr = typed 730 | case RunOption: 731 | typed(&rc) 732 | case *testing.T: 733 | testCtx = typed 734 | default: 735 | if reflect.TypeOf(v).Kind() == reflect.Func { 736 | args = append(args, FuncOrMethodToCmdName(v)) 737 | continue 738 | } 739 | 740 | args = append(args, typed) 741 | // panic(fmt.Errorf("unexpected vars: %v", vars)) 742 | } 743 | } 744 | 745 | if ctx == nil { 746 | ctx = context.Background() 747 | } 748 | 749 | if rc.Stdout.w != nil { 750 | ctx = context.WithStdout(ctx, rc.Stdout.w) 751 | } 752 | 753 | if rc.Stderr.w != nil { 754 | ctx = context.WithStderr(ctx, rc.Stderr.w) 755 | } 756 | 757 | if testCtx != nil { 758 | ctx = context.WithValue(ctx, testingTKey{}, testCtx) 759 | } 760 | 761 | t.Diagf("Running %v", args) 762 | 763 | var initErr error 764 | 765 | t.Once.Do(func() { 766 | ex, err := os.Executable() 767 | if err != nil { 768 | println(err.Error()) 769 | initErr = err 770 | return 771 | } 772 | 773 | dir, err := os.Getwd() 774 | if err != nil { 775 | println(err.Error()) 776 | initErr = err 777 | return 778 | } 779 | 780 | // Without sync.Once, the number should be 1 781 | _, filename, _, _ := runtime.Caller(4 + t.additionalCallerSkip) 782 | 783 | var pkg string 784 | 785 | if _, err := os.Stat(filename); err == nil { 786 | pkg = filepath.Dir(filename) 787 | } 788 | 789 | var selfArgs []string 790 | 791 | const GoshTestNameEnv = "GOSH_TEST_NAME" 792 | 793 | var env []string 794 | 795 | if testCtx != nil { 796 | // selfArgs = append(selfArgs, os.Args[1:]...) 797 | selfArgs = append(selfArgs, "-test.run=^"+testCtx.Name()+"$") 798 | 799 | env = append(env, GoshTestNameEnv+"="+testCtx.Name()) 800 | } else { 801 | // os.Args can be something like the below when run via test 802 | // /tmp/go-build2810781305/b001/arctest.test -test.testlogfile=/tmp/go-build2810781305/b001/testlog.txt -test.paniconexit0 -test.timeout=30s -test.run=^TestAcc$ ::: hello world 803 | // 804 | // It's especially important to set/inherit `-test.run`, but not `-test.paniconexit0`. 805 | // The former is required to correctly redirect the recursive command to the test function that invoked it. 806 | // The latter is required to not pollute the recursively invoked command's stdout/stderr with go test output. 807 | var testRun string 808 | for _, a := range os.Args[1:] { 809 | if strings.HasPrefix(a, "-test.run=") { 810 | testRun = a 811 | break 812 | } 813 | } 814 | 815 | // Needed to only trigger the target command when you run all the go tests 816 | if testRun != "" { 817 | selfArgs = append(selfArgs, testRun) 818 | 819 | env = append(env, GoshTestNameEnv+"="+strings.TrimRight(strings.TrimLeft(strings.TrimLeft(testRun, "-test.run="), "^"), "$")) 820 | } else if strings.HasSuffix(os.Args[0], ".test") { 821 | panic(fmt.Errorf("missing testing.T object in Run() args: %v", args)) 822 | } 823 | } 824 | 825 | t.app = &App{ 826 | funcs: t.funcs, 827 | Pkg: pkg, 828 | BashPath: "/bin/bash", 829 | Dir: dir, 830 | TriggerArg: ":::", 831 | SelfPath: ex, 832 | SelfArgs: selfArgs, 833 | Env: env, 834 | } 835 | }) 836 | 837 | if initErr != nil { 838 | return initErr 839 | } 840 | 841 | if t.app == nil { 842 | return fmt.Errorf("[bug] app is not initialized") 843 | } 844 | 845 | if len(cmds) > 0 { 846 | return t.runPipeline(ctx, cmds) 847 | } 848 | 849 | rc.Env = append(rc.Env, t.app.Env...) 850 | 851 | return t.app.Run(ctx, args, rc) 852 | } 853 | 854 | func (c *App) Dep(args ...interface{}) error { 855 | return nil 856 | } 857 | 858 | func (c *App) DepString(args ...interface{}) (string, error) { 859 | return "", nil 860 | } 861 | 862 | func (c *App) DepStringMap(args ...interface{}) (map[string]string, error) { 863 | return nil, nil 864 | } 865 | 866 | func (sh *Shell) GoRun(ctx context.Context, vars ...interface{}) <-chan error { 867 | err := make(chan error) 868 | 869 | go func() { 870 | vars = append([]interface{}{ctx}, vars...) 871 | e := sh.Run(vars...) 872 | err <- e 873 | }() 874 | 875 | return err 876 | } 877 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gosh 2 | 3 | `gosh` is a framework for operating and extending Linux shells with Go. 4 | 5 | The author started developing `gosh` to: 6 | 7 | - Make it extremely easy to gradually rewrite your complex shell script into a more maintainable equivalent - A Go application 8 | - Make it extremely easy to write a maintainable End-to-End test that involves many shell commands and takes an hour or so to run 9 | 10 | But you can also use it for the following use-cases: 11 | 12 | - Write Go instead of Shell scripts 13 | - Build your project, as an alternative to `make` 14 | - Write Bash functions in Go 15 | - Build your own shell with custom functions written in Go 16 | - Incrementally refactor your Bash scripts with Go 17 | - Test your Bash scripts, as a dependency injection system and a test framework/runner 18 | 19 | For more information, see the respective guides below: 20 | 21 | - [Interactive Shell with Hot Reloading](#interactive-shell-with-hot-reloading) 22 | - [Commands and Pipelines](#commands-and-pipelines) 23 | - [Use as a Build Tool](#use-as-a-build-tool) 24 | - [Go interoperability](#go-interoperability) 25 | - [Automatic Arguments](#automatic-arguments), [Automatic Flags](#automatic-flags) 26 | - [Diagnostic Logging](#diagnostic-logging) 27 | - [`go test` Integration](#go-test-integration) 28 | - [Ginkgo Integration](#ginkgo-integration) 29 | - [Writing End-to-End test](#writing-end-to-end-test) 30 | 31 | Note that `gosh` primarily targets `bash` today. But it can be easily enhanced to support other shells, too. 32 | Any contributions to add more shell supports are always welcomed. 33 | 34 | ## Getting Started 35 | 36 | Get started by writing a Go application provides a shell that has a built-in `hello` function: 37 | 38 | ```go 39 | $ mkdir ./myapp; cat < ./myapp/main.go 40 | package main 41 | 42 | import ( 43 | "os" 44 | 45 | "github.com/mumoshu/gosh" 46 | ) 47 | 48 | func main() { 49 | sh := &gosh.Shell{} 50 | 51 | sh.Export("hello", func(ctx gosh.Context, target string) { 52 | ctx.Stdout().Write([]byte("hello " + target + "\n")) 53 | }) 54 | 55 | sh.MustExec(os.Args) 56 | } 57 | EOS 58 | ``` 59 | 60 | Go-running it takes you into a shell session that has the builtin function: 61 | 62 | ``` 63 | $ go run ./myapp 64 | 65 | bash$ hello world 66 | hello world 67 | ``` 68 | 69 | This shell session rebuilds your command automatically so that you can test the modified version of `hello` function without restarting the session. 70 | 71 | Once you're confident with what you built, you'd want to distribute it. 72 | 73 | Just use a standard `go build` command to create a single executable that provides a `bash` enviroment that provides custom functions: 74 | 75 | ``` 76 | $ go build -o myapp ./myapp 77 | ``` 78 | 79 | You can directly invoke the custom function by providing the command name and arguments like: 80 | 81 | ``` 82 | $ myapp hello world 83 | ``` 84 | 85 | ``` 86 | hello world! 87 | ``` 88 | 89 | As it's a bash in the end, you can run a shell script, with access to the `hello` function written in Go: 90 | 91 | ``` 92 | $ myapp < test.gosh 154 | for ((i=0; i<3; i++)); do 155 | hello world 156 | done 157 | EOS 158 | ``` 159 | 160 | Then you can point it to a file or use redirection to source the script to run, as you would usually do with bash: 161 | 162 | ``` 163 | $ go run ./examples/getting-started test.gosh 164 | $ go run ./examples/getting-started ` to the standard output. 178 | 179 | ```go 180 | sh.Export("hello", func(ctx gosh.Context, target string) { 181 | ctx.Stdout().Write([]byte("hello " + target + "\n")) 182 | }) 183 | ``` 184 | 185 | To invoke `hello`, refer to it like a standard shell function: 186 | 187 | ``` 188 | gosh$ hello world 189 | hello world 190 | ``` 191 | 192 | The interactive shell hot-reloads your Go code. 193 | That is, you can modify the custom function code and just reinvoke `hello`, without restarting the shell. 194 | 195 | For example, we modify the code so that the custom function prints it without another prefix `konnichiwa`: 196 | 197 | ``` 198 | $ code ./examples/getting-started/main.go 199 | ``` 200 | 201 | ```go 202 | sh.Export("hello", func(ctx gosh.Context, target string) { 203 | ctx.Stdout().Write([]byte("konnichiwa " + target + "\n")) 204 | }) 205 | ``` 206 | 207 | Go back to your termiinal and rerun `hello world` to see the code hot-reloaded: 208 | 209 | ``` 210 | gosh$ hello world 211 | konnichiwa world 212 | ``` 213 | 214 | ## Commands and Pipelines 215 | 216 | `gosh` has a convenient helper functions to write command executions and shell pipelines in Go, as easy as you've been in a standard *nix shell like Bash. 217 | 218 | See the [commands](./examples/commands/commands.go) example for more information. 219 | 220 | In the example, we implement `gocat` and `gogrep` in Go, each is the simplest possible alternatives to standard `cat` and `grep`, respectively. 221 | 222 | Running the example takes you into a custom shell session as usual: 223 | 224 | ``` 225 | $ go run ./examples/commands/cmd 226 | ``` 227 | 228 | Create an example input file: 229 | 230 | ```bash 231 | $ cat < input.txt 232 | foo 233 | bar 234 | baz 235 | EOS 236 | ``` 237 | 238 | Now, let's try any of the following combinations of the standard and custom commands and see the output is consistent across runs, which means we've successfully reimplemented `cat` and `grep` in Go. 239 | 240 | ``` 241 | $ cat input.txt | grep bar 242 | $ gocat input.txt | grep bar 243 | $ cat input.txt | gogrep bar 244 | $ gocat input.txt | gogrep bar 245 | ``` 246 | 247 | ## Use as a Build Tool 248 | 249 | As you can seen in our [`project` example](project/build.go), `gosh` has a few utilities to help 250 | using your `gosh` application as a build tool like `make`. 251 | 252 | Let's say you previously had a `Makefile` that looked like this: 253 | 254 | ```makefile 255 | .PHONY: all build test 256 | 257 | all: build test 258 | 259 | build: 260 | go build -o getting-started ./examples/getting-started 261 | 262 | test: 263 | go test ./... 264 | ``` 265 | 266 | You can rewrite it by using some Go code powered by `gosh` that looks like the below: 267 | 268 | ```go 269 | Export("all", Dep("build"), Dep("test"), func() { 270 | 271 | }) 272 | 273 | Export("build", func() { 274 | Run("go", "build", "-o", "getting-started", "./examples/getting-started") 275 | }) 276 | 277 | Export("test", func() { 278 | Run("go", "test", "./...") 279 | }) 280 | ``` 281 | 282 | `Dep` is a function provided by `gosh` to let it run the said command before running the exported function itself. 283 | 284 | So, in the above example, running `all` triggers runs of `build` and `test` beforehand. 285 | 286 | Instead of `make all`, `make build`, and `make test` you used to run, you can now run respective `go run` commands: 287 | 288 | ``` 289 | # Runs a go build 290 | $ go run -tags=project ./project build 291 | 292 | # Runs a go test 293 | $ go run -tags=project ./project test 294 | 295 | # runs test and build 296 | $ go run -tags=project ./project < HELLO WORLD 408 | ``` 409 | 410 | Now, you'd export this to the `gosh`-powered shell by using `Export` as usual: 411 | 412 | ``` 413 | sh.Export(Hello) 414 | ``` 415 | 416 | This makes it available to the custom shell from both the Go side and the shell side. 417 | That is, you can call it from Go using `Run`: 418 | 419 | ```go 420 | sh.Run("hello", "world", Opts{UppserCase: true}) 421 | ``` 422 | 423 | while you can call it from shell using the automatically defined flags: 424 | 425 | ``` 426 | hello world -upper-case=true 427 | ``` 428 | 429 | For compatibility reason, you can actually use a more shell-like syntax when you call it from Go: 430 | 431 | ```go 432 | sh.Run("hello", "world", "-upper-case=true") 433 | ``` 434 | 435 | This magic is driven by you define a struct tag. In the original example, you've seen in the struct: 436 | 437 | ```go 438 | UpperCase bool `flag:"upper-case"` 439 | ``` 440 | 441 | This reads as `the struct field "UpperCase" has a tag named "flag" whose value is set to "upper-case"`. 442 | 443 | `gosh` reads the field along with its tag to use it when you provided some flag-like strings in a function argument where the function parameter expected a struct value. 444 | 445 | This way, you don't need to write a length switch-case or call many Go's `flag` functions or deal with `FlagSet` yourself. `gosh` does it all for you. 446 | 447 | ## Diagnostic Logging 448 | 449 | In case you aren't sure why your custom shell functions and the whole application doesn't work, 450 | try reading diagnostic logs that contains various debugging information from `gosh`. 451 | 452 | To access the diagnostic logs, use the file descriptor `3` to redirect it to an arbitrary destination: 453 | 454 | ``` 455 | $ go run ./examples/getting-started hello world 3>diags.out 456 | 457 | $ cat diags.out 458 | 2021-05-29T05:59:38Z app.go:466 registering func hello 459 | ``` 460 | 461 | You can also emit your own diagnostic logs from your custom functions, standard shell functions, shell snippets, and even another application written in a totally different progmming language. 462 | 463 | If you want to write a diagnostic log message from a custom function written in Go, use the `gosh.Shell.Diagf` function: 464 | 465 | ``` 466 | $ code ./examples/getting-started/main.go 467 | ``` 468 | 469 | ```go 470 | sh.Export("hello", func(ctx gosh.Context, target string) { 471 | // Add the below Diagf call 472 | sh.Diagf("My own debug message someData=%s someNumber=%d", "foobar", 123) 473 | 474 | ctx.Stdout().Write([]byte("hello " + target + "\n")) 475 | }) 476 | ``` 477 | 478 | ``` 479 | $ go run ./examples/getting-started hello world 3>diags.out 480 | 481 | $ cat diags.out 482 | 2021-05-29T06:03:58Z app.go:466 registering func hello 483 | 2021-05-29T06:03:58Z main.go:13 My own debug message someData=foobar someNumber=123 484 | ``` 485 | 486 | It also works with a script, without any surprise: 487 | 488 | ``` 489 | $ go run ./examples/getting-started <diags.out 490 | hello world 491 | hello world 492 | EOS 493 | 494 | $ cat diags.out 495 | 2021/05/29 06:16:57 496 | 2021-05-29T06:16:54Z app.go:466 registering func hello 497 | 2021-05-29T06:16:54Z app.go:466 registering func hello 498 | 2021-05-29T06:16:54Z app.go:466 registering func hello 499 | 2021-05-29T06:16:54Z main.go:13 My own debug message someData=foobar someNumber=123 500 | 2021-05-29T06:16:55Z app.go:466 registering func hello 501 | 2021-05-29T06:16:55Z app.go:466 registering func hello 502 | 2021-05-29T06:16:55Z main.go:13 My own debug message someData=foobar someNumber=123 503 | ``` 504 | 505 | To write a log message from a shell script, just write to fd 3 using a standard shell syntax. 506 | 507 | In Bash, you use `>&3` like: 508 | 509 | ```bash 510 | echo my own debug message >&3 511 | ``` 512 | 513 | To test, you can e.g. Bash [`Here Strings`](https://www.gnu.org/software/bash/manual/html_node/Redirections.html#Here-Strings): 514 | 515 | ``` 516 | $ go run ./examples/getting-started <diags.out 517 | echo my own debug message >&3 518 | EOS 519 | 520 | $ cat diags.out 521 | 2021-05-29T06:08:19Z app.go:466 registering func hello 522 | 2021-05-29T06:08:19Z app.go:466 registering func hello 523 | my own debug message 524 | ``` 525 | 526 | For a bash function, it is as easy as...: 527 | 528 | ``` 529 | $ go run ./examples/getting-started <diags.out 530 | myfunc() { 531 | echo my own debug message from myfunc >&3 532 | } 533 | 534 | myfunc 535 | EOS 536 | 537 | $ cat diags.out 538 | 2021-05-29T06:14:09Z app.go:466 registering func hello 539 | 2021-05-29T06:14:09Z app.go:466 registering func hello 540 | my own debug message from myfunc 541 | ``` 542 | 543 | ## `go test` Integration 544 | 545 | See the [gotest example](./examples/gotest/gotest_test.go) for how to write unit and integration tests against your `gosh` applications, shell scripts, or even a regular command that isn't implemented using `gosh`. 546 | 547 | The below is the most standard structure of an unit test for your `gosh` application: 548 | 549 | ```go 550 | func TestUnit(t *testing.T) { 551 | gotest := gotest.New() 552 | 553 | var stdout bytes.Buffer 554 | 555 | if err := gotest.Run("hello", "world", gosh.WriteStdout(&stdout)); err != nil { 556 | t.Fatal(err) 557 | } 558 | 559 | assert.Equal(t, "hello world\n", stdout.String()) 560 | } 561 | ``` 562 | 563 | In the above example, `gotest.New` is implemented by you to provide an instance of `*gosh.Shell`. You write tests against it by calling the `Run` function, using some helper like `gosh.WriteStdout` to capture what's written to the standard output by your application. 564 | 565 | If you are curious how you would implement `gotest.New`, read on. 566 | 567 | ### Structuring your gosh application for ease of testing 568 | 569 | A recommended approach to structure your `gosh` application is to put everything except the entrypoint to your application to a dedicated package. 570 | 571 | In the `gotest` example, we have two packages: 572 | 573 | - `gotest/cmd` that contains the `main` package 574 | - `gotest` for everything else 575 | 576 | The only source file that exists in the first package is `gotest/cmd/main.go`. 577 | 578 | Basically, It contains only a call to the second package: 579 | 580 | ```go 581 | package main 582 | 583 | func main() { 584 | gotest.MustExec(os.Args) 585 | } 586 | ``` 587 | 588 | The `gotest.MustExec` call refers to the `MustExec` function defined in the second package. 589 | 590 | It looks like: 591 | 592 | ```go 593 | package gotest 594 | 595 | func MustExec(osArgs []string) { 596 | New().MustExec(osArgs) 597 | } 598 | ``` 599 | 600 | `New` is the function that initializes the instance of your `gosh` application: 601 | 602 | ```go 603 | package gotest 604 | 605 | func New() *gosh.Shell { 606 | sh := &gosh.Shell{} 607 | 608 | sh.Export("hello", func(ctx gosh.Context, target string) { 609 | ctx.Stdout().Write([]byte("hello " + target + "\n")) 610 | }) 611 | 612 | return sh 613 | } 614 | ``` 615 | 616 | This way, you can just call `New` to create an instance of your gosh application for testing, and then call `Run` on the application in the test, as you would do while writing the application itself. 617 | 618 | ```go 619 | package gotest_test 620 | 621 | func TestUnit(t *testing.T) { 622 | gotest := gotest.New() 623 | 624 | var stdout bytes.Buffer 625 | 626 | if err := gotest.Run("hello", "world", gosh.WriteStdout(&stdout)); err != nil { 627 | t.Fatal(err) 628 | } 629 | 630 | assert.Equal(t, "hello world\n", stdout.String()) 631 | } 632 | ``` 633 | 634 | ## Ginkgo Integration 635 | 636 | If you find yourself repeating a lot of "test setup" code in your tests or have hard time structuring your tests against a lot of cases, you might find [Ginkgo](https://onsi.github.io/ginkgo/) helpful. 637 | 638 | > Ginkgo is a Go testing framework built to help you efficiently write expressive and comprehensive tests using Behavior-Driven Development (“BDD”) style 639 | > https://onsi.github.io/ginkgo/ 640 | 641 | See the [ginkgotest example](./examples/ginkgotest/ginkgotest_test.go) for how to write integration and End-to-End tests against your `gosh` applications, shell scripts, or even a regular command that isn't implemented using `gosh`. 642 | 643 | The below is the most standard structure of an Ginkgo test for your `gosh` application: 644 | 645 | ```go 646 | var app *gosh.Shell 647 | 648 | func TestAcc(t *testing.T) { 649 | app = ginkgotest.New() 650 | 651 | goshtest.Run(t, app, func() { 652 | RegisterFailHandler(Fail) 653 | RunSpecs(t, "Your App's Suite") 654 | }) 655 | } 656 | 657 | var _ = Describe("Your App", func() { 658 | var ( 659 | config struct { 660 | cmd string 661 | args []interface{} 662 | } 663 | 664 | err error 665 | stdout string 666 | ) 667 | 668 | JustBeforeEach(func() { 669 | var stdoutBuf bytes.Buffer 670 | 671 | var args []interface{} 672 | 673 | args = append(args, config.cmd) 674 | args = append(args, config.args...) 675 | args = append(args, gosh.WriteStdout(&stdoutBuf)) 676 | 677 | err = app.Run(args...) 678 | 679 | stdout = stdoutBuf.String() 680 | }) 681 | 682 | Describe("hello", func() { 683 | BeforeEach(func() { 684 | config.cmd = "hello" 685 | }) 686 | 687 | Context("world", func() { 688 | BeforeEach(func() { 689 | config.args = []interface{}{"world"} 690 | }) 691 | 692 | It("should output \"hello world\"", func() { 693 | Expect(stdout).To(Equal("hello world\n")) 694 | }) 695 | }) 696 | }) 697 | }) 698 | ``` 699 | 700 | In the above example, `gotest.New` is implemented by you to provide an instance of `*gosh.Shell`. You write tests against it by calling the `Run` function, using some helper like `gosh.WriteStdout` to capture what's written to the standard output by your application. 701 | 702 | If you are curious how you would implement `gotest.New`, read [Structuring your gosh application for ease of testing](#structuring-your-gosh-application-for-ease-of-testing). 703 | 704 | The followings are standard functions provided by Ginkgo: 705 | 706 | - RegisterFailHandler 707 | - RunSpecs 708 | - Describe 709 | - JustBeforeEach / BeforeEach 710 | - It 711 | 712 | The followings are standard functions provided by Gomega, which is Ginkgo's preferred test helpers and matchers library. 713 | 714 | - Expect 715 | - Equal 716 | 717 | Please refer to [Ginkgo's official documentation](https://onsi.github.io/ginkgo/) for knowing what each Ginkgo and Gomega functions mean and how to write Ginkgo test scenarios. 718 | 719 | ## Writinng End-to-End test 720 | 721 | As stated in the very beginning of this documentation, one of primary goals of `gosh` is to help writing maintainable End-to-End tests. 722 | 723 | > That use-case is driven by all the features explained so far. So please read the above guides beforehand, or go back to read any of those when you had hard time understanding what's explained in this section. 724 | 725 | 726 | 727 | # Acknowledgements 728 | 729 | `gosh` has been inspired by numerous open-source projects listed below. 730 | 731 | Much appreciation to the authors and the open-source community! 732 | 733 | Task runners: 734 | 735 | - https://github.com/magefile 736 | 737 | *unix pipeline-like things: 738 | 739 | - https://github.com/b4b4r07/go-pipe 740 | - https://github.com/go-pipe/pipe 741 | - https://github.com/urjitbhatia/gopipe 742 | - https://github.com/mattn/go-pipeline 743 | 744 | Misc: 745 | 746 | - https://github.com/taylorflatt/remote-shell 747 | - https://github.com/hashicorp/go-plugin 748 | - https://github.com/a8m/reflect-examples 749 | - https://medium.com/swlh/effective-ginkgo-gomega-b6c28d476a09 750 | - https://medium.com/@william.la.martin/ginkgotchas-yeh-also-gomega-13e39185ec96 751 | - https://github.com/fsnotify/fsnotify 752 | 753 | FD handling: 754 | 755 | - https://gist.github.com/miguelmota/4ac6c2c127b6853593808d9d3bba067f 756 | - https://stackoverflow.com/questions/7082001/how-do-file-descriptors-work 757 | 758 | Shell scripting tips: 759 | 760 | - https://www.cyberciti.biz/faq/bash-for-loop/ 761 | - https://www.gnu.org/software/bash/manual/html_node/Redirections.html#Here-Strings 762 | --------------------------------------------------------------------------------