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
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())
}
40 changes: 37 additions & 3 deletions executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmds

import (
"context"
"os"
)

type Executor interface {
Expand Down Expand Up @@ -50,14 +51,45 @@ 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

// 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
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 +117,10 @@ 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
displayCloseErr := <-errCh

switch runCloseErr {
case ErrClosingClosedEmitter, nil:
Stebalien marked this conversation as resolved.
Show resolved Hide resolved
default:
Expand All @@ -97,5 +131,5 @@ func (x *executor) Execute(req *Request, re ResponseEmitter, env Environment) er
default:
return postCloseErr
}
return nil
return displayCloseErr
}
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