cmd2: simplified main
Attention @maybebtc @mappum I cleaned up + simplified the main flow. Now, all printing is contained inside main itself! (:cheer:). I do this with the help of a cmdInvocation struct that has both a Parse and Run. The only major clunkiness left is that the "CallCommand" is still its own function. But *shrug*. Please test it works as we would expect. i changed much of the flow, so likely that i missed a complicated edge case. main roadmap: - parse the commandline to get a cmdInvocation - if user requests, help, print it and exit. - run the command invocation - output the response - if anything fails, print error, maybe with help
Juan Batiz-Benet committed
Nov 12, 2014 at 17:48 UTC
f738e899c2f25f1b226275b53ab9d2238038e0da
2 files changed
+121
-138
cmd/ipfs2/main.go
+117
-137
@@ -17,7 +17,6 @@ import (
17
cmdsHttp "github.com/jbenet/go-ipfs/commands/http"
18
"github.com/jbenet/go-ipfs/config"
19
"github.com/jbenet/go-ipfs/core"
20
- commands "github.com/jbenet/go-ipfs/core/commands2"
20
daemon "github.com/jbenet/go-ipfs/daemon2"
21
u "github.com/jbenet/go-ipfs/util"
22
)
@@ -34,174 +33,170 @@ const (
33
errorFormat = "ERROR: %v\n\n"
34
)
35
36
+type cmdInvocation struct {
37
+ path []string
38
+ cmd *cmds.Command
39
+ root *cmds.Command
40
+ req cmds.Request
41
+}
42
+
43
+// main roadmap:
44
+// - parse the commandline to get a cmdInvocation
45
+// - if user requests, help, print it and exit.
46
+// - run the command invocation
47
+// - output the response
48
+// - if anything fails, print error, maybe with help
49
func main() {
38
- err := run()
50
+ var invoc cmdInvocation
51
+ var err error
52
+
53
+ // we'll call this local helper to output errors.
54
+ // this is so we control how to print errors in one place.
55
+ printErr := func(err error) {
56
+ fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
57
+ }
58
+
59
+ // this is a local helper to print out help text.
60
+ // there's some considerations that this makes easier.
61
+ printHelp := func(long bool) {
62
+ helpFunc := cmdsCli.ShortHelp
63
+ if long {
64
+ helpFunc = cmdsCli.LongHelp
65
+ }
66
+
67
+ helpFunc("ipfs", invoc.root, invoc.path, os.Stderr)
68
+ }
69
+
70
+ // parse the commandline into a command invocation
71
+ err = invoc.Parse(os.Args[1:])
72
+
73
+ // BEFORE handling the parse error, if we have enough information
74
+ // AND the user requested help, print it out and exit
75
+ if invoc.cmd != nil {
76
+ longH, shortH, err := invoc.requestedHelp()
77
+ if err != nil {
78
+ printErr(err)
79
+ os.Exit(1)
80
+ }
81
+ if longH || shortH {
82
+ printHelp(longH)
83
+ os.Exit(0)
84
+ }
85
+ }
86
+
87
+ // ok now handle handle parse error (which means cli input was wrong,
88
+ // e.g. incorrect number of args, or nonexistent subcommand)
89
if err != nil {
40
- fmt.Println(err)
90
+ printErr(err)
91
+
92
+ // this was a user error, print help.
93
+ if invoc.cmd != nil {
94
+ // we need a newline space.
95
+ fmt.Fprintf(os.Stderr, "\n")
96
+ printHelp(false)
97
+ }
98
os.Exit(1)
99
}
43
-}
100
45
-func run() error {
46
- handleInterrupt()
47
-
48
- args := os.Args[1:]
49
- req, root, err := createRequest(args)
101
+ // ok, finally, run the command invocation.
102
+ output, err := invoc.Run()
103
if err != nil {
51
- // when the error is errOutputHelp, just exit gracefully.
52
- if err == errHelpRequested {
53
- return nil
104
+ printErr(err)
105
+
106
+ // if this error was a client error, print short help too.
107
+ if isClientError(err) {
108
+ printHelp(false)
109
}
55
- return err
110
+ os.Exit(1)
111
}
112
58
- debug, _, err := req.Option("debug").Bool()
113
+ // everything went better than expected :)
114
+ io.Copy(os.Stdout, output)
115
+}
116
+
117
+func (i *cmdInvocation) Run() (output io.Reader, err error) {
118
+ handleInterrupt()
119
+
120
+ // check if user wants to debug. option OR env var.
121
+ debug, _, err := i.req.Option("debug").Bool()
122
if err != nil {
60
- return err
123
+ return nil, err
124
}
62
- if debug {
125
+ if debug || u.GetenvBool("DEBUG") {
126
u.Debug = true
127
u.SetAllLoggers(logging.DEBUG)
128
}
129
130
+ // if debugging, let's profile.
131
+ // TODO maybe change this to its own option... profiling makes it slower.
132
if u.Debug {
133
stopProfilingFunc, err := startProfiling()
134
if err != nil {
70
- return err
135
+ return nil, err
136
}
137
defer stopProfilingFunc() // to be executed as late as possible
138
}
139
75
- helpTextDisplayed, err := handleHelpOption(req, root)
76
- if err != nil {
77
- return err
78
- }
79
- if helpTextDisplayed {
80
- return nil
81
- }
82
-
83
- res, err := callCommand(req, root)
84
- if err != nil {
85
- return err
86
- }
87
-
88
- err = outputResponse(res, root)
140
+ res, err := callCommand(i.req, i.root)
141
if err != nil {
90
- return err
142
+ return nil, err
143
}
144
93
- return nil
145
+ return res.Reader()
146
}
147
96
-func createRequest(args []string) (cmds.Request, *cmds.Command, error) {
97
- req, root, cmd, path, err := cmdsCli.Parse(args, Root)
148
+func (i *cmdInvocation) Parse(args []string) error {
149
+ var err error
150
99
- // handle parse error (which means the commandline input was wrong,
100
- // e.g. incorrect number of args, or nonexistent subcommand)
151
+ i.req, i.root, i.cmd, i.path, err = cmdsCli.Parse(args, Root)
152
if err != nil {
102
- return nil, nil, handleParseError(req, root, cmd, path, err)
153
+ return err
154
}
155
105
- configPath, err := getConfigRoot(req)
156
+ configPath, err := getConfigRoot(i.req)
157
if err != nil {
107
- return nil, nil, err
158
+ return err
159
}
160
161
conf, err := getConfig(configPath)
162
if err != nil {
112
- return nil, nil, err
163
+ return err
164
}
114
- ctx := req.Context()
165
+ ctx := i.req.Context()
166
ctx.ConfigRoot = configPath
167
ctx.Config = conf
168
169
// if no encoding was specified by user, default to plaintext encoding
170
// (if command doesn't support plaintext, use JSON instead)
120
- if !req.Option("encoding").Found() {
121
- if req.Command().Marshallers != nil && req.Command().Marshallers[cmds.Text] != nil {
122
- req.SetOption("encoding", cmds.Text)
171
+ if !i.req.Option("encoding").Found() {
172
+ if i.req.Command().Marshallers != nil && i.req.Command().Marshallers[cmds.Text] != nil {
173
+ i.req.SetOption("encoding", cmds.Text)
174
} else {
124
- req.SetOption("encoding", cmds.JSON)
175
+ i.req.SetOption("encoding", cmds.JSON)
176
}
177
}
178
128
- return req, root, nil
129
-}
130
-
131
-func handleParseError(req cmds.Request, root *cmds.Command, cmd *cmds.Command, path []string, parseError error) error {
132
- var longHelp, shortHelp bool
133
-
134
- if req != nil {
135
- // help and h are defined in the root. We expect them to be bool.
136
- var err error
137
- longHelp, _, err = req.Option("help").Bool()
138
- if err != nil {
139
- return err
140
- }
141
- shortHelp, _, err = req.Option("h").Bool()
142
- if err != nil {
143
- return err
144
- }
145
-
146
- // override the error to avoid signaling other issues.
147
- parseError = errHelpRequested
148
- }
149
-
150
- // if the -help flag wasn't specified, show the error message
151
- // or if a path was returned (user specified a valid subcommand), show the error message
152
- // (this means there was an option or argument error)
153
- if path != nil && len(path) > 0 {
154
- if !longHelp && !shortHelp {
155
- fmt.Printf(errorFormat, parseError)
156
- }
157
- }
158
-
159
- if cmd == nil {
160
- root = commands.Root
161
- }
162
-
163
- // show the long help text if the -help flag was specified or we are at the root command
164
- // otherwise, show short help text
165
- helpFunc := cmdsCli.ShortHelp
166
- if longHelp || len(path) == 0 {
167
- helpFunc = cmdsCli.LongHelp
168
- }
169
-
170
- htErr := helpFunc("ipfs", root, path, os.Stdout)
171
- if htErr != nil {
172
- fmt.Println(htErr)
173
- }
174
- return parseError
179
+ return nil
180
}
181
177
-func handleHelpOption(req cmds.Request, root *cmds.Command) (helpTextDisplayed bool, err error) {
178
- longHelp, _, err := req.Option("help").Bool()
182
+func (i *cmdInvocation) requestedHelp() (short bool, long bool, err error) {
183
+ longHelp, _, err := i.req.Option("help").Bool()
184
if err != nil {
180
- return false, err
185
+ return false, false, err
186
}
182
- shortHelp, _, err := req.Option("h").Bool()
183
- if err != nil {
184
- return false, err
185
- }
186
- if !longHelp && !shortHelp {
187
- return false, nil
188
- }
189
- helpFunc := cmdsCli.ShortHelp
190
- if longHelp || len(req.Path()) == 0 {
191
- helpFunc = cmdsCli.LongHelp
192
- }
193
-
194
- err = helpFunc("ipfs", root, req.Path(), os.Stdout)
187
+ shortHelp, _, err := i.req.Option("h").Bool()
188
if err != nil {
196
- return false, err
189
+ return false, false, err
190
}
198
- return true, nil
191
+ return longHelp, shortHelp, nil
192
}
193
194
func callCommand(req cmds.Request, root *cmds.Command) (cmds.Response, error) {
195
var res cmds.Response
196
204
- if root == Root { // TODO explain what it means when root == Root
197
+ // TODO explain what it means when root == Root
198
+ // @mappum o/
199
+ if root == Root {
200
res = root.Call(req)
201
202
} else {
@@ -247,35 +242,20 @@ func callCommand(req cmds.Request, root *cmds.Command) (cmds.Response, error) {
242
return res, nil
243
}
244
250
-func outputResponse(res cmds.Response, root *cmds.Command) error {
251
- if res.Error() != nil {
252
- fmt.Printf(errorFormat, res.Error().Error())
253
-
254
- if res.Error().Code != cmds.ErrClient {
255
- // TODO does ErrClient mean "error the client should see" or "this
256
- // is an error caused by the user ie. user error"?
257
- return res.Error()
258
- }
259
-
260
- // if this is a client error, we try to display help text
261
- if res.Error().Code == cmds.ErrClient {
262
- err := cmdsCli.ShortHelp("ipfs", root, res.Request().Path(), os.Stdout)
263
- if err != nil {
264
- fmt.Println(err)
265
- }
266
- }
267
-
268
- emptyErr := errors.New("") // already displayed error text
269
- return emptyErr
245
+func isClientError(err error) bool {
246
+ // cast to cmds.Error
247
+ cmdErr, ok := err.(*cmds.Error)
248
+ if !ok {
249
+ return false
250
}
251
272
- out, err := res.Reader()
273
- if err != nil {
274
- return err
252
+ // here we handle the case where commands with
253
+ // no Run func are invoked directly. As help requests.
254
+ if err == cmds.ErrNotCallable {
255
+ return true
256
}
257
277
- io.Copy(os.Stdout, out)
278
- return nil
258
+ return cmdErr.Code == cmds.ErrClient
259
}
260
261
func getConfigRoot(req cmds.Request) (string, error) {
commands/cli/parse.go
+4
-1
@@ -9,6 +9,9 @@ import (
9
cmds "github.com/jbenet/go-ipfs/commands"
10
)
11
12
+// ErrInvalidSubcmd signals when the parse error is not found
13
+var ErrInvalidSubcmd = errors.New("subcommand not found")
14
+
15
// Parse parses the input commandline string (cmd, flags, and args).
16
// returns the corresponding command Request object.
17
// Multiple root commands are supported:
@@ -41,7 +44,7 @@ func Parse(input []string, roots ...*cmds.Command) (cmds.Request, *cmds.Command,
44
}
45
46
if maxLength == 0 {
44
- return nil, root, nil, path, errors.New("Not a valid subcommand")
47
+ return nil, root, nil, path, ErrInvalidSubcmd
48
}
49
50
args, err := parseArgs(stringArgs, cmd)