Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}

// 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:
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