parse.go 13.5 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
	cmds "github.com/ipfs/go-ipfs/commands"
13
	u "gx/ipfs/QmSU6eubNdhXjFBJBSksTp8kv8YRub8mGAPv8tVJHmL2EU/go-ipfs-util"
Jeromy's avatar
Jeromy committed
14
	logging "gx/ipfs/QmSpJByNKFX1sCsHBEp3R73FL4NF6FnQTEGyNAXHm2GS52/go-log"
Steven Allen's avatar
Steven Allen committed
15 16
	"gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit"
	"gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit/files"
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)

63
	return req, cmd, path, err
64 65
}

Jan Winkelmann's avatar
Jan Winkelmann committed
66
func ParseArgs(req cmds.Request, inputs []string, stdin *os.File, argDefs []cmdkit.Argument, root *cmds.Command) ([]string, []files.File, error) {
67 68 69 70
	var err error

	// if -r is provided, and it is associated with the package builtin
	// recursive path option, allow recursive file paths
Jan Winkelmann's avatar
Jan Winkelmann committed
71
	recursiveOpt := req.Option(cmdkit.RecShort)
72
	recursive := false
Jan Winkelmann's avatar
Jan Winkelmann committed
73
	if recursiveOpt != nil && recursiveOpt.Definition() == cmdkit.OptionRecursivePath {
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
		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
92 93 94 95 96 97 98 99 100 101
// 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))
Jan Winkelmann's avatar
Jan Winkelmann committed
102
	optDefs := map[string]cmdkit.Option{}
Etienne Laurin's avatar
Etienne Laurin committed
103 104 105 106 107 108 109 110 111
	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)
		}
112

Etienne Laurin's avatar
Etienne Laurin committed
113 114 115 116
		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
117
		}
118 119 120 121 122 123
		// 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
Jan Winkelmann's avatar
Jan Winkelmann committed
124
		if optDef.Type() == cmdkit.Bool {
125 126 127 128 129 130 131 132 133 134 135 136 137 138
			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
139 140 141 142 143 144 145
			}
		} 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
146 147
		}
	}
148

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

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

Etienne Laurin's avatar
Etienne Laurin committed
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
		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
184
			}
Etienne Laurin's avatar
Etienne Laurin committed
185 186 187
			consumed, err = parseFlag(arg[2:], next, len(split) == 2)
			if err != nil {
				return
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
188
			}
Etienne Laurin's avatar
Etienne Laurin committed
189 190
			if !slurped {
				consumed = false
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
191
			}
192

Etienne Laurin's avatar
Etienne Laurin committed
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
		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
217
				end, err = parseFlag(arg[:1], rest, mustUse)
Etienne Laurin's avatar
Etienne Laurin committed
218 219 220 221 222 223 224 225
				if err != nil {
					return
				}
				if end {
					consumed = slurped
					break
				}
			}
226

Etienne Laurin's avatar
Etienne Laurin committed
227 228 229 230 231 232 233 234 235 236
		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
				}
237 238 239 240 241 242 243

				// 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
244 245
			} else {
				stringVals = append(stringVals, arg)
246 247 248 249 250
				if len(path) == 0 {
					// found a typo or early argument
					err = printSuggestions(stringVals, root)
					return
				}
Etienne Laurin's avatar
Etienne Laurin committed
251
			}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
252 253
		}
	}
Etienne Laurin's avatar
Etienne Laurin committed
254
	return
255
}
256

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

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

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

273 274 275
	// 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.
276
	numInputs := len(inputs)
277
	if len(argDefs) > 0 && argDefs[len(argDefs)-1].SupportsStdin && stdin != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
278
		numInputs++
279 280
	}

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

289
	stringArgs := make([]string, 0, numInputs)
290

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

294
	for i := 0; i < numInputs; i++ {
295
		argDef := getArgDef(argDefIndex, argDefs)
296

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

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

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

					file = nf
338 339
				}

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

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

		argDefIndex++
354 355
	}

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

Jeromy's avatar
Jeromy committed
365 366 367 368 369
	return stringArgs, filesMapToSortedArr(fileArgs), nil
}

func filesMapToSortedArr(fs map[string]files.File) []files.File {
	var names []string
Jan Winkelmann's avatar
Jan Winkelmann committed
370
	for name := range fs {
Jeromy's avatar
Jeromy committed
371 372 373 374 375 376 377 378 379 380 381
		names = append(names, name)
	}

	sort.Strings(names)

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

	return out
382
}
383

Jan Winkelmann's avatar
Jan Winkelmann committed
384
func getArgDef(i int, argDefs []cmdkit.Argument) *cmdkit.Argument {
385 386 387 388 389 390 391 392 393 394 395 396 397
	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
398 399
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
400
const winDriveLetterFmtStr = "%q is a drive letter, not a drive path"
Jeromy's avatar
Jeromy committed
401

Jan Winkelmann's avatar
Jan Winkelmann committed
402
func appendFile(fpath string, argDef *cmdkit.Argument, recursive, hidden bool) (files.File, error) {
Dominic Della Valle's avatar
Dominic Della Valle committed
403 404 405 406 407 408 409 410 411 412 413
	// 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
			}
		}
	}

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

Dominic Della Valle's avatar
Dominic Della Valle committed
426
	fpath = filepath.Clean(fpath)
427

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

	if stat.IsDir() {
		if !argDef.Recursive {
Jeromy's avatar
Jeromy committed
435
			return nil, fmt.Errorf(dirNotSupportedFmtStr, fpath, argDef.Name)
436 437
		}
		if !recursive {
Jan Winkelmann's avatar
Jan Winkelmann committed
438
			return nil, fmt.Errorf(notRecursiveFmtStr, fpath, cmdkit.RecShort)
439 440 441
		}
	}

Dominic Della Valle's avatar
Dominic Della Valle committed
442 443 444 445
	if osh.IsWindows() {
		return windowsParseFile(fpath, hidden, stat)
	}

Jeromy's avatar
Jeromy committed
446
	return files.NewSerialFile(path.Base(fpath), fpath, hidden, stat)
447
}
448 449

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

456
	if isTty {
457
		return newMessageReader(f, fmt.Sprintf(msg, f.Name())), nil
458 459
	}

460
	return f, nil
461
}
462 463 464 465 466 467 468 469 470 471

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
}
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488

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)
489
		r.done = true
490 491 492 493 494 495 496 497
	}

	return r.r.Read(b)
}

func (r *messageReader) Close() error {
	return r.r.Close()
}
Dominic Della Valle's avatar
Dominic Della Valle committed
498 499 500 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

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