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

import (
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
4
	"fmt"
5
	"os"
6
	"path"
7
	"path/filepath"
8
	"runtime"
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"
14
	logging "gx/ipfs/QmNQynaz7qfriSUJkiEZUrm2Wen1u3Kj9goZzWtrPyu7XR/go-log"
15
	u "gx/ipfs/QmZNVWh8LLjAavuQ2JXuFmuYH3C11xo988vSgp7UQrTRj1/go-ipfs-util"
16 17
)

18 19
var log = logging.Logger("commands/cli")

Jeromy's avatar
cleanup  
Jeromy committed
20 21 22 23 24
// stdinSpecialName is a name applied to the 'stdin' file so we can differentiate
// it from potential 'real' files being passed in. The '*' character is invalid in
// path names and won't appear otherwise.
const stdinSpecialName = "*stdin*"

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

33 34
	optDefs, err := root.GetOptions(path)
	if err != nil {
35
		return nil, cmd, path, err
36 37
	}

38 39 40 41
	req, err := cmds.NewRequest(path, opts, nil, nil, cmd, optDefs)
	if err != nil {
		return nil, cmd, path, err
	}
42

43 44 45 46 47 48 49
	// 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 {
50
			return req, nil, nil, u.ErrCast()
51
		}
52
	}
53

Jeromy's avatar
Jeromy committed
54 55 56 57 58 59 60 61 62 63 64
	// if '--hidden' is provided, enumerate hidden paths
	hiddenOpt := req.Option("hidden")
	hidden := false
	if hiddenOpt != nil {
		hidden, _, err = hiddenOpt.Bool()
		if err != nil {
			return req, nil, nil, u.ErrCast()
		}
	}

	stringArgs, fileArgs, err := parseArgs(stringVals, stdin, cmd.Arguments, recursive, hidden, root)
65
	if err != nil {
66
		return req, cmd, path, err
67
	}
68 69
	req.SetArguments(stringArgs)

70 71 72 73
	if len(fileArgs) > 0 {
		file := files.NewSliceFile("", "", fileArgs)
		req.SetFiles(file)
	}
74 75 76

	err = cmd.CheckArguments(req)
	if err != nil {
77
		return req, cmd, path, err
78 79
	}

80
	return req, cmd, path, nil
81 82
}

Etienne Laurin's avatar
Etienne Laurin committed
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
// 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)
		}
103

Etienne Laurin's avatar
Etienne Laurin committed
104 105 106 107
		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
108
		}
109 110 111 112 113 114
		// 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
115
		if optDef.Type() == cmds.Bool {
116 117 118 119 120 121 122 123 124 125 126 127 128 129
			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
130 131 132 133 134 135 136
			}
		} 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
137 138
		}
	}
139

Etienne Laurin's avatar
Etienne Laurin committed
140 141 142 143
	optDefs, err = root.GetOptions(path)
	if err != nil {
		return
	}
144

Etienne Laurin's avatar
Etienne Laurin committed
145 146 147 148 149 150 151
	consumed := false
	for i, arg := range args {
		switch {
		case consumed:
			// arg was already consumed by the preceding flag
			consumed = false
			continue
152

Etienne Laurin's avatar
Etienne Laurin committed
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
		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
175
			}
Etienne Laurin's avatar
Etienne Laurin committed
176 177 178
			consumed, err = parseFlag(arg[2:], next, len(split) == 2)
			if err != nil {
				return
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
179
			}
Etienne Laurin's avatar
Etienne Laurin committed
180 181
			if !slurped {
				consumed = false
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
182
			}
183

Etienne Laurin's avatar
Etienne Laurin committed
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
		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
208
				end, err = parseFlag(arg[:1], rest, mustUse)
Etienne Laurin's avatar
Etienne Laurin committed
209 210 211 212 213 214 215 216
				if err != nil {
					return
				}
				if end {
					consumed = slurped
					break
				}
			}
217

Etienne Laurin's avatar
Etienne Laurin committed
218 219 220 221 222 223 224 225 226 227
		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
				}
228 229 230 231 232 233 234

				// 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
235 236
			} else {
				stringVals = append(stringVals, arg)
237 238 239 240 241
				if len(path) == 0 {
					// found a typo or early argument
					err = printSuggestions(stringVals, root)
					return
				}
Etienne Laurin's avatar
Etienne Laurin committed
242
			}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
243 244
		}
	}
Etienne Laurin's avatar
Etienne Laurin committed
245
	return
246
}
247

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

Jeromy's avatar
Jeromy committed
250
func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursive, hidden bool, root *cmds.Command) ([]string, []files.File, error) {
251 252 253 254 255
	// ignore stdin on Windows
	if runtime.GOOS == "windows" {
		stdin = nil
	}

256
	// count required argument definitions
257
	numRequired := 0
258
	for _, argDef := range argDefs {
259
		if argDef.Required {
260
			numRequired++
261
		}
262
	}
263

264 265 266
	// 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.
267
	numInputs := len(inputs)
268
	if len(argDefs) > 0 && argDefs[len(argDefs)-1].SupportsStdin && stdin != nil {
269
		numInputs += 1
270 271
	}

272 273 274
	// 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
275
	if notVariadic && len(inputs) > len(argDefs) {
276 277
		err := printSuggestions(inputs, root)
		return nil, nil, err
278 279
	}

280
	stringArgs := make([]string, 0, numInputs)
281

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

285
	for i := 0; i < numInputs; i++ {
286
		argDef := getArgDef(argDefIndex, argDefs)
287

288
		// skip optional argument definitions if there aren't sufficient remaining inputs
289 290 291 292 293
		for numInputs-i <= numRequired && !argDef.Required {
			argDefIndex++
			argDef = getArgDef(argDefIndex, argDefs)
		}
		if argDef.Required {
294
			numRequired--
295
		}
296

297
		fillingVariadic := argDefIndex+1 > len(argDefs)
Jeromy's avatar
Jeromy committed
298 299
		switch argDef.Type {
		case cmds.ArgString:
300
			if len(inputs) > 0 {
301
				stringArgs, inputs = append(stringArgs, inputs[0]), inputs[1:]
302
			} else {
Jeromy's avatar
Jeromy committed
303
				if stdin != nil && argDef.SupportsStdin && !fillingVariadic {
304 305 306 307 308 309
					fname := ""
					istty, err := isTty(stdin)
					if err != nil {
						return nil, nil, err
					}
					if istty {
Jeromy's avatar
cleanup  
Jeromy committed
310
						fname = stdinSpecialName
Jeromy's avatar
Jeromy committed
311
					}
312 313 314

					fileArgs[stdin.Name()] = files.NewReaderFile(fname, "", stdin, nil)
					stdin = nil
Jeromy's avatar
Jeromy committed
315
				}
316
			}
Jeromy's avatar
Jeromy committed
317
		case cmds.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 324
				var file files.File
				var err error
				if fpath == "-" {
325
					if err = printReadInfo(stdin, msgStdinInfo); err == nil {
326 327
						fpath = stdin.Name()
						file = files.NewReaderFile("", fpath, stdin, nil)
328
					}
329 330 331
				} else {
					file, err = appendFile(fpath, argDef, recursive, hidden)
				}
332 333 334 335
				if err != nil {
					return nil, nil, err
				}

Jeromy's avatar
Jeromy committed
336
				fileArgs[fpath] = file
337
			} else {
338 339
				if stdin != nil && argDef.SupportsStdin &&
					argDef.Required && !fillingVariadic {
340 341 342
					if err := printReadInfo(stdin, msgStdinInfo); err != nil {
						return nil, nil, err
					}
343 344
					fpath := stdin.Name()
					fileArgs[fpath] = files.NewReaderFile("", fpath, stdin, nil)
345 346
				} else {
					break
347
				}
348 349
			}
		}
350 351

		argDefIndex++
352 353
	}

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

Jeromy's avatar
Jeromy committed
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
	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
380
}
381

382 383 384 385 386 387 388 389 390 391 392 393 394 395
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
396 397 398
const notRecursiveFmtStr = "'%s' is a directory, use the '-%s' flag to specify directories"
const dirNotSupportedFmtStr = "Invalid path '%s', argument '%s' does not support directories"

Jeromy's avatar
Jeromy committed
399
func appendFile(fpath string, argDef *cmds.Argument, recursive, hidden bool) (files.File, error) {
400 401 402
	if fpath == "." {
		cwd, err := os.Getwd()
		if err != nil {
Jeromy's avatar
Jeromy committed
403
			return nil, err
404 405 406
		}
		fpath = cwd
	}
Jeromy's avatar
Jeromy committed
407

408
	fpath = filepath.ToSlash(filepath.Clean(fpath))
409 410 411 412
	fpath, err := filepath.EvalSymlinks(fpath)
	if err != nil {
		return nil, err
	}
Jeromy's avatar
Jeromy committed
413
	stat, err := os.Lstat(fpath)
414
	if err != nil {
Jeromy's avatar
Jeromy committed
415
		return nil, err
416 417 418 419
	}

	if stat.IsDir() {
		if !argDef.Recursive {
Jeromy's avatar
Jeromy committed
420
			return nil, fmt.Errorf(dirNotSupportedFmtStr, fpath, argDef.Name)
421 422
		}
		if !recursive {
Jeromy's avatar
Jeromy committed
423
			return nil, fmt.Errorf(notRecursiveFmtStr, fpath, cmds.RecShort)
424 425 426
		}
	}

Jeromy's avatar
Jeromy committed
427
	return files.NewSerialFile(path.Base(fpath), fpath, hidden, stat)
428
}
429 430 431

// Inform the user if a file is waiting on input
func printReadInfo(f *os.File, msg string) error {
432
	isTty, err := isTty(f)
433 434 435 436
	if err != nil {
		return err
	}

437
	if isTty {
438 439 440 441 442
		fmt.Fprintf(os.Stderr, msg, f.Name())
	}

	return nil
}
443 444 445 446 447 448 449 450 451 452

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
}