parse.go 14 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"
11
	"reflect"
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
12
	"strings"
13

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

Jan Winkelmann's avatar
Jan Winkelmann committed
20
var log = logging.Logger("cmds/cli")
Dominic Della Valle's avatar
Dominic Della Valle committed
21 22 23 24 25 26 27
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."
	}
}
28

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

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

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

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

48 49 50 51 52 53 54
	// 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)
55 56 57
		}
	}

58
	return req, nil
59 60
}

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

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

71 72 73 74 75 76 77 78 79 80
func getIgnoreRulesFile(req *cmds.Request) string {
	rulesFile, _ := req.Options[cmds.IgnoreRules].(string)
	return rulesFile
}

func getIgnoreRules(req *cmds.Request) []string {
	rules, _ := req.Options[cmds.Ignore].([]string)
	return rules
}

81 82 83 84 85
func stdinName(req *cmds.Request) string {
	name, _ := req.Options[cmds.StdinName].(string)
	return name
}

86 87 88 89 90 91 92 93 94 95 96 97 98
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]
}

99 100 101 102 103 104 105 106 107 108 109 110 111
func setOpts(kv kv, kvType reflect.Kind, opts cmds.OptMap) error {

	if kvType == cmds.Strings {
		res, _ := opts[kv.Key].([]string)
		opts[kv.Key] = append(res, kv.Value.(string))
	} else if _, exists := opts[kv.Key]; !exists {
		opts[kv.Key] = kv.Value
	} else {
		return fmt.Errorf("multiple values for option %q", kv.Key)
	}
	return nil
}

112
func parse(req *cmds.Request, cmdline []string, root *cmds.Command) (err error) {
113 114 115
	var (
		path = make([]string, 0, len(cmdline))
		args = make([]string, 0, len(cmdline))
Steven Allen's avatar
Steven Allen committed
116
		opts = cmds.OptMap{}
117 118 119
		cmd  = root
	)

120 121
	st := &parseState{cmdline: cmdline}

122 123
	// get root options
	optDefs, err := root.GetOptions([]string{})
124 125 126
	if err != nil {
		return err
	}
127

128 129
L:
	// don't range so we can seek
130 131
	for !st.done() {
		param := st.peek()
Etienne Laurin's avatar
Etienne Laurin committed
132
		switch {
133 134
		case param == "--":
			// use the rest as positional arguments
135
			args = append(args, st.cmdline[st.i+1:]...)
136 137 138
			break L
		case strings.HasPrefix(param, "--"):
			// long option
139 140 141 142
			k, v, err := st.parseLongOpt(optDefs)
			if err != nil {
				return err
			}
143

144 145 146 147 148 149
			kvType, err := getOptType(k, optDefs)
			if err != nil {
				return err // shouldn't happen b/c k,v was parsed from optsDef
			}
			if err := setOpts(kv{Key: k, Value: v}, kvType, opts); err != nil {
				return err
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
150
			}
151

152 153
		case strings.HasPrefix(param, "-") && param != "-":
			// short options
154 155 156 157
			kvs, err := st.parseShortOpts(optDefs)
			if err != nil {
				return err
			}
158 159 160 161

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

162 163 164 165 166 167
				kvType, err := getOptType(kv.Key, optDefs)
				if err != nil {
					return err // shouldn't happen b/c kvs was parsed from optsDef
				}
				if err := setOpts(kv, kvType, opts); err != nil {
					return err
Etienne Laurin's avatar
Etienne Laurin committed
168
				}
169
			}
Etienne Laurin's avatar
Etienne Laurin committed
170
		default:
171
			arg := param
Etienne Laurin's avatar
Etienne Laurin committed
172
			// arg is a sub-command or a positional argument
keks's avatar
cleanup  
keks committed
173
			sub := cmd.Subcommands[arg]
Etienne Laurin's avatar
Etienne Laurin committed
174 175 176 177 178
			if sub != nil {
				cmd = sub
				path = append(path, arg)
				optDefs, err = root.GetOptions(path)
				if err != nil {
179
					return err
Etienne Laurin's avatar
Etienne Laurin committed
180
				}
181 182 183 184

				// If we've come across an external binary call, pass all the remaining
				// arguments on to it
				if cmd.External {
185
					args = append(args, st.cmdline[st.i+1:]...)
186
					break L
187
				}
Etienne Laurin's avatar
Etienne Laurin committed
188
			} else {
189
				args = append(args, arg)
190 191
				if len(path) == 0 {
					// found a typo or early argument
192
					return printSuggestions(args, root)
193
				}
Etienne Laurin's avatar
Etienne Laurin committed
194
			}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
195
		}
196

197
		st.i++
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
198
	}
199

200 201 202 203 204
	req.Root = root
	req.Command = cmd
	req.Path = path
	req.Arguments = args
	req.Options = opts
205

206
	return nil
207
}
208

209 210 211
func parseArgs(req *cmds.Request, root *cmds.Command, stdin *os.File) error {
	argDefs := req.Command.Arguments

212
	// count required argument definitions
213
	var numRequired int
214
	for _, argDef := range argDefs {
215
		if argDef.Required {
216
			numRequired++
217
		}
218
	}
219

220 221
	inputs := req.Arguments

222 223 224
	// 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.
225
	numInputs := len(inputs)
226

227
	if len(argDefs) > 0 && argDefs[len(argDefs)-1].SupportsStdin && stdin != nil {
228
		numInputs += 1
229 230
	}

231 232 233
	// 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
234
	if notVariadic && len(inputs) > len(argDefs) {
235
		return fmt.Errorf("expected %d argument(s), got %d", len(argDefs), len(inputs))
236 237
	}

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

241 242 243 244 245 246 247 248 249 250 251
	// 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)
252

253
		// skip optional argument definitions if there aren't sufficient remaining inputs
254 255 256
		for remInputs <= remRequired && !argDef.Required {
			iArgDef++
			argDef = getArgDef(iArgDef, argDefs)
257 258
		}
		if argDef.Required {
259
			remRequired--
260
		}
261

262
		fillingVariadic := iArgDef+1 > len(argDefs)
Jeromy's avatar
Jeromy committed
263
		switch argDef.Type {
Steven Allen's avatar
Steven Allen committed
264
		case cmds.ArgString:
265
			if len(inputs) > 0 {
266
				stringArgs, inputs = append(stringArgs, inputs[0]), inputs[1:]
Jeromy's avatar
Jeromy committed
267
			} else if stdin != nil && argDef.SupportsStdin && !fillingVariadic {
268
				if r, err := maybeWrapStdin(stdin, msgStdinInfo); err == nil {
Łukasz Magiera's avatar
Łukasz Magiera committed
269 270 271 272
					fileArgs["stdin"], err = files.NewReaderPathFile(stdin.Name(), r, nil)
					if err != nil {
						return err
					}
Jeromy's avatar
Jeromy committed
273
					stdin = nil
Jeromy's avatar
Jeromy committed
274
				}
275
			}
Steven Allen's avatar
Steven Allen committed
276
		case cmds.ArgFile:
277
			if len(inputs) > 0 {
278
				// treat stringArg values as file paths
Jeromy's avatar
Jeromy committed
279 280
				fpath := inputs[0]
				inputs = inputs[1:]
Łukasz Magiera's avatar
Łukasz Magiera committed
281
				var file files.Node
282
				if fpath == "-" {
283 284
					r, err := maybeWrapStdin(stdin, msgStdinInfo)
					if err != nil {
285
						return err
286
					}
287

288
					fpath = stdinName(req)
Łukasz Magiera's avatar
Łukasz Magiera committed
289 290 291 292
					file, err = files.NewReaderPathFile(stdin.Name(), r, nil)
					if err != nil {
						return err
					}
jmank88's avatar
jmank88 committed
293
				} else if u := isURL(fpath); u != nil {
jmank88's avatar
jmank88 committed
294 295 296 297 298 299 300 301 302 303 304
					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
305
					file = files.NewWebFile(u)
306
				} else {
Łukasz Magiera's avatar
Łukasz Magiera committed
307 308 309 310 311 312 313 314 315 316 317 318 319
					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:
320 321 322 323
						if fpath, err = filepath.EvalSymlinks(fpath); err != nil {
							return err
						}
					}
324 325 326 327 328 329 330
					rulesFile := getIgnoreRulesFile(req)
					ignoreRules := getIgnoreRules(req)
					filter, err := files.NewFilter(rulesFile, ignoreRules, isHidden(req))
					if err != nil {
						return err
					}
					nf, err := appendFile(fpath, argDef, isRecursive(req), filter)
331
					if err != nil {
332
						return err
333 334
					}

Łukasz Magiera's avatar
Łukasz Magiera committed
335
					fpath = path.Base(fpath)
336
					file = nf
337 338
				}

Łukasz Magiera's avatar
Łukasz Magiera committed
339
				fileArgs[fpath] = file
Jeromy's avatar
Jeromy committed
340 341
			} else if stdin != nil && argDef.SupportsStdin &&
				argDef.Required && !fillingVariadic {
342 343
				r, err := maybeWrapStdin(stdin, msgStdinInfo)
				if err != nil {
344
					return err
345
				}
346

347
				fileArgs[stdinName(req)], err = files.NewReaderPathFile(stdin.Name(), r, nil)
Łukasz Magiera's avatar
Łukasz Magiera committed
348 349 350
				if err != nil {
					return err
				}
351 352
			}
		}
353

354 355 356 357 358 359 360
		iArgDef++
	}

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

363
	// check to make sure we didn't miss any required arguments
364 365
	if len(argDefs) > iArgDef {
		for _, argDef := range argDefs[iArgDef:] {
366
			if argDef.Required {
367 368 369 370 371 372 373
				return fmt.Errorf("argument %q is required", argDef.Name)
			}
		}
	}

	req.Arguments = stringArgs
	if len(fileArgs) > 0 {
Łukasz Magiera's avatar
Łukasz Magiera committed
374
		req.Files = files.NewMapDirectory(fileArgs)
375 376 377 378 379
	}

	return nil
}

jmank88's avatar
jmank88 committed
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
// 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
397 398 399 400 401 402 403
func urlBase(u *url.URL) string {
	if u.Path == "" {
		return u.Host
	}
	return path.Base(u.Path)
}

404 405 406 407 408 409 410 411 412
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
	}
}

413
func parseOpt(opt, value string, opts map[string]cmds.Option) (string, interface{}, error) {
414 415
	optDef, ok := opts[opt]
	if !ok {
416
		return "", nil, fmt.Errorf("unknown option %q", opt)
417 418 419 420
	}

	v, err := optDef.Parse(value)
	if err != nil {
421
		return "", nil, err
422
	}
423
	return optDef.Name(), v, nil
424 425 426 427 428 429 430
}

type kv struct {
	Key   string
	Value interface{}
}

Steven Allen's avatar
Steven Allen committed
431
func (st *parseState) parseShortOpts(optDefs map[string]cmds.Option) ([]kv, error) {
432
	k, vStr, ok := splitkv(st.cmdline[st.i][1:])
keks's avatar
keks committed
433
	kvs := make([]kv, 0, len(k))
434 435 436

	if ok {
		// split at = successful
437
		k, v, err := parseOpt(k, vStr, optDefs)
438 439 440 441 442
		if err != nil {
			return nil, err
		}

		kvs = append(kvs, kv{Key: k, Value: v})
443 444

	} else {
keks's avatar
keks committed
445 446
	LOOP:
		for j := 0; j < len(k); {
447
			flag := k[j : j+1]
keks's avatar
keks committed
448
			od, ok := optDefs[flag]
449

keks's avatar
keks committed
450 451
			switch {
			case !ok:
452
				return nil, fmt.Errorf("unknown option %q", k)
453

Steven Allen's avatar
Steven Allen committed
454
			case od.Type() == cmds.Bool:
455 456
				// single char flags for bools
				kvs = append(kvs, kv{
457
					Key:   od.Name(),
458 459 460 461
					Value: true,
				})
				j++

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

466
				k, v, err := parseOpt(flag, rest, optDefs)
467 468 469 470
				if err != nil {
					return nil, err
				}

471
				kvs = append(kvs, kv{Key: k, Value: v})
keks's avatar
keks committed
472
				break LOOP
473

474
			case st.i < len(st.cmdline)-1:
475
				// single char flag for non-bools (use the next word as value)
476
				st.i++
477
				k, v, err := parseOpt(flag, st.cmdline[st.i], optDefs)
478 479 480 481
				if err != nil {
					return nil, err
				}

482
				kvs = append(kvs, kv{Key: k, Value: v})
keks's avatar
keks committed
483
				break LOOP
484

keks's avatar
keks committed
485
			default:
486
				return nil, fmt.Errorf("missing argument for option %q", k)
487
			}
488 489 490
		}
	}

491
	return kvs, nil
Jeromy's avatar
Jeromy committed
492 493
}

Steven Allen's avatar
Steven Allen committed
494
func (st *parseState) parseLongOpt(optDefs map[string]cmds.Option) (string, interface{}, error) {
495
	k, v, ok := splitkv(st.peek()[2:])
496
	if !ok {
497 498 499 500
		optDef, ok := optDefs[k]
		if !ok {
			return "", nil, fmt.Errorf("unknown option %q", k)
		}
Steven Allen's avatar
Steven Allen committed
501
		if optDef.Type() == cmds.Bool {
502 503 504 505
			return k, true, nil
		} else if st.i < len(st.cmdline)-1 {
			st.i++
			v = st.peek()
506
		} else {
507
			return "", nil, fmt.Errorf("missing argument for option %q", k)
508 509 510
		}
	}

511
	k, optval, err := parseOpt(k, v, optDefs)
512
	return k, optval, err
513
}
514

Steven Allen's avatar
Steven Allen committed
515
func getArgDef(i int, argDefs []cmds.Argument) *cmds.Argument {
516 517 518 519 520 521 522 523 524 525 526 527 528
	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
529
const notRecursiveFmtStr = "'%s' is a directory, use the '-%s' flag to specify directories"
530
const dirNotSupportedFmtStr = "invalid path '%s', argument '%s' does not support directories"
Jeromy's avatar
Jeromy committed
531

532
func appendFile(fpath string, argDef *cmds.Argument, recursive bool, filter *files.Filter) (files.Node, error) {
Jeromy's avatar
Jeromy committed
533
	stat, err := os.Lstat(fpath)
534
	if err != nil {
Jeromy's avatar
Jeromy committed
535
		return nil, err
536 537 538 539
	}

	if stat.IsDir() {
		if !argDef.Recursive {
Jeromy's avatar
Jeromy committed
540
			return nil, fmt.Errorf(dirNotSupportedFmtStr, fpath, argDef.Name)
541 542
		}
		if !recursive {
543
			return nil, fmt.Errorf(notRecursiveFmtStr, fpath, cmds.RecShort)
544
		}
545 546 547 548 549 550 551 552
	} 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
		}
553

554
		return files.NewReaderFile(file), nil
555
	}
556
	return files.NewSerialFileWithFilter(fpath, filter, stat)
557
}
558 559

// Inform the user if a file is waiting on input
560
func maybeWrapStdin(f *os.File, msg string) (io.ReadCloser, error) {
561
	isTty, err := isTty(f)
562
	if err != nil {
563
		return nil, err
564 565
	}

566
	if isTty {
567
		return newMessageReader(f, fmt.Sprintf(msg, f.Name())), nil
568 569
	}

570
	return f, nil
571
}
572 573 574 575 576 577 578 579 580 581

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
}
582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598

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)
599
		r.done = true
600 601 602 603 604 605 606 607
	}

	return r.r.Read(b)
}

func (r *messageReader) Close() error {
	return r.r.Close()
}
608 609 610 611 612 613 614

func getOptType(k string, optDefs map[string]cmds.Option) (reflect.Kind, error) {
	if opt, ok := optDefs[k]; ok {
		return opt.Type(), nil
	}
	return reflect.Invalid, fmt.Errorf("unknown option %q", k)
}