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

import (
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
4
	"fmt"
5
	"io"
6
	"os"
7
	"path"
8
	"path/filepath"
Jeromy's avatar
Jeromy committed
9
	"sort"
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
10
	"strings"
11

12 13
	cmds "github.com/ipfs/go-ipfs/commands"
	files "github.com/ipfs/go-ipfs/commands/files"
Dominic Della Valle's avatar
Dominic Della Valle committed
14

Jeromy's avatar
Jeromy committed
15
	logging "gx/ipfs/QmSpJByNKFX1sCsHBEp3R73FL4NF6FnQTEGyNAXHm2GS52/go-log"
16
	u "gx/ipfs/QmWbjfz3u6HkAdPh34dgPchGbQjob6LXLhAeCGii2TX69n/go-ipfs-util"
Dominic Della Valle's avatar
Dominic Della Valle committed
17
	osh "gx/ipfs/QmXuBJ7DR6k3rmUEKtvVMhwjmXDuJgXXPUt4LQXKBMsU93/go-os-helper"
18 19
)

20 21
var log = logging.Logger("commands/cli")

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
22 23
// Parse parses the input commandline string (cmd, flags, and args).
// returns the corresponding command Request object.
24
func Parse(input []string, stdin *os.File, root *cmds.Command) (cmds.Request, *cmds.Command, []string, error) {
Etienne Laurin's avatar
Etienne Laurin committed
25
	path, opts, stringVals, cmd, err := parseOpts(input, root)
26
	if err != nil {
Etienne Laurin's avatar
Etienne Laurin committed
27
		return nil, nil, path, err
28 29
	}

30 31
	optDefs, err := root.GetOptions(path)
	if err != nil {
32
		return nil, cmd, path, err
33 34
	}

35 36 37 38
	req, err := cmds.NewRequest(path, opts, nil, nil, cmd, optDefs)
	if err != nil {
		return nil, cmd, path, err
	}
39

40 41 42 43 44 45 46 47 48 49
	// 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.
	if len(path) == 2 && path[0] == "bootstrap" {
		if (path[1] == "add" && opts["default"] == true) ||
			(path[1] == "rm" && opts["all"] == true) {
			stdin = nil
		}
	}

50
	stringArgs, fileArgs, err := ParseArgs(req, stringVals, stdin, cmd.Arguments, root)
51
	if err != nil {
52
		return req, cmd, path, err
53
	}
54 55
	req.SetArguments(stringArgs)

56 57 58 59
	if len(fileArgs) > 0 {
		file := files.NewSliceFile("", "", fileArgs)
		req.SetFiles(file)
	}
60 61 62

	err = cmd.CheckArguments(req)
	if err != nil {
63
		return req, cmd, path, err
64 65
	}

66
	return req, cmd, path, nil
67 68
}

69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
func ParseArgs(req cmds.Request, inputs []string, stdin *os.File, argDefs []cmds.Argument, root *cmds.Command) ([]string, []files.File, error) {
	var err error

	// if -r is provided, and it is associated with the package builtin
	// recursive path option, allow recursive file paths
	recursiveOpt := req.Option(cmds.RecShort)
	recursive := false
	if recursiveOpt != nil && recursiveOpt.Definition() == cmds.OptionRecursivePath {
		recursive, _, err = recursiveOpt.Bool()
		if err != nil {
			return nil, nil, u.ErrCast()
		}
	}

	// if '--hidden' is provided, enumerate hidden paths
	hiddenOpt := req.Option("hidden")
	hidden := false
	if hiddenOpt != nil {
		hidden, _, err = hiddenOpt.Bool()
		if err != nil {
			return nil, nil, u.ErrCast()
		}
	}
	return parseArgs(inputs, stdin, argDefs, recursive, hidden, root)
}

Etienne Laurin's avatar
Etienne Laurin committed
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
// Parse a command line made up of sub-commands, short arguments, long arguments and positional arguments
func parseOpts(args []string, root *cmds.Command) (
	path []string,
	opts map[string]interface{},
	stringVals []string,
	cmd *cmds.Command,
	err error,
) {
	path = make([]string, 0, len(args))
	stringVals = make([]string, 0, len(args))
	optDefs := map[string]cmds.Option{}
	opts = map[string]interface{}{}
	cmd = root

	// parseFlag checks that a flag is valid and saves it into opts
	// Returns true if the optional second argument is used
	parseFlag := func(name string, arg *string, mustUse bool) (bool, error) {
		if _, ok := opts[name]; ok {
			return false, fmt.Errorf("Duplicate values for option '%s'", name)
		}
115

Etienne Laurin's avatar
Etienne Laurin committed
116 117 118 119
		optDef, found := optDefs[name]
		if !found {
			err = fmt.Errorf("Unrecognized option '%s'", name)
			return false, err
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
120
		}
121 122 123 124 125 126
		// mustUse implies that you must use the argument given after the '='
		// eg. -r=true means you must take true into consideration
		//		mustUse == true in the above case
		// eg. ipfs -r <file> means disregard <file> since there is no '='
		//		mustUse == false in the above situation
		//arg == nil implies the flag was specified without an argument
Etienne Laurin's avatar
Etienne Laurin committed
127
		if optDef.Type() == cmds.Bool {
128 129 130 131 132 133 134 135 136 137 138 139 140 141
			if arg == nil || !mustUse {
				opts[name] = true
				return false, nil
			}
			argVal := strings.ToLower(*arg)
			switch argVal {
			case "true":
				opts[name] = true
				return true, nil
			case "false":
				opts[name] = false
				return true, nil
			default:
				return true, fmt.Errorf("Option '%s' takes true/false arguments, but was passed '%s'", name, argVal)
Etienne Laurin's avatar
Etienne Laurin committed
142 143 144 145 146 147 148
			}
		} else {
			if arg == nil {
				return true, fmt.Errorf("Missing argument for option '%s'", name)
			}
			opts[name] = *arg
			return true, nil
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
149 150
		}
	}
151

Etienne Laurin's avatar
Etienne Laurin committed
152 153 154 155
	optDefs, err = root.GetOptions(path)
	if err != nil {
		return
	}
156

Etienne Laurin's avatar
Etienne Laurin committed
157 158 159 160 161 162 163
	consumed := false
	for i, arg := range args {
		switch {
		case consumed:
			// arg was already consumed by the preceding flag
			consumed = false
			continue
164

Etienne Laurin's avatar
Etienne Laurin committed
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
		case arg == "--":
			// treat all remaining arguments as positional arguments
			stringVals = append(stringVals, args[i+1:]...)
			return

		case strings.HasPrefix(arg, "--"):
			// arg is a long flag, with an optional argument specified
			// using `=' or in args[i+1]
			var slurped bool
			var next *string
			split := strings.SplitN(arg, "=", 2)
			if len(split) == 2 {
				slurped = false
				arg = split[0]
				next = &split[1]
			} else {
				slurped = true
				if i+1 < len(args) {
					next = &args[i+1]
				} else {
					next = nil
				}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
187
			}
Etienne Laurin's avatar
Etienne Laurin committed
188 189 190
			consumed, err = parseFlag(arg[2:], next, len(split) == 2)
			if err != nil {
				return
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
191
			}
Etienne Laurin's avatar
Etienne Laurin committed
192 193
			if !slurped {
				consumed = false
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
194
			}
195

Etienne Laurin's avatar
Etienne Laurin committed
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
		case strings.HasPrefix(arg, "-") && arg != "-":
			// args is one or more flags in short form, followed by an optional argument
			// all flags except the last one have type bool
			for arg = arg[1:]; len(arg) != 0; arg = arg[1:] {
				var rest *string
				var slurped bool
				mustUse := false
				if len(arg) > 1 {
					slurped = false
					str := arg[1:]
					if len(str) > 0 && str[0] == '=' {
						str = str[1:]
						mustUse = true
					}
					rest = &str
				} else {
					slurped = true
					if i+1 < len(args) {
						rest = &args[i+1]
					} else {
						rest = nil
					}
				}
				var end bool
rht's avatar
rht committed
220
				end, err = parseFlag(arg[:1], rest, mustUse)
Etienne Laurin's avatar
Etienne Laurin committed
221 222 223 224 225 226 227 228
				if err != nil {
					return
				}
				if end {
					consumed = slurped
					break
				}
			}
229

Etienne Laurin's avatar
Etienne Laurin committed
230 231 232 233 234 235 236 237 238 239
		default:
			// arg is a sub-command or a positional argument
			sub := cmd.Subcommand(arg)
			if sub != nil {
				cmd = sub
				path = append(path, arg)
				optDefs, err = root.GetOptions(path)
				if err != nil {
					return
				}
240 241 242 243 244 245 246

				// If we've come across an external binary call, pass all the remaining
				// arguments on to it
				if cmd.External {
					stringVals = append(stringVals, args[i+1:]...)
					return
				}
Etienne Laurin's avatar
Etienne Laurin committed
247 248
			} else {
				stringVals = append(stringVals, arg)
249 250 251 252 253
				if len(path) == 0 {
					// found a typo or early argument
					err = printSuggestions(stringVals, root)
					return
				}
Etienne Laurin's avatar
Etienne Laurin committed
254
			}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
255 256
		}
	}
Etienne Laurin's avatar
Etienne Laurin committed
257
	return
258
}
259

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

Jeromy's avatar
Jeromy committed
262
func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursive, hidden bool, root *cmds.Command) ([]string, []files.File, error) {
263
	// ignore stdin on Windows
Dominic Della Valle's avatar
Dominic Della Valle committed
264
	if osh.IsWindows() {
265 266 267
		stdin = nil
	}

268
	// count required argument definitions
269
	numRequired := 0
270
	for _, argDef := range argDefs {
271
		if argDef.Required {
272
			numRequired++
273
		}
274
	}
275

276 277 278
	// 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.
279
	numInputs := len(inputs)
280
	if len(argDefs) > 0 && argDefs[len(argDefs)-1].SupportsStdin && stdin != nil {
281
		numInputs += 1
282 283
	}

284 285 286
	// 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
287
	if notVariadic && len(inputs) > len(argDefs) {
288 289
		err := printSuggestions(inputs, root)
		return nil, nil, err
290 291
	}

292
	stringArgs := make([]string, 0, numInputs)
293

Jeromy's avatar
Jeromy committed
294
	fileArgs := make(map[string]files.File)
295
	argDefIndex := 0 // the index of the current argument definition
Jeromy's avatar
Jeromy committed
296

297
	for i := 0; i < numInputs; i++ {
298
		argDef := getArgDef(argDefIndex, argDefs)
299

300
		// skip optional argument definitions if there aren't sufficient remaining inputs
301 302 303 304 305
		for numInputs-i <= numRequired && !argDef.Required {
			argDefIndex++
			argDef = getArgDef(argDefIndex, argDefs)
		}
		if argDef.Required {
306
			numRequired--
307
		}
308

309
		fillingVariadic := argDefIndex+1 > len(argDefs)
Jeromy's avatar
Jeromy committed
310 311
		switch argDef.Type {
		case cmds.ArgString:
312
			if len(inputs) > 0 {
313
				stringArgs, inputs = append(stringArgs, inputs[0]), inputs[1:]
Jeromy's avatar
Jeromy committed
314
			} else if stdin != nil && argDef.SupportsStdin && !fillingVariadic {
315 316
				if r, err := maybeWrapStdin(stdin, msgStdinInfo); err == nil {
					fileArgs[stdin.Name()] = files.NewReaderFile("stdin", "", r, nil)
Jeromy's avatar
Jeromy committed
317
					stdin = nil
Jeromy's avatar
Jeromy committed
318
				}
319
			}
Jeromy's avatar
Jeromy committed
320
		case cmds.ArgFile:
321
			if len(inputs) > 0 {
322
				// treat stringArg values as file paths
Jeromy's avatar
Jeromy committed
323 324
				fpath := inputs[0]
				inputs = inputs[1:]
325 326
				var file files.File
				if fpath == "-" {
327 328 329
					r, err := maybeWrapStdin(stdin, msgStdinInfo)
					if err != nil {
						return nil, nil, err
330
					}
331 332 333

					fpath = stdin.Name()
					file = files.NewReaderFile("", fpath, r, nil)
334
				} else {
335 336 337 338 339 340
					nf, err := appendFile(fpath, argDef, recursive, hidden)
					if err != nil {
						return nil, nil, err
					}

					file = nf
341 342
				}

Jeromy's avatar
Jeromy committed
343
				fileArgs[fpath] = file
Jeromy's avatar
Jeromy committed
344 345
			} else if stdin != nil && argDef.SupportsStdin &&
				argDef.Required && !fillingVariadic {
346 347
				r, err := maybeWrapStdin(stdin, msgStdinInfo)
				if err != nil {
Jeromy's avatar
Jeromy committed
348
					return nil, nil, err
349
				}
350

Jeromy's avatar
Jeromy committed
351
				fpath := stdin.Name()
352
				fileArgs[fpath] = files.NewReaderFile("", fpath, r, nil)
353 354
			}
		}
355 356

		argDefIndex++
357 358
	}

359
	// check to make sure we didn't miss any required arguments
360 361 362 363 364
	if len(argDefs) > argDefIndex {
		for _, argDef := range argDefs[argDefIndex:] {
			if argDef.Required {
				return nil, nil, fmt.Errorf("Argument '%s' is required", argDef.Name)
			}
365 366 367
		}
	}

Jeromy's avatar
Jeromy committed
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
	return stringArgs, filesMapToSortedArr(fileArgs), nil
}

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

387 388 389 390 391 392 393 394 395 396 397 398 399 400
func getArgDef(i int, argDefs []cmds.Argument) *cmds.Argument {
	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
401 402
const notRecursiveFmtStr = "'%s' is a directory, use the '-%s' flag to specify directories"
const dirNotSupportedFmtStr = "Invalid path '%s', argument '%s' does not support directories"
Dominic Della Valle's avatar
Dominic Della Valle committed
403
const winDriveLetterFmtStr = "%q is a drive letter, not a drive path"
Jeromy's avatar
Jeromy committed
404

Jeromy's avatar
Jeromy committed
405
func appendFile(fpath string, argDef *cmds.Argument, recursive, hidden bool) (files.File, error) {
Dominic Della Valle's avatar
Dominic Della Valle committed
406 407 408 409 410 411 412 413 414 415 416
	// resolve Windows relative dot paths like `X:.\somepath`
	if osh.IsWindows() {
		if len(fpath) >= 3 && fpath[1:3] == ":." {
			var err error
			fpath, err = filepath.Abs(fpath)
			if err != nil {
				return nil, err
			}
		}
	}

417 418 419
	if fpath == "." {
		cwd, err := os.Getwd()
		if err != nil {
Jeromy's avatar
Jeromy committed
420
			return nil, err
421
		}
422 423 424 425
		cwd, err = filepath.EvalSymlinks(cwd)
		if err != nil {
			return nil, err
		}
426 427
		fpath = cwd
	}
Jeromy's avatar
Jeromy committed
428

Dominic Della Valle's avatar
Dominic Della Valle committed
429
	fpath = filepath.Clean(fpath)
430

Jeromy's avatar
Jeromy committed
431
	stat, err := os.Lstat(fpath)
432
	if err != nil {
Jeromy's avatar
Jeromy committed
433
		return nil, err
434 435 436 437
	}

	if stat.IsDir() {
		if !argDef.Recursive {
Jeromy's avatar
Jeromy committed
438
			return nil, fmt.Errorf(dirNotSupportedFmtStr, fpath, argDef.Name)
439 440
		}
		if !recursive {
Jeromy's avatar
Jeromy committed
441
			return nil, fmt.Errorf(notRecursiveFmtStr, fpath, cmds.RecShort)
442 443 444
		}
	}

Dominic Della Valle's avatar
Dominic Della Valle committed
445 446 447 448
	if osh.IsWindows() {
		return windowsParseFile(fpath, hidden, stat)
	}

Jeromy's avatar
Jeromy committed
449
	return files.NewSerialFile(path.Base(fpath), fpath, hidden, stat)
450
}
451 452

// Inform the user if a file is waiting on input
453
func maybeWrapStdin(f *os.File, msg string) (io.ReadCloser, error) {
454
	isTty, err := isTty(f)
455
	if err != nil {
456
		return nil, err
457 458
	}

459
	if isTty {
460
		return newMessageReader(f, fmt.Sprintf(msg, f.Name())), nil
461 462
	}

463
	return f, nil
464
}
465 466 467 468 469 470 471 472 473 474

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
}
475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491

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)
492
		r.done = true
493 494 495 496 497 498 499 500
	}

	return r.r.Read(b)
}

func (r *messageReader) Close() error {
	return r.r.Close()
}
Dominic Della Valle's avatar
Dominic Della Valle committed
501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529

func windowsParseFile(fpath string, hidden bool, stat os.FileInfo) (files.File, error) {
	// special cases for Windows drive roots i.e. `X:\` and their long form `\\?\X:\`
	// drive path must be preserved as `X:\` (or it's longform) and not converted to `X:`, `X:.`, `\`, or `/` here
	switch len(fpath) {
	case 3:
		// `X:` is cleaned to `X:.` which may not be the expected behaviour by the user, they'll need to provide more specific input
		if fpath[1:3] == ":." {
			return nil, fmt.Errorf(winDriveLetterFmtStr, fpath[:2])
		}
		// `X:\` needs to preserve the `\`, path.Base(filepath.ToSlash(fpath)) results in `X:` which is not valid
		if fpath[1:3] == ":\\" {
			return files.NewSerialFile(fpath, fpath, hidden, stat)
		}
	case 6:
		// `\\?\X:` long prefix form of `X:`, still ambiguous
		if fpath[:4] == "\\\\?\\" && fpath[5] == ':' {
			return nil, fmt.Errorf(winDriveLetterFmtStr, fpath)
		}
	case 7:
		// `\\?\X:\` long prefix form is translated into short form `X:\`
		if fpath[:4] == "\\\\?\\" && fpath[5] == ':' && fpath[6] == '\\' {
			fpath = string(fpath[4]) + ":\\"
			return files.NewSerialFile(fpath, fpath, hidden, stat)
		}
	}

	return files.NewSerialFile(path.Base(filepath.ToSlash(fpath)), fpath, hidden, stat)
}