From 957684fae2ba51dd2e33a1b8920053ff4c157111 Mon Sep 17 00:00:00 2001 From: Jesse Bouwman Date: Sat, 21 Aug 2021 11:47:54 -0700 Subject: [PATCH 1/7] Add Command.DisplayCLI method signature Following the approach described in https://github.com/ipfs/go-ipfs-cmds/issues/115, define a new method signature on `Command` that supports full processing of the `Response` object when text encoding is requested. Add an encoding check and dispatch to DisplayCLI in local, http client, and http handler code paths. Unblocks resolution of `encoding` option processing in multiple go-ipfs issues. - https://github.com/ipfs/go-ipfs/issues/7050 json encoding for `ls` - https://github.com/ipfs/go-ipfs/issues/1121 json encoding for `add` - https://github.com/ipfs/go-ipfs/issues/5594 json encoding for `stats bw` --- cli/parse.go | 2 +- cli/run.go | 2 +- command.go | 11 +++++++++++ executor.go | 30 ++++++++++++++++++++++++++++++ http/client.go | 6 ++++++ http/responseemitter.go | 2 +- 6 files changed, 50 insertions(+), 3 deletions(-) diff --git a/cli/parse.go b/cli/parse.go index 0ebe3a8c..e8538867 100644 --- a/cli/parse.go +++ b/cli/parse.go @@ -48,7 +48,7 @@ func Parse(ctx context.Context, input []string, stdin *os.File, root *cmds.Comma // if no encoding was specified by user, default to plaintext encoding // (if command doesn't support plaintext, use JSON instead) if enc := req.Options[cmds.EncLong]; enc == "" { - if req.Command.Encoders != nil && req.Command.Encoders[cmds.Text] != nil { + if req.Command.HasText() { req.SetOption(cmds.EncLong, cmds.Text) } else { req.SetOption(cmds.EncLong, cmds.JSON) diff --git a/cli/run.go b/cli/run.go index 46b68280..eca364e6 100644 --- a/cli/run.go +++ b/cli/run.go @@ -123,7 +123,7 @@ func Run(ctx context.Context, root *cmds.Command, encType := cmds.EncodingType(encTypeStr) // use JSON if text was requested but the command doesn't have a text-encoder - if _, ok := cmd.Encoders[encType]; encType == cmds.Text && !ok { + if encType == cmds.Text && !cmd.HasText() { req.Options[cmds.EncLong] = cmds.JSON } diff --git a/command.go b/command.go index f31b3de2..14852ece 100644 --- a/command.go +++ b/command.go @@ -11,6 +11,7 @@ package cmds import ( "errors" "fmt" + "io" "strings" files "github.com/ipfs/go-ipfs-files" @@ -66,6 +67,11 @@ type Command struct { // encoding. Encoders EncoderMap + // DisplayCLI provides console output in cases requiring + // access to a full response object rather than individual + // result values. It is always run in the local process. + DisplayCLI func(res Response, stdout, stderr io.Writer) error + // Helptext is the command's help text. Helptext HelpText @@ -194,6 +200,11 @@ func (c *Command) Resolve(pth []string) ([]*Command, error) { return cmds, nil } +// HasText is true if the Command has direct support for text output +func (c *Command) HasText() bool { + return c.DisplayCLI != nil || (c.Encoders != nil && c.Encoders[Text] != nil) +} + // Get resolves and returns the Command addressed by path func (c *Command) Get(path []string) (*Command, error) { cmds, err := c.Resolve(path) diff --git a/executor.go b/executor.go index 5bcfb7a5..9ea40406 100644 --- a/executor.go +++ b/executor.go @@ -2,6 +2,9 @@ package cmds import ( "context" + "io" + "io/ioutil" + "os" ) type Executor interface { @@ -32,6 +35,33 @@ type executor struct { root *Command } +// GetLocalEncoder provides special treatment for text encoding +// when Command.DisplayCLI field is non-nil, by defining an +// Encoder that delegates to a nested emitter that consumes a Response +// and writes to the underlying io.Writer using DisplayCLI. +func GetLocalEncoder(req *Request, w io.Writer, def EncodingType) (EncodingType, Encoder, error) { + encType, enc, err := GetEncoder(req, w, def) + if err != nil { + return encType, nil, err + } + + if req.Command.DisplayCLI != nil && encType == Text { + emitter, response := NewChanResponsePair(req) + go req.Command.DisplayCLI(response, w, ioutil.Discard) + return encType, &emitterEncoder{emitter: emitter}, nil + } + + return encType, enc, nil +} + +type emitterEncoder struct { + emitter ResponseEmitter +} + +func (enc *emitterEncoder) Encode(value interface{}) error { + return enc.emitter.Emit(value) +} + func (x *executor) Execute(req *Request, re ResponseEmitter, env Environment) error { cmd := req.Command diff --git a/http/client.go b/http/client.go index d19c9fb9..0a0589e4 100644 --- a/http/client.go +++ b/http/client.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "net/url" + "os" "strings" "github.com/ipfs/go-ipfs-cmds" @@ -132,6 +133,11 @@ func (c *client) Execute(req *cmds.Request, re cmds.ResponseEmitter, env cmds.En } } + if cmd.DisplayCLI != nil && + cmds.GetEncoding(req, cmds.Undefined) == cmds.Text { + return cmd.DisplayCLI(res, os.Stdout, os.Stderr) + } + return cmds.Copy(re, res) } diff --git a/http/responseemitter.go b/http/responseemitter.go index e5d424e7..9c99da53 100644 --- a/http/responseemitter.go +++ b/http/responseemitter.go @@ -27,7 +27,7 @@ var ( // NewResponseEmitter returns a new ResponseEmitter. func NewResponseEmitter(w http.ResponseWriter, method string, req *cmds.Request, opts ...ResponseEmitterOption) (ResponseEmitter, error) { - encType, enc, err := cmds.GetEncoder(req, w, cmds.JSON) + encType, enc, err := cmds.GetLocalEncoder(req, w, cmds.JSON) if err != nil { return nil, err } From 16f14e96fcdd563caaa088558d92fa42c2430582 Mon Sep 17 00:00:00 2001 From: Jesse Bouwman Date: Wed, 8 Sep 2021 08:54:48 -0700 Subject: [PATCH 2/7] revert local encoder --- executor.go | 21 --------------------- http/responseemitter.go | 2 +- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/executor.go b/executor.go index 9ea40406..89b3fcf4 100644 --- a/executor.go +++ b/executor.go @@ -2,8 +2,6 @@ package cmds import ( "context" - "io" - "io/ioutil" "os" ) @@ -35,25 +33,6 @@ type executor struct { root *Command } -// GetLocalEncoder provides special treatment for text encoding -// when Command.DisplayCLI field is non-nil, by defining an -// Encoder that delegates to a nested emitter that consumes a Response -// and writes to the underlying io.Writer using DisplayCLI. -func GetLocalEncoder(req *Request, w io.Writer, def EncodingType) (EncodingType, Encoder, error) { - encType, enc, err := GetEncoder(req, w, def) - if err != nil { - return encType, nil, err - } - - if req.Command.DisplayCLI != nil && encType == Text { - emitter, response := NewChanResponsePair(req) - go req.Command.DisplayCLI(response, w, ioutil.Discard) - return encType, &emitterEncoder{emitter: emitter}, nil - } - - return encType, enc, nil -} - type emitterEncoder struct { emitter ResponseEmitter } diff --git a/http/responseemitter.go b/http/responseemitter.go index 9c99da53..e5d424e7 100644 --- a/http/responseemitter.go +++ b/http/responseemitter.go @@ -27,7 +27,7 @@ var ( // NewResponseEmitter returns a new ResponseEmitter. func NewResponseEmitter(w http.ResponseWriter, method string, req *cmds.Request, opts ...ResponseEmitterOption) (ResponseEmitter, error) { - encType, enc, err := cmds.GetLocalEncoder(req, w, cmds.JSON) + encType, enc, err := cmds.GetEncoder(req, w, cmds.JSON) if err != nil { return nil, err } From 11083f096c0c23f3a7c25d0075773c2de237107c Mon Sep 17 00:00:00 2001 From: Jesse Bouwman Date: Wed, 8 Sep 2021 11:53:16 -0700 Subject: [PATCH 3/7] Post-run emitter helper - When both PostRun (output transformer) and DisplayCLI (terminal output) are present, ensure that both are run. `EmitResponse` helper in executor.go is invoked by both local and HTTP client executors: the client fibs the command.Run interface via anonymous function. --- examples/adder/cmd.go | 136 ++++++++++++++++++++++++++++++++++- examples/adder/local/main.go | 27 ++----- executor.go | 47 ++++++++---- http/client.go | 23 +----- 4 files changed, 174 insertions(+), 59 deletions(-) diff --git a/examples/adder/cmd.go b/examples/adder/cmd.go index 58af9577..4f168e63 100644 --- a/examples/adder/cmd.go +++ b/examples/adder/cmd.go @@ -93,7 +93,7 @@ var RootCmd = &cmds.Command{ }), }, }, - // the best UX + // using stdio via PostRun "postRunAdd": { Arguments: []cmds.Argument{ cmds.StringArg("summands", true, true, "values that are supposed to be summed"), @@ -151,6 +151,140 @@ var RootCmd = &cmds.Command{ }, }, }, + // DisplayCLI for terminal control + "displayCliAdd": { + Arguments: []cmds.Argument{ + cmds.StringArg("summands", true, true, "values that are supposed to be summed"), + }, + // this is the same as for encoderAdd + Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { + sum := 0 + + for i, str := range req.Arguments { + num, err := strconv.Atoi(str) + if err != nil { + return err + } + + sum += num + err = re.Emit(&AddStatus{ + Current: sum, + Left: len(req.Arguments) - i - 1, + }) + if err != nil { + return err + } + + time.Sleep(200 * time.Millisecond) + } + return nil + }, + Type: &AddStatus{}, + DisplayCLI: func(res cmds.Response, stdout, stderr io.Writer) error { + defer fmt.Fprintln(stdout) + + // length of line at last iteration + var lastLen int + + for { + v, err := res.Next() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + fmt.Fprint(stdout, "\r" + strings.Repeat(" ", lastLen)) + + s := v.(*AddStatus) + if s.Left > 0 { + lastLen, _ = fmt.Fprintf(stdout, "\rcalculation sum... current: %d; left: %d", s.Current, s.Left) + } else { + lastLen, _ = fmt.Fprintf(stdout, "\rsum is %d.", s.Current) + } + } + }, + }, + // PostRun and DisplayCLI: PostRun intercepts and doubles the sum + "defectiveAdd": { + Arguments: []cmds.Argument{ + cmds.StringArg("summands", true, true, "values that are supposed to be summed"), + }, + // this is the same as for encoderAdd + Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { + sum := 0 + + for i, str := range req.Arguments { + num, err := strconv.Atoi(str) + if err != nil { + return err + } + + sum += num + err = re.Emit(&AddStatus{ + Current: sum, + Left: len(req.Arguments) - i - 1, + }) + if err != nil { + return err + } + + time.Sleep(200 * time.Millisecond) + } + return nil + }, + Type: &AddStatus{}, + PostRun: cmds.PostRunMap{ + cmds.CLI: func(res cmds.Response, re cmds.ResponseEmitter) error { + defer re.Close() + + for { + v, err := res.Next() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + s := v.(*AddStatus) + err = re.Emit(&AddStatus{ + Current: s.Current + s.Current, + Left: s.Left, + }) + if err != nil { + return err + } + } + }, + }, + DisplayCLI: func(res cmds.Response, stdout, stderr io.Writer) error { + defer fmt.Fprintln(stdout) + + // length of line at last iteration + var lastLen int + + for { + v, err := res.Next() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + fmt.Fprint(stdout, "\r" + strings.Repeat(" ", lastLen)) + + s := v.(*AddStatus) + if s.Left > 0 { + lastLen, _ = fmt.Fprintf(stdout, "\rcalculation sum... current: %d; left: %d", s.Current, s.Left) + } else { + lastLen, _ = fmt.Fprintf(stdout, "\rsum is %d.", s.Current) + } + } + }, + }, // how to set program's return value "exitAdd": { Arguments: []cmds.Argument{ diff --git a/examples/adder/local/main.go b/examples/adder/local/main.go index 8e7eeafe..9be5595d 100644 --- a/examples/adder/local/main.go +++ b/examples/adder/local/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "fmt" "os" "github.com/ipfs/go-ipfs-cmds/examples/adder" @@ -26,29 +25,11 @@ func main() { panic(err) } - wait := make(chan struct{}) - var re cmds.ResponseEmitter = cliRe - if pr, ok := req.Command.PostRun[cmds.CLI]; ok { - var ( - res cmds.Response - lower = re - ) - - re, res = cmds.NewChanResponsePair(req) - - go func() { - defer close(wait) - err := pr(res, lower) - if err != nil { - fmt.Println("error: ", err) - } - }() - } else { - close(wait) + exec := cmds.NewExecutor(adder.RootCmd) + err = exec.Execute(req, cliRe, nil) + if err != nil { + panic(err) } - adder.RootCmd.Call(req, re, nil) - <-wait - os.Exit(cliRe.Status()) } diff --git a/executor.go b/executor.go index 89b3fcf4..0339eb05 100644 --- a/executor.go +++ b/executor.go @@ -33,14 +33,6 @@ type executor struct { root *Command } -type emitterEncoder struct { - emitter ResponseEmitter -} - -func (enc *emitterEncoder) Encode(value interface{}) error { - return enc.emitter.Emit(value) -} - func (x *executor) Execute(req *Request, re ResponseEmitter, env Environment) error { cmd := req.Command @@ -59,19 +51,44 @@ func (x *executor) Execute(req *Request, re ResponseEmitter, env Environment) er return err } } + + return EmitResponse(cmd.Run, req, re, env) +} + +// Helper for Execute that handles post-Run emitter logic +func EmitResponse(run Function, req *Request, re ResponseEmitter, env Environment) error { + + // Keep track of the lowest emitter to select the correct + // PostRun method. + lowest := re + cmd := req.Command + + // contains the error returned by DisplayCLI or PostRun + errCh := make(chan error, 1) + + if cmd.DisplayCLI != nil && GetEncoding(req, "json") == "text" { + var res Response + + // This overwrites the emitter provided as an + // argument. Maybe it's better to provide the + // 'DisplayCLI emitter' as an argument to Execute. + re, res = NewChanResponsePair(req) + + go func() { + defer close(errCh) + errCh <- cmd.DisplayCLI(res, os.Stdout, os.Stderr) + }() + } + + maybeStartPostRun := func(formatters PostRunMap) <-chan error { var ( postRun func(Response, ResponseEmitter) error postRunCh = make(chan error) ) - if postRun == nil { - close(postRunCh) - return postRunCh - } - // check if we have a formatter for this emitter type - typer, isTyper := re.(interface { + typer, isTyper := lowest.(interface { Type() PostRunType }) if isTyper && @@ -97,7 +114,7 @@ func (x *executor) Execute(req *Request, re ResponseEmitter, env Environment) er } postRunCh := maybeStartPostRun(cmd.PostRun) - runCloseErr := re.CloseWithError(cmd.Run(req, re, env)) + runCloseErr := re.CloseWithError(run(req, re, env)) postCloseErr := <-postRunCh switch runCloseErr { case ErrClosingClosedEmitter, nil: diff --git a/http/client.go b/http/client.go index 0a0589e4..0a4a3e5a 100644 --- a/http/client.go +++ b/http/client.go @@ -7,7 +7,6 @@ import ( "net" "net/http" "net/url" - "os" "strings" "github.com/ipfs/go-ipfs-cmds" @@ -118,27 +117,11 @@ func (c *client) Execute(req *cmds.Request, re cmds.ResponseEmitter, env cmds.En return err } - if cmd.PostRun != nil { - if typer, ok := re.(interface { - Type() cmds.PostRunType - }); ok && cmd.PostRun[typer.Type()] != nil { - err := cmd.PostRun[typer.Type()](res, re) - closeErr := re.CloseWithError(err) - if closeErr == cmds.ErrClosingClosedEmitter { - // ignore double close errors - return nil - } - - return closeErr - } - } - - if cmd.DisplayCLI != nil && - cmds.GetEncoding(req, cmds.Undefined) == cmds.Text { - return cmd.DisplayCLI(res, os.Stdout, os.Stderr) + copy := func(_ *cmds.Request, re cmds.ResponseEmitter, _ cmds.Environment) error { + return cmds.Copy(re, res) } - return cmds.Copy(re, res) + return cmds.EmitResponse(copy, req, re, env) } func (c *client) toHTTPRequest(req *cmds.Request) (*http.Request, error) { From 9bff5f3e3c18c8e6185e5a9b6b4005808b1ccd3a Mon Sep 17 00:00:00 2001 From: Jesse Bouwman Date: Wed, 8 Sep 2021 13:11:11 -0700 Subject: [PATCH 4/7] Run gofmt --- examples/adder/cmd.go | 4 ++-- executor.go | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/adder/cmd.go b/examples/adder/cmd.go index 4f168e63..79904311 100644 --- a/examples/adder/cmd.go +++ b/examples/adder/cmd.go @@ -195,7 +195,7 @@ var RootCmd = &cmds.Command{ return err } - fmt.Fprint(stdout, "\r" + strings.Repeat(" ", lastLen)) + fmt.Fprint(stdout, "\r"+strings.Repeat(" ", lastLen)) s := v.(*AddStatus) if s.Left > 0 { @@ -274,7 +274,7 @@ var RootCmd = &cmds.Command{ return err } - fmt.Fprint(stdout, "\r" + strings.Repeat(" ", lastLen)) + fmt.Fprint(stdout, "\r"+strings.Repeat(" ", lastLen)) s := v.(*AddStatus) if s.Left > 0 { diff --git a/executor.go b/executor.go index 0339eb05..c0370e68 100644 --- a/executor.go +++ b/executor.go @@ -80,7 +80,6 @@ func EmitResponse(run Function, req *Request, re ResponseEmitter, env Environmen }() } - maybeStartPostRun := func(formatters PostRunMap) <-chan error { var ( postRun func(Response, ResponseEmitter) error From f47dce613df0b17cdf01aa931f1f597e46de126c Mon Sep 17 00:00:00 2001 From: Jesse Bouwman Date: Thu, 23 Sep 2021 13:47:32 -0700 Subject: [PATCH 5/7] Read from display error channel in EmitResponse Close the channel immediately if it will never be used. --- executor.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/executor.go b/executor.go index 1fae0922..44344ea2 100644 --- a/executor.go +++ b/executor.go @@ -78,6 +78,8 @@ func EmitResponse(run Function, req *Request, re ResponseEmitter, env Environmen defer close(errCh) errCh <- cmd.DisplayCLI(res, os.Stdout, os.Stderr) }() + } else { + close(errCh) } maybeStartPostRun := func(formatters PostRunMap) <-chan error { @@ -117,6 +119,8 @@ func EmitResponse(run Function, req *Request, re ResponseEmitter, env Environmen postRunCh := maybeStartPostRun(cmd.PostRun) runCloseErr := re.CloseWithError(run(req, re, env)) postCloseErr := <-postRunCh + displayCloseErr := <-errCh + switch runCloseErr { case ErrClosingClosedEmitter, nil: default: @@ -127,5 +131,5 @@ func EmitResponse(run Function, req *Request, re ResponseEmitter, env Environmen default: return postCloseErr } - return nil + return displayCloseErr } From d3455dfd190b4a8d64d56d4f1ff609c88ee3118a Mon Sep 17 00:00:00 2001 From: Jesse Bouwman Date: Thu, 30 Sep 2021 10:08:08 -0700 Subject: [PATCH 6/7] Relocate DisplayCLI logic from executor to cli ResponseEmitter --- cli/responseemitter.go | 59 ++++++++++++++++++++++++++++++++++++++++++ executor.go | 29 +++------------------ 2 files changed, 62 insertions(+), 26 deletions(-) diff --git a/cli/responseemitter.go b/cli/responseemitter.go index db46a907..2ca1ad76 100644 --- a/cli/responseemitter.go +++ b/cli/responseemitter.go @@ -15,6 +15,17 @@ var _ ResponseEmitter = &responseEmitter{} // NewResponseEmitter constructs a new response emitter that writes results to // the console. func NewResponseEmitter(stdout, stderr io.Writer, req *cmds.Request) (ResponseEmitter, error) { + + if req.Command != nil && req.Command.DisplayCLI != nil && cmds.GetEncoding(req, "json") == "text" { + re, res := cmds.NewChanResponsePair(req) + + go func() { + req.Command.DisplayCLI(res, os.Stdout, os.Stderr) + }() + + return &displayResponseEmitter{re: re}, nil + } + encType, enc, err := cmds.GetEncoder(req, stdout, cmds.TextNewline) return &responseEmitter{ @@ -25,6 +36,54 @@ func NewResponseEmitter(stdout, stderr io.Writer, req *cmds.Request) (ResponseEm }, err } +// displayResponseEmitter implements cli.ResponseEmitter, for +// delegating to a cmd.ResponseEmitter instance when +// Command.DisplayCLI is defined +type displayResponseEmitter struct { + l sync.Mutex + stdout io.Writer + stderr io.Writer + + re cmds.ResponseEmitter + exit int +} + +func (dre *displayResponseEmitter) Close() error { + return dre.re.Close() +} + +func (dre *displayResponseEmitter) CloseWithError(err error) error { + return dre.re.CloseWithError(err) +} + +func (dre *displayResponseEmitter) Emit(v interface{}) error { + return dre.re.Emit(v) +} + +func (dre *displayResponseEmitter) SetLength(l uint64) { + dre.re.SetLength(l) +} + +func (dre *displayResponseEmitter) SetStatus(code int) { + dre.l.Lock() + defer dre.l.Unlock() + dre.exit = code +} + +func (dre *displayResponseEmitter) Status() int { + dre.l.Lock() + defer dre.l.Unlock() + return dre.exit +} + +func (dre *displayResponseEmitter) Stderr() io.Writer { + return dre.stderr +} + +func (dre *displayResponseEmitter) Stdout() io.Writer { + return dre.stdout +} + // ResponseEmitter extends cmds.ResponseEmitter to give better control over the command line type ResponseEmitter interface { cmds.ResponseEmitter diff --git a/executor.go b/executor.go index 44344ea2..c0f536dd 100644 --- a/executor.go +++ b/executor.go @@ -1,9 +1,6 @@ package cmds -import ( - "context" - "os" -) +import "context" type Executor interface { Execute(req *Request, re ResponseEmitter, env Environment) error @@ -63,25 +60,6 @@ func EmitResponse(run Function, req *Request, re ResponseEmitter, env Environmen lowest := re cmd := req.Command - // contains the error returned by DisplayCLI or PostRun - errCh := make(chan error, 1) - - if cmd.DisplayCLI != nil && GetEncoding(req, "json") == "text" { - var res Response - - // This overwrites the emitter provided as an - // argument. Maybe it's better to provide the - // 'DisplayCLI emitter' as an argument to Execute. - re, res = NewChanResponsePair(req) - - go func() { - defer close(errCh) - errCh <- cmd.DisplayCLI(res, os.Stdout, os.Stderr) - }() - } else { - close(errCh) - } - maybeStartPostRun := func(formatters PostRunMap) <-chan error { var ( postRun func(Response, ResponseEmitter) error @@ -119,8 +97,7 @@ func EmitResponse(run Function, req *Request, re ResponseEmitter, env Environmen postRunCh := maybeStartPostRun(cmd.PostRun) runCloseErr := re.CloseWithError(run(req, re, env)) postCloseErr := <-postRunCh - displayCloseErr := <-errCh - + switch runCloseErr { case ErrClosingClosedEmitter, nil: default: @@ -131,5 +108,5 @@ func EmitResponse(run Function, req *Request, re ResponseEmitter, env Environmen default: return postCloseErr } - return displayCloseErr + return nil } From bce8c6541e43b1898d41ed250277c8790362bffd Mon Sep 17 00:00:00 2001 From: Jesse Bouwman Date: Thu, 30 Sep 2021 10:25:52 -0700 Subject: [PATCH 7/7] Don't ignore the error returned by DisplayCLI --- cli/responseemitter.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cli/responseemitter.go b/cli/responseemitter.go index 2ca1ad76..82f0d825 100644 --- a/cli/responseemitter.go +++ b/cli/responseemitter.go @@ -18,12 +18,16 @@ func NewResponseEmitter(stdout, stderr io.Writer, req *cmds.Request) (ResponseEm if req.Command != nil && req.Command.DisplayCLI != nil && cmds.GetEncoding(req, "json") == "text" { re, res := cmds.NewChanResponsePair(req) + dre := &displayResponseEmitter{re: re} go func() { - req.Command.DisplayCLI(res, os.Stdout, os.Stderr) + err := req.Command.DisplayCLI(res, os.Stdout, os.Stderr) + if err != nil { + dre.CloseWithError(err) + } }() - return &displayResponseEmitter{re: re}, nil + return dre, nil } encType, enc, err := cmds.GetEncoder(req, stdout, cmds.TextNewline)