parse.go 12.8 KB
Newer Older
1 2 3
package cli

import (
4
	"context"
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
5
	"fmt"
6
	"io"
jmank88's avatar
jmank88 committed
7
	"net/url"
8
	"os"
9
	"path"
10
	"path/filepath"
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
11
	"strings"
12

Steven Allen's avatar
Steven Allen committed
13
	osh "github.com/Kubuxu/go-os-helper"
14 15
	cmds "github.com/ipfs/go-ipfs-cmds"
	files "github.com/ipfs/go-ipfs-files"
Steven Allen's avatar
Steven Allen committed
16
	logging "github.com/ipfs/go-log"
17 18
)

Jan Winkelmann's avatar
Jan Winkelmann committed
19
var log = logging.Logger("cmds/cli")
Dominic Della Valle's avatar
Dominic Della Valle committed
20 21 22 23 24 25 26
var msgStdinInfo = "ipfs: Reading from %s; send Ctrl-d to stop."

func init() {
	if osh.IsWindows() {
		msgStdinInfo = "ipfs: Reading from %s; send Ctrl-z to stop."
	}
}
27

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
28 29
// Parse parses the input commandline string (cmd, flags, and args).
// returns the corresponding command Request object.
30 31
//
// This function never returns nil, even on error.
32
func Parse(ctx context.Context, input []string, stdin *os.File, root *cmds.Command) (*cmds.Request, error) {
33 34 35
	req := &cmds.Request{Context: ctx}

	if err := parse(req, input, root); err != nil {
36
		return req, err
37 38
	}

39 40 41
	if err := req.FillDefaults(); err != nil {
		return req, err
	}
42

43
	if err := parseArgs(req, root, stdin); err != nil {
44
		return req, err
45
	}
46

47 48 49 50 51 52 53
	// 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 {
			req.SetOption(cmds.EncLong, cmds.Text)
		} else {
			req.SetOption(cmds.EncLong, cmds.JSON)
54 55 56
		}
	}

57
	return req, nil
58 59
}

60
func isHidden(req *cmds.Request) bool {
Łukasz Magiera's avatar
Łukasz Magiera committed
61
	h, ok := req.Options[cmds.Hidden].(bool)
62 63
	return h && ok
}
64

65 66 67 68
func isRecursive(req *cmds.Request) bool {
	rec, ok := req.Options[cmds.RecLong].(bool)
	return rec && ok
}
69

70 71 72 73 74
func stdinName(req *cmds.Request) string {
	name, _ := req.Options[cmds.StdinName].(string)
	return name
}

75 76 77 78 79 80 81 82 83 84 85 86 87 88
type parseState struct {
	cmdline []string
	i       int
}

func (st *parseState) done() bool {
	return st.i >= len(st.cmdline)
}

func (st *parseState) peek() string {
	return st.cmdline[st.i]
}

func parse(req *cmds.Request, cmdline []string, root *cmds.Command) (err error) {
89 90 91
	var (
		path = make([]string, 0, len(cmdline))
		args = make([]string, 0, len(cmdline))
Steven Allen's avatar
Steven Allen committed
92
		opts = cmds.OptMap{}
93 94 95
		cmd  = root
	)

96 97
	st := &parseState{cmdline: cmdline}

98 99
	// get root options
	optDefs, err := root.GetOptions([]string{})
100 101 102
	if err != nil {
		return err
	}
103

104 105
L:
	// don't range so we can seek
106 107
	for !st.done() {
		param := st.peek()
Etienne Laurin's avatar
Etienne Laurin committed
108
		switch {
109 110
		case param == "--":
			// use the rest as positional arguments
111
			args = append(args, st.cmdline[st.i+1:]...)
112 113 114
			break L
		case strings.HasPrefix(param, "--"):
			// long option
115 116 117 118 119
			k, v, err := st.parseLongOpt(optDefs)
			if err != nil {
				return err
			}

120
			if _, exists := opts[k]; exists {
121
				return fmt.Errorf("multiple values for option %q", k)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
122
			}
123

124 125
			k = optDefs[k].Name()
			opts[k] = v
126

127 128
		case strings.HasPrefix(param, "-") && param != "-":
			// short options
129 130 131 132
			kvs, err := st.parseShortOpts(optDefs)
			if err != nil {
				return err
			}
133 134 135 136 137

			for _, kv := range kvs {
				kv.Key = optDefs[kv.Key].Names()[0]

				if _, exists := opts[kv.Key]; exists {
138
					return fmt.Errorf("multiple values for option %q", kv.Key)
Etienne Laurin's avatar
Etienne Laurin committed
139
				}
140

141 142
				opts[kv.Key] = kv.Value
			}
Etienne Laurin's avatar
Etienne Laurin committed
143
		default:
144
			arg := param
Etienne Laurin's avatar
Etienne Laurin committed
145
			// arg is a sub-command or a positional argument
keks's avatar
cleanup  
keks committed
146
			sub := cmd.Subcommands[arg]
Etienne Laurin's avatar
Etienne Laurin committed
147 148 149 150 151
			if sub != nil {
				cmd = sub
				path = append(path, arg)
				optDefs, err = root.GetOptions(path)
				if err != nil {
152
					return err
Etienne Laurin's avatar
Etienne Laurin committed
153
				}
154 155 156 157

				// If we've come across an external binary call, pass all the remaining
				// arguments on to it
				if cmd.External {
158
					args = append(args, st.cmdline[st.i+1:]...)
159
					break L
160
				}
Etienne Laurin's avatar
Etienne Laurin committed
161
			} else {
162
				args = append(args, arg)
163 164
				if len(path) == 0 {
					// found a typo or early argument
165
					return printSuggestions(args, root)
166
				}
Etienne Laurin's avatar
Etienne Laurin committed
167
			}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
168
		}
169

170
		st.i++
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
171
	}
172

173 174 175 176 177
	req.Root = root
	req.Command = cmd
	req.Path = path
	req.Arguments = args
	req.Options = opts
178

179
	return nil
180
}
181

182 183 184
func parseArgs(req *cmds.Request, root *cmds.Command, stdin *os.File) error {
	argDefs := req.Command.Arguments

185
	// count required argument definitions
186
	var numRequired int
187
	for _, argDef := range argDefs {
188
		if argDef.Required {
189
			numRequired++
190
		}
191
	}
192

193 194
	inputs := req.Arguments

195 196 197
	// count number of values provided by user.
	// if there is at least one ArgDef, we can safely trigger the inputs loop
	// below to parse stdin.
198
	numInputs := len(inputs)
199

200
	if len(argDefs) > 0 && argDefs[len(argDefs)-1].SupportsStdin && stdin != nil {
201
		numInputs += 1
202 203
	}

204 205 206
	// if we have more arg values provided than argument definitions,
	// and the last arg definition is not variadic (or there are no definitions), return an error
	notVariadic := len(argDefs) == 0 || !argDefs[len(argDefs)-1].Variadic
Christian Couder's avatar
Christian Couder committed
207
	if notVariadic && len(inputs) > len(argDefs) {
208
		return fmt.Errorf("expected %d argument(s), got %d", len(argDefs), len(inputs))
209 210
	}

211
	stringArgs := make([]string, 0, numInputs)
Łukasz Magiera's avatar
Łukasz Magiera committed
212
	fileArgs := make(map[string]files.Node)
Jeromy's avatar
Jeromy committed
213

214 215 216 217 218 219 220 221 222 223 224
	// the index of the current argument definition
	iArgDef := 0

	// remaining number of required arguments
	remRequired := numRequired

	for iInput := 0; iInput < numInputs; iInput++ {
		// remaining number of passed arguments
		remInputs := numInputs - iInput

		argDef := getArgDef(iArgDef, argDefs)
225

226
		// skip optional argument definitions if there aren't sufficient remaining inputs
227 228 229
		for remInputs <= remRequired && !argDef.Required {
			iArgDef++
			argDef = getArgDef(iArgDef, argDefs)
230 231
		}
		if argDef.Required {
232
			remRequired--
233
		}
234

235
		fillingVariadic := iArgDef+1 > len(argDefs)
Jeromy's avatar
Jeromy committed
236
		switch argDef.Type {
Steven Allen's avatar
Steven Allen committed
237
		case cmds.ArgString:
238
			if len(inputs) > 0 {
239
				stringArgs, inputs = append(stringArgs, inputs[0]), inputs[1:]
Jeromy's avatar
Jeromy committed
240
			} else if stdin != nil && argDef.SupportsStdin && !fillingVariadic {
241
				if r, err := maybeWrapStdin(stdin, msgStdinInfo); err == nil {
Łukasz Magiera's avatar
Łukasz Magiera committed
242 243 244 245
					fileArgs["stdin"], err = files.NewReaderPathFile(stdin.Name(), r, nil)
					if err != nil {
						return err
					}
Jeromy's avatar
Jeromy committed
246
					stdin = nil
Jeromy's avatar
Jeromy committed
247
				}
248
			}
Steven Allen's avatar
Steven Allen committed
249
		case cmds.ArgFile:
250
			if len(inputs) > 0 {
251
				// treat stringArg values as file paths
Jeromy's avatar
Jeromy committed
252 253
				fpath := inputs[0]
				inputs = inputs[1:]
Łukasz Magiera's avatar
Łukasz Magiera committed
254
				var file files.Node
255
				if fpath == "-" {
256 257
					r, err := maybeWrapStdin(stdin, msgStdinInfo)
					if err != nil {
258
						return err
259
					}
260

261
					fpath = stdinName(req)
Łukasz Magiera's avatar
Łukasz Magiera committed
262 263 264 265
					file, err = files.NewReaderPathFile(stdin.Name(), r, nil)
					if err != nil {
						return err
					}
jmank88's avatar
jmank88 committed
266
				} else if u := isURL(fpath); u != nil {
jmank88's avatar
jmank88 committed
267 268 269 270 271 272 273 274 275 276 277
					base := urlBase(u)
					fpath = base
					if _, ok := fileArgs[fpath]; ok {
						// Ensure a unique fpath by suffixing ' (n)'.
						for i := 1; ; i++ {
							fpath = fmt.Sprintf("%s (%d)", base, i)
							if _, ok := fileArgs[fpath]; !ok {
								break
							}
						}
					}
jmank88's avatar
jmank88 committed
278
					file = files.NewWebFile(u)
279
				} else {
Łukasz Magiera's avatar
Łukasz Magiera committed
280 281 282 283 284 285 286 287 288 289 290 291 292
					fpath = filepath.ToSlash(filepath.Clean(fpath))
					derefArgs, _ := req.Options[cmds.DerefLong].(bool)
					var err error

					switch {
					case fpath == ".":
						cwd, err := os.Getwd()
						if err != nil {
							return err
						}
						fpath = filepath.ToSlash(cwd)
						fallthrough
					case derefArgs:
293 294 295 296
						if fpath, err = filepath.EvalSymlinks(fpath); err != nil {
							return err
						}
					}
Łukasz Magiera's avatar
Łukasz Magiera committed
297

298
					nf, err := appendFile(fpath, argDef, isRecursive(req), isHidden(req))
299
					if err != nil {
300
						return err
301 302
					}

Łukasz Magiera's avatar
Łukasz Magiera committed
303
					fpath = path.Base(fpath)
304
					file = nf
305 306
				}

Łukasz Magiera's avatar
Łukasz Magiera committed
307
				fileArgs[fpath] = file
Jeromy's avatar
Jeromy committed
308 309
			} else if stdin != nil && argDef.SupportsStdin &&
				argDef.Required && !fillingVariadic {
310 311
				r, err := maybeWrapStdin(stdin, msgStdinInfo)
				if err != nil {
312
					return err
313
				}
314

315
				fileArgs[stdinName(req)], err = files.NewReaderPathFile(stdin.Name(), r, nil)
Łukasz Magiera's avatar
Łukasz Magiera committed
316 317 318
				if err != nil {
					return err
				}
319 320
			}
		}
321

322 323 324 325 326 327 328
		iArgDef++
	}

	if iArgDef == len(argDefs)-1 && stdin != nil &&
		req.Command.Arguments[iArgDef].SupportsStdin {
		// handle this one at runtime, pretend it's there
		iArgDef++
329 330
	}

331
	// check to make sure we didn't miss any required arguments
332 333
	if len(argDefs) > iArgDef {
		for _, argDef := range argDefs[iArgDef:] {
334
			if argDef.Required {
335 336 337 338 339 340 341
				return fmt.Errorf("argument %q is required", argDef.Name)
			}
		}
	}

	req.Arguments = stringArgs
	if len(fileArgs) > 0 {
Łukasz Magiera's avatar
Łukasz Magiera committed
342
		req.Files = files.NewMapDirectory(fileArgs)
343 344 345 346 347
	}

	return nil
}

jmank88's avatar
jmank88 committed
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364
// isURL returns a url.URL for valid http:// and https:// URLs, otherwise it returns nil.
func isURL(path string) *url.URL {
	u, err := url.Parse(path)
	if err != nil {
		return nil
	}
	if u.Host == "" {
		return nil
	}
	switch u.Scheme {
	default:
		return nil
	case "http", "https":
		return u
	}
}

jmank88's avatar
jmank88 committed
365 366 367 368 369 370 371
func urlBase(u *url.URL) string {
	if u.Path == "" {
		return u.Host
	}
	return path.Base(u.Path)
}

372 373 374 375 376 377 378 379 380
func splitkv(opt string) (k, v string, ok bool) {
	split := strings.SplitN(opt, "=", 2)
	if len(split) == 2 {
		return split[0], split[1], true
	} else {
		return opt, "", false
	}
}

Steven Allen's avatar
Steven Allen committed
381
func parseOpt(opt, value string, opts map[string]cmds.Option) (interface{}, error) {
382 383
	optDef, ok := opts[opt]
	if !ok {
384
		return nil, fmt.Errorf("unknown option %q", opt)
385 386 387 388
	}

	v, err := optDef.Parse(value)
	if err != nil {
389
		return nil, err
390
	}
391
	return v, nil
392 393 394 395 396 397 398
}

type kv struct {
	Key   string
	Value interface{}
}

Steven Allen's avatar
Steven Allen committed
399
func (st *parseState) parseShortOpts(optDefs map[string]cmds.Option) ([]kv, error) {
400
	k, vStr, ok := splitkv(st.cmdline[st.i][1:])
keks's avatar
keks committed
401
	kvs := make([]kv, 0, len(k))
402 403 404

	if ok {
		// split at = successful
405 406 407 408 409 410
		v, err := parseOpt(k, vStr, optDefs)
		if err != nil {
			return nil, err
		}

		kvs = append(kvs, kv{Key: k, Value: v})
411 412

	} else {
keks's avatar
keks committed
413 414
	LOOP:
		for j := 0; j < len(k); {
415
			flag := k[j : j+1]
keks's avatar
keks committed
416
			od, ok := optDefs[flag]
417

keks's avatar
keks committed
418 419
			switch {
			case !ok:
420
				return nil, fmt.Errorf("unknown option %q", k)
421

Steven Allen's avatar
Steven Allen committed
422
			case od.Type() == cmds.Bool:
423 424 425 426 427 428 429
				// single char flags for bools
				kvs = append(kvs, kv{
					Key:   flag,
					Value: true,
				})
				j++

keks's avatar
keks committed
430
			case j < len(k)-1:
431 432 433
				// single char flag for non-bools (use the rest of the flag as value)
				rest := k[j+1:]

434 435 436 437 438 439
				v, err := parseOpt(flag, rest, optDefs)
				if err != nil {
					return nil, err
				}

				kvs = append(kvs, kv{Key: flag, Value: v})
keks's avatar
keks committed
440
				break LOOP
441

442
			case st.i < len(st.cmdline)-1:
443
				// single char flag for non-bools (use the next word as value)
444 445 446 447 448 449 450
				st.i++
				v, err := parseOpt(flag, st.cmdline[st.i], optDefs)
				if err != nil {
					return nil, err
				}

				kvs = append(kvs, kv{Key: flag, Value: v})
keks's avatar
keks committed
451
				break LOOP
452

keks's avatar
keks committed
453
			default:
454
				return nil, fmt.Errorf("missing argument for option %q", k)
455
			}
456 457 458
		}
	}

459
	return kvs, nil
Jeromy's avatar
Jeromy committed
460 461
}

Steven Allen's avatar
Steven Allen committed
462
func (st *parseState) parseLongOpt(optDefs map[string]cmds.Option) (string, interface{}, error) {
463
	k, v, ok := splitkv(st.peek()[2:])
464
	if !ok {
465 466 467 468
		optDef, ok := optDefs[k]
		if !ok {
			return "", nil, fmt.Errorf("unknown option %q", k)
		}
Steven Allen's avatar
Steven Allen committed
469
		if optDef.Type() == cmds.Bool {
470 471 472 473
			return k, true, nil
		} else if st.i < len(st.cmdline)-1 {
			st.i++
			v = st.peek()
474
		} else {
475
			return "", nil, fmt.Errorf("missing argument for option %q", k)
476 477 478
		}
	}

479 480
	optval, err := parseOpt(k, v, optDefs)
	return k, optval, err
481
}
482

Steven Allen's avatar
Steven Allen committed
483
func getArgDef(i int, argDefs []cmds.Argument) *cmds.Argument {
484 485 486 487 488 489 490 491 492 493 494 495 496
	if i < len(argDefs) {
		// get the argument definition (usually just argDefs[i])
		return &argDefs[i]

	} else if len(argDefs) > 0 {
		// but if i > len(argDefs) we use the last argument definition)
		return &argDefs[len(argDefs)-1]
	}

	// only happens if there aren't any definitions
	return nil
}

Jeromy's avatar
Jeromy committed
497
const notRecursiveFmtStr = "'%s' is a directory, use the '-%s' flag to specify directories"
498
const dirNotSupportedFmtStr = "invalid path '%s', argument '%s' does not support directories"
Jeromy's avatar
Jeromy committed
499

Steven Allen's avatar
Steven Allen committed
500
func appendFile(fpath string, argDef *cmds.Argument, recursive, hidden bool) (files.Node, error) {
Jeromy's avatar
Jeromy committed
501
	stat, err := os.Lstat(fpath)
502
	if err != nil {
Jeromy's avatar
Jeromy committed
503
		return nil, err
504 505 506 507
	}

	if stat.IsDir() {
		if !argDef.Recursive {
Jeromy's avatar
Jeromy committed
508
			return nil, fmt.Errorf(dirNotSupportedFmtStr, fpath, argDef.Name)
509 510
		}
		if !recursive {
511
			return nil, fmt.Errorf(notRecursiveFmtStr, fpath, cmds.RecShort)
512
		}
513 514 515 516 517 518 519 520
	} else if (stat.Mode() & os.ModeNamedPipe) != 0 {
		// Special case pipes that are provided directly on the command line
		// We do this here instead of go-ipfs-files, as we need to differentiate between
		// recursive(unsupported) and direct(supported) mode
		file, err := os.Open(fpath)
		if err != nil {
			return nil, err
		}
521

522
		return files.NewReaderFile(file), nil
523 524
	}

525
	return files.NewSerialFile(fpath, hidden, stat)
526
}
527 528

// Inform the user if a file is waiting on input
529
func maybeWrapStdin(f *os.File, msg string) (io.ReadCloser, error) {
530
	isTty, err := isTty(f)
531
	if err != nil {
532
		return nil, err
533 534
	}

535
	if isTty {
536
		return newMessageReader(f, fmt.Sprintf(msg, f.Name())), nil
537 538
	}

539
	return f, nil
540
}
541 542 543 544 545 546 547 548 549 550

func isTty(f *os.File) (bool, error) {
	fInfo, err := f.Stat()
	if err != nil {
		log.Error(err)
		return false, err
	}

	return (fInfo.Mode() & os.ModeCharDevice) != 0, nil
}
551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567

type messageReader struct {
	r       io.ReadCloser
	done    bool
	message string
}

func newMessageReader(r io.ReadCloser, msg string) io.ReadCloser {
	return &messageReader{
		r:       r,
		message: msg,
	}
}

func (r *messageReader) Read(b []byte) (int, error) {
	if !r.done {
		fmt.Fprintln(os.Stderr, r.message)
568
		r.done = true
569 570 571 572 573 574 575 576
	}

	return r.r.Read(b)
}

func (r *messageReader) Close() error {
	return r.r.Close()
}