-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #150 from jbenet/commands
Command Interface
- Loading branch information
Showing
8 changed files
with
755 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package cli | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/jbenet/go-ipfs/commands" | ||
) | ||
|
||
// Parse parses the input commandline string (cmd, flags, and args). | ||
// returns the corresponding command Request object. | ||
func Parse(input []string, root *commands.Command) (commands.Request, error) { | ||
path, input := parsePath(input, root) | ||
opts, args, err := parseOptions(input) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return commands.NewRequest(path, opts, args, nil), nil | ||
} | ||
|
||
// parsePath gets the command path from the command line input | ||
func parsePath(input []string, root *commands.Command) ([]string, []string) { | ||
cmd := root | ||
i := 0 | ||
|
||
for _, blob := range input { | ||
if strings.HasPrefix(blob, "-") { | ||
break | ||
} | ||
|
||
cmd := cmd.Subcommand(blob) | ||
if cmd == nil { | ||
break | ||
} | ||
|
||
i++ | ||
} | ||
|
||
return input[:i], input[i:] | ||
} | ||
|
||
// parseOptions parses the raw string values of the given options | ||
// returns the parsed options as strings, along with the CLI args | ||
func parseOptions(input []string) (map[string]interface{}, []string, error) { | ||
opts := make(map[string]interface{}) | ||
args := []string{} | ||
|
||
for i := 0; i < len(input); i++ { | ||
blob := input[i] | ||
|
||
if strings.HasPrefix(blob, "-") { | ||
name := blob[1:] | ||
value := "" | ||
|
||
// support single and double dash | ||
if strings.HasPrefix(name, "-") { | ||
name = name[1:] | ||
} | ||
|
||
if strings.Contains(name, "=") { | ||
split := strings.SplitN(name, "=", 2) | ||
name = split[0] | ||
value = split[1] | ||
} | ||
|
||
if _, ok := opts[name]; ok { | ||
return nil, nil, fmt.Errorf("Duplicate values for option '%s'", name) | ||
} | ||
|
||
opts[name] = value | ||
|
||
} else { | ||
args = append(args, blob) | ||
} | ||
} | ||
|
||
return opts, args, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package cli | ||
|
||
import ( | ||
//"fmt" | ||
"testing" | ||
|
||
"github.com/jbenet/go-ipfs/commands" | ||
) | ||
|
||
func TestOptionParsing(t *testing.T) { | ||
cmd := &commands.Command{ | ||
Options: []commands.Option{ | ||
commands.Option{Names: []string{"b"}, Type: commands.String}, | ||
}, | ||
Subcommands: map[string]*commands.Command{ | ||
"test": &commands.Command{}, | ||
}, | ||
} | ||
|
||
opts, input, err := parseOptions([]string{"--beep", "-boop=lol", "test2", "-c", "beep", "--foo=5"}) | ||
/*for k, v := range opts { | ||
fmt.Printf("%s: %s\n", k, v) | ||
} | ||
fmt.Printf("%s\n", input)*/ | ||
if err != nil { | ||
t.Error("Should have passed") | ||
} | ||
if len(opts) != 4 || opts["beep"] != "" || opts["boop"] != "lol" || opts["c"] != "" || opts["foo"] != "5" { | ||
t.Error("Returned options were defferent than expected: %v", opts) | ||
} | ||
if len(input) != 2 || input[0] != "test2" || input[1] != "beep" { | ||
t.Error("Returned input was different than expected: %v", input) | ||
} | ||
|
||
_, _, err = parseOptions([]string{"-beep=1", "-boop=2", "-beep=3"}) | ||
if err == nil { | ||
t.Error("Should have failed (duplicate option name)") | ||
} | ||
|
||
path, args := parsePath([]string{"test", "beep", "boop"}, cmd) | ||
if len(path) != 1 || path[0] != "test" { | ||
t.Error("Returned path was defferent than expected: %v", path) | ||
} | ||
if len(args) != 2 || args[0] != "beep" || args[1] != "boop" { | ||
t.Error("Returned args were different than expected: %v", args) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package commands | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"strings" | ||
|
||
u "github.com/jbenet/go-ipfs/util" | ||
) | ||
|
||
var log = u.Logger("command") | ||
|
||
// Function is the type of function that Commands use. | ||
// It reads from the Request, and writes results to the Response. | ||
type Function func(Request, Response) | ||
|
||
// Command is a runnable command, with input arguments and options (flags). | ||
// It can also have Subcommands, to group units of work into sets. | ||
type Command struct { | ||
Help string | ||
Options []Option | ||
Run Function | ||
Subcommands map[string]*Command | ||
} | ||
|
||
// ErrNotCallable signals a command that cannot be called. | ||
var ErrNotCallable = errors.New("This command can't be called directly. Try one of its subcommands.") | ||
|
||
// Call invokes the command for the given Request | ||
func (c *Command) Call(req Request) Response { | ||
res := NewResponse(req) | ||
|
||
cmds, err := c.Resolve(req.Path()) | ||
if err != nil { | ||
res.SetError(err, ErrClient) | ||
return res | ||
} | ||
cmd := cmds[len(cmds)-1] | ||
|
||
if cmd.Run == nil { | ||
res.SetError(ErrNotCallable, ErrClient) | ||
return res | ||
} | ||
|
||
options, err := c.GetOptions(req.Path()) | ||
if err != nil { | ||
res.SetError(err, ErrClient) | ||
return res | ||
} | ||
|
||
err = req.ConvertOptions(options) | ||
if err != nil { | ||
res.SetError(err, ErrClient) | ||
return res | ||
} | ||
|
||
cmd.Run(req, res) | ||
|
||
return res | ||
} | ||
|
||
// Resolve gets the subcommands at the given path | ||
func (c *Command) Resolve(path []string) ([]*Command, error) { | ||
cmds := make([]*Command, len(path)+1) | ||
cmds[0] = c | ||
|
||
cmd := c | ||
for i, name := range path { | ||
cmd = cmd.Subcommand(name) | ||
|
||
if cmd == nil { | ||
pathS := strings.Join(path[0:i], "/") | ||
return nil, fmt.Errorf("Undefined command: '%s'", pathS) | ||
} | ||
|
||
cmds[i+1] = cmd | ||
} | ||
|
||
return cmds, nil | ||
} | ||
|
||
// Get resolves and returns the Command addressed by path | ||
func (c *Command) Get(path []string) (*Command, error) { | ||
cmds, err := c.Resolve(path) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return cmds[len(cmds)-1], nil | ||
} | ||
|
||
// GetOptions gets the options in the given path of commands | ||
func (c *Command) GetOptions(path []string) (map[string]Option, error) { | ||
options := make([]Option, len(c.Options)) | ||
|
||
cmds, err := c.Resolve(path) | ||
if err != nil { | ||
return nil, err | ||
} | ||
cmds = append(cmds, globalCommand) | ||
|
||
for _, cmd := range cmds { | ||
options = append(options, cmd.Options...) | ||
} | ||
|
||
optionsMap := make(map[string]Option) | ||
for _, opt := range options { | ||
for _, name := range opt.Names { | ||
if _, found := optionsMap[name]; found { | ||
return nil, fmt.Errorf("Option name '%s' used multiple times", name) | ||
} | ||
|
||
optionsMap[name] = opt | ||
} | ||
} | ||
|
||
return optionsMap, nil | ||
} | ||
|
||
// Subcommand returns the subcommand with the given id | ||
func (c *Command) Subcommand(id string) *Command { | ||
return c.Subcommands[id] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
package commands | ||
|
||
import "testing" | ||
|
||
func TestOptionValidation(t *testing.T) { | ||
cmd := Command{ | ||
Options: []Option{ | ||
Option{[]string{"b", "beep"}, Int}, | ||
Option{[]string{"B", "boop"}, String}, | ||
}, | ||
Run: func(req Request, res Response) {}, | ||
} | ||
|
||
req := NewEmptyRequest() | ||
req.SetOption("foo", 5) | ||
res := cmd.Call(req) | ||
if res.Error() == nil { | ||
t.Error("Should have failed (unrecognized option)") | ||
} | ||
|
||
req = NewEmptyRequest() | ||
req.SetOption("beep", 5) | ||
req.SetOption("b", 10) | ||
res = cmd.Call(req) | ||
if res.Error() == nil { | ||
t.Error("Should have failed (duplicate options)") | ||
} | ||
|
||
req = NewEmptyRequest() | ||
req.SetOption("beep", "foo") | ||
res = cmd.Call(req) | ||
if res.Error() == nil { | ||
t.Error("Should have failed (incorrect type)") | ||
} | ||
|
||
req = NewEmptyRequest() | ||
req.SetOption("beep", 5) | ||
res = cmd.Call(req) | ||
if res.Error() != nil { | ||
t.Error(res.Error(), "Should have passed") | ||
} | ||
|
||
req = NewEmptyRequest() | ||
req.SetOption("beep", 5) | ||
req.SetOption("boop", "test") | ||
res = cmd.Call(req) | ||
if res.Error() != nil { | ||
t.Error("Should have passed") | ||
} | ||
|
||
req = NewEmptyRequest() | ||
req.SetOption("b", 5) | ||
req.SetOption("B", "test") | ||
res = cmd.Call(req) | ||
if res.Error() != nil { | ||
t.Error("Should have passed") | ||
} | ||
|
||
req = NewEmptyRequest() | ||
req.SetOption(EncShort, "json") | ||
res = cmd.Call(req) | ||
if res.Error() != nil { | ||
t.Error("Should have passed") | ||
} | ||
|
||
req = NewEmptyRequest() | ||
req.SetOption("b", "100") | ||
res = cmd.Call(req) | ||
if res.Error() != nil { | ||
t.Error("Should have passed") | ||
} | ||
|
||
req = NewEmptyRequest() | ||
req.SetOption("b", ":)") | ||
res = cmd.Call(req) | ||
if res.Error == nil { | ||
t.Error(res.Error, "Should have failed (string value not convertible to int)") | ||
} | ||
} | ||
|
||
func TestRegistration(t *testing.T) { | ||
noop := func(req Request, res Response) {} | ||
|
||
cmdA := &Command{ | ||
Options: []Option{ | ||
Option{[]string{"beep"}, Int}, | ||
}, | ||
Run: noop, | ||
} | ||
|
||
cmdB := &Command{ | ||
Options: []Option{ | ||
Option{[]string{"beep"}, Int}, | ||
}, | ||
Run: noop, | ||
Subcommands: map[string]*Command{ | ||
"a": cmdA, | ||
}, | ||
} | ||
|
||
cmdC := &Command{ | ||
Options: []Option{ | ||
Option{[]string{"encoding"}, String}, | ||
}, | ||
Run: noop, | ||
} | ||
|
||
res := cmdB.Call(NewRequest([]string{"a"}, nil, nil, nil)) | ||
if res.Error() == nil { | ||
t.Error("Should have failed (option name collision)") | ||
} | ||
|
||
res = cmdC.Call(NewEmptyRequest()) | ||
if res.Error() == nil { | ||
t.Error("Should have failed (option name collision with global options)") | ||
} | ||
} | ||
|
||
func TestResolving(t *testing.T) { | ||
cmdC := &Command{} | ||
cmdB := &Command{ | ||
Subcommands: map[string]*Command{ | ||
"c": cmdC, | ||
}, | ||
} | ||
cmdB2 := &Command{} | ||
cmdA := &Command{ | ||
Subcommands: map[string]*Command{ | ||
"b": cmdB, | ||
"B": cmdB2, | ||
}, | ||
} | ||
cmd := &Command{ | ||
Subcommands: map[string]*Command{ | ||
"a": cmdA, | ||
}, | ||
} | ||
|
||
cmds, err := cmd.Resolve([]string{"a", "b", "c"}) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
if len(cmds) != 4 || cmds[0] != cmd || cmds[1] != cmdA || cmds[2] != cmdB || cmds[3] != cmdC { | ||
t.Error("Returned command path is different than expected", cmds) | ||
} | ||
} |
Oops, something went wrong.