Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI and API command output encoding #215

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion cli/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
63 changes: 63 additions & 0 deletions cli/responseemitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. This isn't going to work:

  1. The caller will send an error to dre via CloseWithError.
  2. DisplayCLI will get this error from res, and return it.
  3. We're then going to feed it back into dre here.

Really, we need to expose this error via some method and/or a channel. I'll try to think of the right approach.

}
}()

return dre, nil
}

encType, enc, err := cmds.GetEncoder(req, stdout, cmds.TextNewline)

return &responseEmitter{
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can embed this instead of manually forwarding.

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
}

Comment on lines +67 to +90
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can actually just drop these methods from the interface. But it'll require some investigation.

// ResponseEmitter extends cmds.ResponseEmitter to give better control over the command line
type ResponseEmitter interface {
cmds.ResponseEmitter
Expand Down
2 changes: 1 addition & 1 deletion cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
11 changes: 11 additions & 0 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package cmds
import (
"errors"
"fmt"
"io"
"strings"

files "github.com/ipfs/go-ipfs-files"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
136 changes: 135 additions & 1 deletion examples/adder/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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{
Expand Down
27 changes: 4 additions & 23 deletions examples/adder/local/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"context"
"fmt"
"os"

"github.com/ipfs/go-ipfs-cmds/examples/adder"
Expand All @@ -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())
}
21 changes: 16 additions & 5 deletions executor.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package cmds

import (
"context"
)
import "context"

type Executor interface {
Execute(req *Request, re ResponseEmitter, env Environment) error
Expand Down Expand Up @@ -50,14 +48,26 @@ func (x *executor) Execute(req *Request, re ResponseEmitter, env Environment) er
return err
}
}

return EmitResponse(cmd.Run, req, re, env)
jbouwman marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
postRunCh = make(chan error)
)

// Check if we have a formatter for this emitter type.
typer, isTyper := re.(interface {
typer, isTyper := lowest.(interface {
Type() PostRunType
})
if isTyper {
Expand Down Expand Up @@ -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:
Stebalien marked this conversation as resolved.
Show resolved Hide resolved
default:
Expand Down
17 changes: 3 additions & 14 deletions http/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,22 +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
}
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) {
Expand Down