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

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

12 13 14
	cmds "github.com/ipfs/go-ipfs/commands"
	files "github.com/ipfs/go-ipfs/commands/files"
	u "github.com/ipfs/go-ipfs/util"
15 16
)

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

25 26
	optDefs, err := root.GetOptions(path)
	if err != nil {
27
		return nil, cmd, path, err
28 29
	}

30 31 32 33
	req, err := cmds.NewRequest(path, opts, nil, nil, cmd, optDefs)
	if err != nil {
		return nil, cmd, path, err
	}
34

35 36 37 38 39 40 41
	// 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 {
42
			return req, nil, nil, u.ErrCast()
43
		}
44
	}
45

46
	stringArgs, fileArgs, err := parseArgs(stringVals, stdin, cmd.Arguments, recursive, root)
47
	if err != nil {
48
		return req, cmd, path, err
49
	}
50 51
	req.SetArguments(stringArgs)

52
	file := files.NewSliceFile("", "", fileArgs)
53
	req.SetFiles(file)
54 55 56

	err = cmd.CheckArguments(req)
	if err != nil {
57
		return req, cmd, path, err
58 59
	}

60
	return req, cmd, path, nil
61 62
}

Etienne Laurin's avatar
Etienne Laurin committed
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
// 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)
		}
83

Etienne Laurin's avatar
Etienne Laurin committed
84 85 86 87
		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
88
		}
89 90 91 92 93 94
		// 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
95
		if optDef.Type() == cmds.Bool {
96 97 98 99 100 101 102 103 104 105 106 107 108 109
			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
110 111 112 113 114 115 116
			}
		} 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
117 118
		}
	}
119

Etienne Laurin's avatar
Etienne Laurin committed
120 121 122 123
	optDefs, err = root.GetOptions(path)
	if err != nil {
		return
	}
124

Etienne Laurin's avatar
Etienne Laurin committed
125 126 127 128 129 130 131
	consumed := false
	for i, arg := range args {
		switch {
		case consumed:
			// arg was already consumed by the preceding flag
			consumed = false
			continue
132

Etienne Laurin's avatar
Etienne Laurin committed
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
		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
155
			}
Etienne Laurin's avatar
Etienne Laurin committed
156 157 158
			consumed, err = parseFlag(arg[2:], next, len(split) == 2)
			if err != nil {
				return
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
159
			}
Etienne Laurin's avatar
Etienne Laurin committed
160 161
			if !slurped {
				consumed = false
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
162
			}
163

Etienne Laurin's avatar
Etienne Laurin committed
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
		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
				end, err = parseFlag(arg[0:1], rest, mustUse)
				if err != nil {
					return
				}
				if end {
					consumed = slurped
					break
				}
			}
197

Etienne Laurin's avatar
Etienne Laurin committed
198 199 200 201 202 203 204 205 206 207
		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
				}
208 209 210 211 212 213 214

				// 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
215 216 217
			} else {
				stringVals = append(stringVals, arg)
			}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
218 219
		}
	}
Etienne Laurin's avatar
Etienne Laurin committed
220
	return
221
}
222

223
func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursive bool, root *cmds.Command) ([]string, []files.File, error) {
224 225 226 227 228
	// ignore stdin on Windows
	if runtime.GOOS == "windows" {
		stdin = nil
	}

229 230
	// check if stdin is coming from terminal or is being piped in
	if stdin != nil {
231
		if term, err := isTerminal(stdin); err != nil {
232
			return nil, nil, err
233 234
		} else if term {
			stdin = nil // set to nil so we ignore it
235 236 237
		}
	}

238
	// count required argument definitions
239
	numRequired := 0
240
	for _, argDef := range argDefs {
241
		if argDef.Required {
242
			numRequired++
243
		}
244
	}
245

246 247 248
	// 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.
249
	numInputs := len(inputs)
250
	if len(argDefs) > 0 && argDefs[len(argDefs)-1].SupportsStdin && stdin != nil {
251
		numInputs += 1
252 253
	}

254 255 256
	// 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
257
	if notVariadic && len(inputs) > len(argDefs) {
258 259 260 261 262 263 264 265 266
		suggestions := suggestUnknownCmd(inputs, root)

		if len(suggestions) > 1 {
			return nil, nil, fmt.Errorf("Unknown Command \"%s\"\n\nDid you mean any of these?\n\n\t%s", inputs[0], strings.Join(suggestions, "\n\t"))
		} else if len(suggestions) > 0 {
			return nil, nil, fmt.Errorf("Unknown Command \"%s\"\n\nDid you mean this?\n\n\t%s", inputs[0], suggestions[0])
		} else {
			return nil, nil, fmt.Errorf("Unknown Command \"%s\"\n", inputs[0])
		}
267 268
	}

269
	stringArgs := make([]string, 0, numInputs)
270
	fileArgs := make([]files.File, 0, numInputs)
271 272

	argDefIndex := 0 // the index of the current argument definition
273
	for i := 0; i < numInputs; i++ {
274
		argDef := getArgDef(argDefIndex, argDefs)
275

276
		// skip optional argument definitions if there aren't sufficient remaining inputs
277 278 279 280 281
		for numInputs-i <= numRequired && !argDef.Required {
			argDefIndex++
			argDef = getArgDef(argDefIndex, argDefs)
		}
		if argDef.Required {
282
			numRequired--
283
		}
284

285
		var err error
286
		if argDef.Type == cmds.ArgString {
287
			if stdin == nil || !argDef.SupportsStdin {
288
				// add string values
289
				stringArgs, inputs = appendString(stringArgs, inputs)
290

291
			} else {
292 293 294 295 296 297 298 299 300
				if len(inputs) > 0 {
					// don't use stdin if we have inputs
					stdin = nil
				} else {
					// if we have a stdin, read it in and use the data as a string value
					stringArgs, stdin, err = appendStdinAsString(stringArgs, stdin)
					if err != nil {
						return nil, nil, err
					}
301 302
				}
			}
303
		} else if argDef.Type == cmds.ArgFile {
304
			if stdin == nil || !argDef.SupportsStdin {
305
				// treat stringArg values as file paths
306
				fileArgs, inputs, err = appendFile(fileArgs, inputs, argDef, recursive)
307 308 309 310
				if err != nil {
					return nil, nil, err
				}

311
			} else {
312 313 314 315 316 317 318
				if len(inputs) > 0 {
					// don't use stdin if we have inputs
					stdin = nil
				} else {
					// if we have a stdin, create a file from it
					fileArgs, stdin = appendStdinAsFile(fileArgs, stdin)
				}
319 320
			}
		}
321 322

		argDefIndex++
323 324
	}

325
	// check to make sure we didn't miss any required arguments
326 327 328 329 330
	if len(argDefs) > argDefIndex {
		for _, argDef := range argDefs[argDefIndex:] {
			if argDef.Required {
				return nil, nil, fmt.Errorf("Argument '%s' is required", argDef.Name)
			}
331 332 333
		}
	}

334
	return stringArgs, fileArgs, nil
335
}
336

337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
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
}

func appendString(args, inputs []string) ([]string, []string) {
	return append(args, inputs[0]), inputs[1:]
}

func appendStdinAsString(args []string, stdin *os.File) ([]string, *os.File, error) {
356
	buf := new(bytes.Buffer)
357 358 359 360 361 362

	_, err := buf.ReadFrom(stdin)
	if err != nil {
		return nil, nil, err
	}

363 364
	input := strings.TrimSpace(buf.String())
	return append(args, strings.Split(input, "\n")...), nil, nil
365 366
}

367
func appendFile(args []files.File, inputs []string, argDef *cmds.Argument, recursive bool) ([]files.File, []string, error) {
368
	fpath := filepath.ToSlash(filepath.Clean(inputs[0]))
369

370 371 372 373 374 375 376
	if fpath == "." {
		cwd, err := os.Getwd()
		if err != nil {
			return nil, nil, err
		}
		fpath = cwd
	}
Jeromy's avatar
Jeromy committed
377
	stat, err := os.Lstat(fpath)
378 379 380 381 382 383 384
	if err != nil {
		return nil, nil, err
	}

	if stat.IsDir() {
		if !argDef.Recursive {
			err = fmt.Errorf("Invalid path '%s', argument '%s' does not support directories",
385
				fpath, argDef.Name)
386 387 388 389
			return nil, nil, err
		}
		if !recursive {
			err = fmt.Errorf("'%s' is a directory, use the '-%s' flag to specify directories",
390
				fpath, cmds.RecShort)
391 392 393 394
			return nil, nil, err
		}
	}

395
	arg, err := files.NewSerialFile(path.Base(fpath), fpath, stat)
396 397 398 399 400 401
	if err != nil {
		return nil, nil, err
	}
	return append(args, arg), inputs[1:], nil
}

402
func appendStdinAsFile(args []files.File, stdin *os.File) ([]files.File, *os.File) {
403
	arg := files.NewReaderFile("", "", stdin, nil)
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
	return append(args, arg), nil
}

// isTerminal returns true if stdin is a Stdin pipe (e.g. `cat file | ipfs`),
// and false otherwise (e.g. nothing is being piped in, so stdin is
// coming from the terminal)
func isTerminal(stdin *os.File) (bool, error) {
	stat, err := stdin.Stat()
	if err != nil {
		return false, err
	}

	// if stdin is a CharDevice, return true
	return ((stat.Mode() & os.ModeCharDevice) != 0), nil
}