diff --git a/cli/parse.go b/cli/parse.go index 238ddf77..8ebf3e12 100644 --- a/cli/parse.go +++ b/cli/parse.go @@ -49,7 +49,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/responseemitter.go b/cli/responseemitter.go index db46a907..82f0d825 100644 --- a/cli/responseemitter.go +++ b/cli/responseemitter.go @@ -15,6 +15,21 @@ 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) + dre := &displayResponseEmitter{re: re} + + go func() { + err := req.Command.DisplayCLI(res, os.Stdout, os.Stderr) + if err != nil { + dre.CloseWithError(err) + } + }() + + return dre, nil + } + encType, enc, err := cmds.GetEncoder(req, stdout, cmds.TextNewline) return &responseEmitter{ @@ -25,6 +40,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/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 d1472c61..ca869e19 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 @@ -210,6 +216,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/examples/adder/cmd.go b/examples/adder/cmd.go index 58af9577..79904311 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 63a7fc44..c0f536dd 100644 --- a/executor.go +++ b/executor.go @@ -1,8 +1,6 @@ package cmds -import ( - "context" -) +import "context" type Executor interface { Execute(req *Request, re ResponseEmitter, env Environment) error @@ -50,6 +48,18 @@ 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 + maybeStartPostRun := func(formatters PostRunMap) <-chan error { var ( postRun func(Response, ResponseEmitter) error @@ -57,7 +67,7 @@ func (x *executor) Execute(req *Request, re ResponseEmitter, env Environment) er ) // Check if we have a formatter for this emitter type. - typer, isTyper := re.(interface { + typer, isTyper := lowest.(interface { Type() PostRunType }) if isTyper { @@ -85,8 +95,9 @@ 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: default: diff --git a/http/client.go b/http/client.go index 27c2bf03..7b1bf706 100644 --- a/http/client.go +++ b/http/client.go @@ -128,22 +128,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 - } + 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) {