-
-
Notifications
You must be signed in to change notification settings - Fork 3k
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
Command Interface #150
Merged
Command Interface #150
Changes from all commits
Commits
Show all changes
73 commits
Select commit
Hold shift + click to select a range
dd2a105
commands: Implemented Command
mappum 30ea427
commands: Created Option struct
mappum 15b7388
commands: Request struct
mappum dd68296
commands: Wrote tests for command option validation
mappum 5b18844
commands: Check for option name collisions
mappum e593c18
commands: Added tests for Command.Register
mappum 47ebf17
commands: Created a list of global options (for options owned by comm…
mappum d7e9afc
commands: Use global options when registering and calling commands
mappum 97ce60f
commands: Added global options list to command tests
mappum f31fd53
commands: Added Response
mappum b2ee05a
commands: Updated Command to use Response for output rather than (int…
mappum df034c9
commands: Updated Command tests for new Response API
mappum d1595ce
commands: Added basic methods to Request
mappum 95b0dd2
commands: Added an Error struct for creating marshallable errors
mappum a3a8437
commands: Added marshalling to Response
mappum 808d9c1
commands: Wrote tests for Response marshalling
mappum 308ee5c
commands: Added Request#SetOption so we can set options with multiple…
mappum 01938ac
commands: Updated Response test to use safer option setting
mappum aa592ce
commands: Added error marshalling to Response
mappum 94ca264
commands: Added test for Response error marshalling
mappum 4367097
commands: Formatted code
mappum bf32818
commands/cli: Added CLI option parsing
mappum b3eecf4
commands/cli: Added simple option parser test
mappum 4bd3a77
commands/cli: Added path/args parsing
mappum f437230
commands/cli: Added path/args test
mappum 1b35615
commands: Made Command#GetOption method, for getting all options for …
mappum 66b0727
commands/cli: Renamed parse functions to parse*
mappum bb32633
commands/cli: Refactored parsing to always get the command path at th…
mappum 08885c0
commands/cli: Fixed tests for refactor
mappum 66e6da3
commands/cli: Added value parsing for single-dash options
mappum 5d9fa93
commands/cli: Added test for single-dash option value
mappum 97b8719
commands/cli: Removed parser string handling since the go runtime han…
mappum 86bc450
commands/cli: Pass option definitions as an argument to parseOptions
mappum 793a8de
commands: Refactored to make Request contain command path
mappum e1a4b8d
commands: Added Request#SetPath method
mappum c575b50
commands: Added option value conversion, and moved option validation …
mappum 1e8719e
commands: Fixed tests
mappum 47eea7f
commands: Added a option validation test for convertible string values
mappum 7a36278
commands: Allow setting Request fields in NewRequest
mappum 968ec34
commands/cli: Made Parse return a Request object instead of separate …
mappum 09311d4
commands: Added 'NewEmptyRequest'
mappum 4b0f44e
commands: Fixed tests
mappum 4af61ad
commands: Added Command#Resolve
mappum c054fb3
commands: Added simple Command#Resolve test
mappum d2176c0
commands: Added Command#Get
mappum 4f06c6f
commands: Formatted code
mappum e5e121a
commands: Made Request#Option also return an existence bool
mappum f87c418
commands/cli: Refactored CLI parsing to match go tooling conventions
mappum b48b12e
commands/cli: Fixed test for new parsing
mappum 117af86
commands/cli: Error if there are duplicate values for an option
mappum a9fa767
commands/cli: Added test for detecting duplicate options
mappum 7673ce6
fmt, lint, + vet commands/
jbenet 09d2277
f -> run, Function type.
jbenet 84fa7bc
AddOptionNames func
jbenet 92528ba
Sub -> Subcommand
jbenet 4986600
parsePath no err
jbenet bbef82f
"enc" -> EncShort
jbenet b10fc2c
turned req + res into interfaces
jbenet c0b28dc
commands: Added input stream field to Request
mappum 7bd7ed6
commands: Added output stream field to Response
mappum b022ba4
commands: Fixed tests
mappum 71ff571
commands/cli: Made Parse return component fields instead of a Request
mappum 4896123
commands: Export command Run function
mappum 8786878
commands: Fixed tests
mappum b65a5ba
commands: Made Error implement error interface
mappum dd84a3e
commands: Got rid of Response#Stream() in favor of setting value to a…
mappum 4f10f03
commands: Fixed tests
mappum 6ff98df
commands: Do command collision check in GetOptions
mappum ca44d0d
commands: Removed Command#Register and exported Subcommands so subcom…
mappum dd81bf6
commands: Fixed tests
mappum d464e3d
commands: go fmt
jbenet 12a6a87
commands/cli: Made Parse return a Request (again)
mappum 4303dcc
commands: Added Request#SetStream
mappum File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 i like this |
||
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
f -> function
(meaningful var name at object level)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addressed