parse.go 11.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"
7
	"os"
8
	"path"
9
	"path/filepath"
10
	"runtime"
Jeromy's avatar
Jeromy committed
11
	"sort"
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
12
	"strings"
13

keks's avatar
keks committed
14 15
	"github.com/ipfs/go-ipfs-cmdkit"
	"github.com/ipfs/go-ipfs-cmdkit/files"
keks's avatar
keks committed
16
	"github.com/ipfs/go-ipfs-cmds"
Jan Winkelmann's avatar
Jan Winkelmann committed
17

keks's avatar
keks committed
18
	logging "github.com/ipfs/go-log"
19 20
)

Jan Winkelmann's avatar
Jan Winkelmann committed
21
var log = logging.Logger("cmds/cli")
22

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
23 24
// Parse parses the input commandline string (cmd, flags, and args).
// returns the corresponding command Request object.
25
func Parse(ctx context.Context, input []string, stdin *os.File, root *cmds.Command) (*cmds.Request, error) {
26 27 28
	req := &cmds.Request{Context: ctx}

	if err := parse(req, input, root); err != nil {
29
		return req, err
30 31
	}

32 33 34
	if err := req.FillDefaults(); err != nil {
		return req, err
	}
35

36 37 38
	// This is an ugly hack to maintain our current CLI interface while fixing
	// other stdin usage bugs. Let this serve as a warning, be careful about the
	// choices you make, they will haunt you forever.
39 40 41
	if len(req.Path) == 2 && req.Path[0] == "bootstrap" {
		if (req.Path[1] == "add" && req.Options["default"] == true) ||
			(req.Path[1] == "rm" && req.Options["all"] == true) {
42 43 44 45
			stdin = nil
		}
	}

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

50
	if err := req.Command.CheckArguments(req); err != nil {
51
		return req, err
52
	}
53

54 55 56 57 58 59 60
	// 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)
61 62 63
		}
	}

64
	return req, nil
65 66
}

67 68 69 70
func isHidden(req *cmds.Request) bool {
	h, ok := req.Options["hidden"].(bool)
	return h && ok
}
71

72 73 74 75
func isRecursive(req *cmds.Request) bool {
	rec, ok := req.Options[cmds.RecLong].(bool)
	return rec && ok
}
76

77 78 79 80 81 82 83 84 85 86 87 88 89 90
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) {
91 92 93 94 95 96 97
	var (
		path = make([]string, 0, len(cmdline))
		args = make([]string, 0, len(cmdline))
		opts = cmdkit.OptMap{}
		cmd  = root
	)

98 99
	st := &parseState{cmdline: cmdline}

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

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

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

126 127
			k = optDefs[k].Name()
			opts[k] = v
128

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

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

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

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

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

172
		st.i++
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
173
	}
174

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

181
	return nil
182
}
183

184
func parseArgs(req *cmds.Request, root *cmds.Command, stdin *os.File) error {
185 186 187 188 189
	// ignore stdin on Windows
	if runtime.GOOS == "windows" {
		stdin = nil
	}

190 191
	argDefs := req.Command.Arguments

192
	// count required argument definitions
193
	var numRequired int
194
	for _, argDef := range argDefs {
195
		if argDef.Required {
196
			numRequired++
197
		}
198
	}
199

200 201
	inputs := req.Arguments

202 203 204
	// 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.
205
	numInputs := len(inputs)
206

207
	if len(argDefs) > 0 && argDefs[len(argDefs)-1].SupportsStdin && stdin != nil {
208
		numInputs += 1
209 210
	}

211 212 213
	// 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
214
	if notVariadic && len(inputs) > len(argDefs) {
215
		return printSuggestions(inputs, root)
216 217
	}

218
	stringArgs := make([]string, 0, numInputs)
Jeromy's avatar
Jeromy committed
219
	fileArgs := make(map[string]files.File)
Jeromy's avatar
Jeromy committed
220

221 222 223 224 225 226 227 228 229 230 231
	// 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)
232

233
		// skip optional argument definitions if there aren't sufficient remaining inputs
234 235 236
		for remInputs <= remRequired && !argDef.Required {
			iArgDef++
			argDef = getArgDef(iArgDef, argDefs)
237 238
		}
		if argDef.Required {
239
			remRequired--
240
		}
241

242
		fillingVariadic := iArgDef+1 > len(argDefs)
Jeromy's avatar
Jeromy committed
243
		switch argDef.Type {
244
		case cmdkit.ArgString:
245
			if len(inputs) > 0 {
246
				stringArgs, inputs = append(stringArgs, inputs[0]), inputs[1:]
Jeromy's avatar
Jeromy committed
247
			} else if stdin != nil && argDef.SupportsStdin && !fillingVariadic {
248 249
				if r, err := maybeWrapStdin(stdin, msgStdinInfo); err == nil {
					fileArgs[stdin.Name()] = files.NewReaderFile("stdin", "", r, nil)
Jeromy's avatar
Jeromy committed
250
					stdin = nil
Jeromy's avatar
Jeromy committed
251
				}
252
			}
253
		case cmdkit.ArgFile:
254
			if len(inputs) > 0 {
255
				// treat stringArg values as file paths
Jeromy's avatar
Jeromy committed
256 257
				fpath := inputs[0]
				inputs = inputs[1:]
258 259
				var file files.File
				if fpath == "-" {
260 261
					r, err := maybeWrapStdin(stdin, msgStdinInfo)
					if err != nil {
262
						return err
263
					}
264 265 266

					fpath = stdin.Name()
					file = files.NewReaderFile("", fpath, r, nil)
267
				} else {
268
					nf, err := appendFile(fpath, argDef, isRecursive(req), isHidden(req))
269
					if err != nil {
270
						return err
271 272 273
					}

					file = nf
274 275
				}

Jeromy's avatar
Jeromy committed
276
				fileArgs[fpath] = file
Jeromy's avatar
Jeromy committed
277 278
			} else if stdin != nil && argDef.SupportsStdin &&
				argDef.Required && !fillingVariadic {
279 280
				r, err := maybeWrapStdin(stdin, msgStdinInfo)
				if err != nil {
281
					return err
282
				}
283

Jeromy's avatar
Jeromy committed
284
				fpath := stdin.Name()
285
				fileArgs[fpath] = files.NewReaderFile("", fpath, r, nil)
286 287
			}
		}
288

289 290 291 292 293 294 295
		iArgDef++
	}

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

298
	// check to make sure we didn't miss any required arguments
299 300
	if len(argDefs) > iArgDef {
		for _, argDef := range argDefs[iArgDef:] {
301
			if argDef.Required {
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
				return fmt.Errorf("argument %q is required", argDef.Name)
			}
		}
	}

	req.Arguments = stringArgs
	if len(fileArgs) > 0 {
		req.Files = files.NewSliceFile("", "", filesMapToSortedArr(fileArgs))
	}

	return nil
}

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
	}
}

324
func parseOpt(opt, value string, opts map[string]cmdkit.Option) (interface{}, error) {
325 326 327 328 329 330 331
	optDef, ok := opts[opt]
	if !ok {
		panic(fmt.Errorf("unknown option %q", opt))
	}

	v, err := optDef.Parse(value)
	if err != nil {
332
		return nil, err
333
	}
334
	return v, nil
335 336 337 338 339 340 341
}

type kv struct {
	Key   string
	Value interface{}
}

342 343
func (st *parseState) parseShortOpts(optDefs map[string]cmdkit.Option) ([]kv, error) {
	k, vStr, ok := splitkv(st.cmdline[st.i][1:])
keks's avatar
keks committed
344
	kvs := make([]kv, 0, len(k))
345 346 347

	if ok {
		// split at = successful
348 349 350 351 352 353
		v, err := parseOpt(k, vStr, optDefs)
		if err != nil {
			return nil, err
		}

		kvs = append(kvs, kv{Key: k, Value: v})
354 355

	} else {
keks's avatar
keks committed
356 357
	LOOP:
		for j := 0; j < len(k); {
358
			flag := k[j : j+1]
keks's avatar
keks committed
359
			od, ok := optDefs[flag]
360

keks's avatar
keks committed
361 362
			switch {
			case !ok:
363
				return nil, fmt.Errorf("unknown option %q", k)
364

keks's avatar
keks committed
365
			case od.Type() == cmdkit.Bool:
366 367 368 369 370 371 372
				// single char flags for bools
				kvs = append(kvs, kv{
					Key:   flag,
					Value: true,
				})
				j++

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

377 378 379 380 381 382
				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
383
				break LOOP
384

385
			case st.i < len(st.cmdline)-1:
386
				// single char flag for non-bools (use the next word as value)
387 388 389 390 391 392 393
				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
394
				break LOOP
395

keks's avatar
keks committed
396
			default:
397
				return nil, fmt.Errorf("missing argument for option %q", k)
398
			}
399 400 401
		}
	}

402
	return kvs, nil
Jeromy's avatar
Jeromy committed
403 404
}

405 406
func (st *parseState) parseLongOpt(optDefs map[string]cmdkit.Option) (string, interface{}, error) {
	k, v, ok := splitkv(st.peek()[2:])
407 408
	if !ok {
		if optDefs[k].Type() == cmdkit.Bool {
409 410 411 412
			return k, true, nil
		} else if st.i < len(st.cmdline)-1 {
			st.i++
			v = st.peek()
413
		} else {
414
			return "", nil, fmt.Errorf("missing argument for option %q", k)
415 416 417
		}
	}

418 419
	optval, err := parseOpt(k, v, optDefs)
	return k, optval, err
420 421 422 423
}

const msgStdinInfo = "ipfs: Reading from %s; send Ctrl-d to stop."

Jeromy's avatar
Jeromy committed
424 425 426 427 428 429 430 431 432 433 434 435 436 437
func filesMapToSortedArr(fs map[string]files.File) []files.File {
	var names []string
	for name, _ := range fs {
		names = append(names, name)
	}

	sort.Strings(names)

	var out []files.File
	for _, f := range names {
		out = append(out, fs[f])
	}

	return out
438
}
439

440
func getArgDef(i int, argDefs []cmdkit.Argument) *cmdkit.Argument {
441 442 443 444 445 446 447 448 449 450 451 452 453
	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
454 455 456
const notRecursiveFmtStr = "'%s' is a directory, use the '-%s' flag to specify directories"
const dirNotSupportedFmtStr = "Invalid path '%s', argument '%s' does not support directories"

457
func appendFile(fpath string, argDef *cmdkit.Argument, recursive, hidden bool) (files.File, error) {
458 459 460
	if fpath == "." {
		cwd, err := os.Getwd()
		if err != nil {
Jeromy's avatar
Jeromy committed
461
			return nil, err
462
		}
463 464 465 466
		cwd, err = filepath.EvalSymlinks(cwd)
		if err != nil {
			return nil, err
		}
467 468
		fpath = cwd
	}
Jeromy's avatar
Jeromy committed
469

470
	fpath = filepath.ToSlash(filepath.Clean(fpath))
471

Jeromy's avatar
Jeromy committed
472
	stat, err := os.Lstat(fpath)
473
	if err != nil {
Jeromy's avatar
Jeromy committed
474
		return nil, err
475 476 477 478
	}

	if stat.IsDir() {
		if !argDef.Recursive {
Jeromy's avatar
Jeromy committed
479
			return nil, fmt.Errorf(dirNotSupportedFmtStr, fpath, argDef.Name)
480 481
		}
		if !recursive {
482
			return nil, fmt.Errorf(notRecursiveFmtStr, fpath, cmds.RecShort)
483 484 485
		}
	}

Jeromy's avatar
Jeromy committed
486
	return files.NewSerialFile(path.Base(fpath), fpath, hidden, stat)
487
}
488 489

// Inform the user if a file is waiting on input
490
func maybeWrapStdin(f *os.File, msg string) (io.ReadCloser, error) {
491
	isTty, err := isTty(f)
492
	if err != nil {
493
		return nil, err
494 495
	}

496
	if isTty {
497
		return newMessageReader(f, fmt.Sprintf(msg, f.Name())), nil
498 499
	}

500
	return f, nil
501
}
502 503 504 505 506 507 508 509 510 511

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
}
512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528

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)
529
		r.done = true
530 531 532 533 534 535 536 537
	}

	return r.r.Read(b)
}

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