command.go 4.13 KB
Newer Older
Matt Bell's avatar
Matt Bell committed
1 2 3
package commands

import (
Matt Bell's avatar
Matt Bell committed
4
	"errors"
Matt Bell's avatar
Matt Bell committed
5 6
	"fmt"
	"strings"
7
  "io"
8 9

	u "github.com/jbenet/go-ipfs/util"
Matt Bell's avatar
Matt Bell committed
10 11
)

12 13
var log = u.Logger("command")

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
14 15
// Function is the type of function that Commands use.
// It reads from the Request, and writes results to the Response.
16
type Function func(Request, Response)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
17 18 19

// Command is a runnable command, with input arguments and options (flags).
// It can also have subcommands, to group units of work into sets.
Matt Bell's avatar
Matt Bell committed
20
type Command struct {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
21 22
	Help    string
	Options []Option
23
	Run     Function
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
24

Matt Bell's avatar
Matt Bell committed
25
	subcommands map[string]*Command
Matt Bell's avatar
Matt Bell committed
26 27
}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
28 29
// ErrNotCallable signals a command that cannot be called.
var ErrNotCallable = errors.New("This command can't be called directly. Try one of its subcommands.")
Matt Bell's avatar
Matt Bell committed
30

Matt Bell's avatar
Matt Bell committed
31 32
// Register adds a subcommand
func (c *Command) Register(id string, sub *Command) error {
Matt Bell's avatar
Matt Bell committed
33 34 35 36 37
	if c.subcommands == nil {
		c.subcommands = make(map[string]*Command)
	}

	// check for duplicate option names (only checks downwards)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
38
	if err := checkOptionClashes(globalCommand, c, sub); err != nil {
Matt Bell's avatar
Matt Bell committed
39 40 41
		return err
	}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
42
	if _, found := c.subcommands[id]; found {
Matt Bell's avatar
Matt Bell committed
43 44 45 46 47
		return fmt.Errorf("There is already a subcommand registered with id '%s'", id)
	}

	c.subcommands[id] = sub
	return nil
Matt Bell's avatar
Matt Bell committed
48 49
}

50 51 52 53
// Call invokes the command for the given Request
// Streaming output is written to `out`
func (c *Command) Call(req Request, out io.Writer) Response {
	res := NewResponse(req, out)
Matt Bell's avatar
Matt Bell committed
54

55
	cmds, err := c.Resolve(req.Path())
Matt Bell's avatar
Matt Bell committed
56
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
57
		res.SetError(err, ErrClient)
Matt Bell's avatar
Matt Bell committed
58 59 60
		return res
	}
	cmd := cmds[len(cmds)-1]
Matt Bell's avatar
Matt Bell committed
61

62
	if cmd.Run == nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
63
		res.SetError(ErrNotCallable, ErrClient)
Matt Bell's avatar
Matt Bell committed
64 65
		return res
	}
Matt Bell's avatar
Matt Bell committed
66

67
	options, err := c.GetOptions(req.Path())
Matt Bell's avatar
Matt Bell committed
68
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
69
		res.SetError(err, ErrClient)
Matt Bell's avatar
Matt Bell committed
70 71
		return res
	}
Matt Bell's avatar
Matt Bell committed
72

73
	err = req.ConvertOptions(options)
Matt Bell's avatar
Matt Bell committed
74
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
75
		res.SetError(err, ErrClient)
Matt Bell's avatar
Matt Bell committed
76 77
		return res
	}
Matt Bell's avatar
Matt Bell committed
78

79
	cmd.Run(req, res)
Matt Bell's avatar
Matt Bell committed
80 81

	return res
Matt Bell's avatar
Matt Bell committed
82 83
}

Matt Bell's avatar
Matt Bell committed
84 85
// Resolve gets the subcommands at the given path
func (c *Command) Resolve(path []string) ([]*Command, error) {
Matt Bell's avatar
Matt Bell committed
86 87
	cmds := make([]*Command, len(path)+1)
	cmds[0] = c
Matt Bell's avatar
Matt Bell committed
88

Matt Bell's avatar
Matt Bell committed
89 90
	cmd := c
	for i, name := range path {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
91
		cmd = cmd.Subcommand(name)
Matt Bell's avatar
Matt Bell committed
92

Matt Bell's avatar
Matt Bell committed
93 94 95 96
		if cmd == nil {
			pathS := strings.Join(path[0:i], "/")
			return nil, fmt.Errorf("Undefined command: '%s'", pathS)
		}
Matt Bell's avatar
Matt Bell committed
97

Matt Bell's avatar
Matt Bell committed
98 99
		cmds[i+1] = cmd
	}
Matt Bell's avatar
Matt Bell committed
100

Matt Bell's avatar
Matt Bell committed
101
	return cmds, nil
Matt Bell's avatar
Matt Bell committed
102 103
}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
104
// Get resolves and returns the Command addressed by path
Matt Bell's avatar
Matt Bell committed
105
func (c *Command) Get(path []string) (*Command, error) {
Matt Bell's avatar
Matt Bell committed
106 107 108 109 110
	cmds, err := c.Resolve(path)
	if err != nil {
		return nil, err
	}
	return cmds[len(cmds)-1], nil
Matt Bell's avatar
Matt Bell committed
111 112
}

113 114
// GetOptions gets the options in the given path of commands
func (c *Command) GetOptions(path []string) (map[string]Option, error) {
Matt Bell's avatar
Matt Bell committed
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
	options := make([]Option, len(c.Options))
	copy(options, c.Options)
	options = append(options, globalOptions...)

	cmds, err := c.Resolve(path)
	if err != nil {
		return nil, err
	}
	for _, cmd := range cmds {
		options = append(options, cmd.Options...)
	}

	optionsMap := make(map[string]Option)
	for _, opt := range options {
		for _, name := range opt.Names {
			optionsMap[name] = opt
		}
	}

	return optionsMap, nil
135 136
}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
137 138
// Subcommand returns the subcommand with the given id
func (c *Command) Subcommand(id string) *Command {
Matt Bell's avatar
Matt Bell committed
139
	return c.subcommands[id]
Matt Bell's avatar
Matt Bell committed
140
}
141

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
142 143 144 145
// AddOptionNames returns a map of all command options names, and the command
// they belong to. Will error if names clash in the command hierarchy.
func AddOptionNames(c *Command, names map[string]*Command) error {

Matt Bell's avatar
Matt Bell committed
146 147
	for _, opt := range c.Options {
		for _, name := range opt.Names {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
148 149 150 151 152 153 154
			if c2, found := names[name]; found {

				// option can be reused in same command, but more often than not
				// the clash will be across commands so error out with that, as
				// commands tell us where the problem is
				errstr := "Option name ('%s') used multiple times (%v, %v)"
				return fmt.Errorf(errstr, c2, c)
Matt Bell's avatar
Matt Bell committed
155
			}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
156 157 158 159 160 161 162 163 164 165

			// mark the name as in use
			names[name] = c
		}
	}

	// for every subcommand, recurse
	for _, c2 := range c.subcommands {
		if err := AddOptionNames(c2, names); err != nil {
			return err
Matt Bell's avatar
Matt Bell committed
166 167 168
		}
	}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
169 170 171 172 173 174 175 176 177
	return nil
}

// checkOptionClashes checks all command option names for clashes
func checkOptionClashes(cmds ...*Command) error {
	names := map[string]*Command{}

	for _, c := range cmds {
		if err := AddOptionNames(c, names); err != nil {
Matt Bell's avatar
Matt Bell committed
178 179 180 181 182
			return err
		}
	}

	return nil
183
}