github.com/keltia/go-ipfs@v0.3.8-0.20150909044612-210793031c63/commands/cli/parse.go (about)

     1  package cli
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  	"path"
     8  	"runtime"
     9  	"strings"
    10  
    11  	cmds "github.com/ipfs/go-ipfs/commands"
    12  	files "github.com/ipfs/go-ipfs/commands/files"
    13  	u "github.com/ipfs/go-ipfs/util"
    14  )
    15  
    16  // Parse parses the input commandline string (cmd, flags, and args).
    17  // returns the corresponding command Request object.
    18  func Parse(input []string, stdin *os.File, root *cmds.Command) (cmds.Request, *cmds.Command, []string, error) {
    19  	path, opts, stringVals, cmd, err := parseOpts(input, root)
    20  	if err != nil {
    21  		return nil, nil, path, err
    22  	}
    23  
    24  	optDefs, err := root.GetOptions(path)
    25  	if err != nil {
    26  		return nil, cmd, path, err
    27  	}
    28  
    29  	req, err := cmds.NewRequest(path, opts, nil, nil, cmd, optDefs)
    30  	if err != nil {
    31  		return nil, cmd, path, err
    32  	}
    33  
    34  	// if -r is provided, and it is associated with the package builtin
    35  	// recursive path option, allow recursive file paths
    36  	recursiveOpt := req.Option(cmds.RecShort)
    37  	recursive := false
    38  	if recursiveOpt != nil && recursiveOpt.Definition() == cmds.OptionRecursivePath {
    39  		recursive, _, err = recursiveOpt.Bool()
    40  		if err != nil {
    41  			return req, nil, nil, u.ErrCast()
    42  		}
    43  	}
    44  
    45  	stringArgs, fileArgs, err := parseArgs(stringVals, stdin, cmd.Arguments, recursive, root)
    46  	if err != nil {
    47  		return req, cmd, path, err
    48  	}
    49  	req.SetArguments(stringArgs)
    50  
    51  	file := files.NewSliceFile("", "", fileArgs)
    52  	req.SetFiles(file)
    53  
    54  	err = cmd.CheckArguments(req)
    55  	if err != nil {
    56  		return req, cmd, path, err
    57  	}
    58  
    59  	return req, cmd, path, nil
    60  }
    61  
    62  // Parse a command line made up of sub-commands, short arguments, long arguments and positional arguments
    63  func parseOpts(args []string, root *cmds.Command) (
    64  	path []string,
    65  	opts map[string]interface{},
    66  	stringVals []string,
    67  	cmd *cmds.Command,
    68  	err error,
    69  ) {
    70  	path = make([]string, 0, len(args))
    71  	stringVals = make([]string, 0, len(args))
    72  	optDefs := map[string]cmds.Option{}
    73  	opts = map[string]interface{}{}
    74  	cmd = root
    75  
    76  	// parseFlag checks that a flag is valid and saves it into opts
    77  	// Returns true if the optional second argument is used
    78  	parseFlag := func(name string, arg *string, mustUse bool) (bool, error) {
    79  		if _, ok := opts[name]; ok {
    80  			return false, fmt.Errorf("Duplicate values for option '%s'", name)
    81  		}
    82  
    83  		optDef, found := optDefs[name]
    84  		if !found {
    85  			err = fmt.Errorf("Unrecognized option '%s'", name)
    86  			return false, err
    87  		}
    88  
    89  		if optDef.Type() == cmds.Bool {
    90  			if mustUse {
    91  				return false, fmt.Errorf("Option '%s' takes no arguments, but was passed '%s'", name, *arg)
    92  			}
    93  			opts[name] = ""
    94  			return false, nil
    95  		} else {
    96  			if arg == nil {
    97  				return true, fmt.Errorf("Missing argument for option '%s'", name)
    98  			}
    99  			opts[name] = *arg
   100  			return true, nil
   101  		}
   102  	}
   103  
   104  	optDefs, err = root.GetOptions(path)
   105  	if err != nil {
   106  		return
   107  	}
   108  
   109  	consumed := false
   110  	for i, arg := range args {
   111  		switch {
   112  		case consumed:
   113  			// arg was already consumed by the preceding flag
   114  			consumed = false
   115  			continue
   116  
   117  		case arg == "--":
   118  			// treat all remaining arguments as positional arguments
   119  			stringVals = append(stringVals, args[i+1:]...)
   120  			return
   121  
   122  		case strings.HasPrefix(arg, "--"):
   123  			// arg is a long flag, with an optional argument specified
   124  			// using `=' or in args[i+1]
   125  			var slurped bool
   126  			var next *string
   127  			split := strings.SplitN(arg, "=", 2)
   128  			if len(split) == 2 {
   129  				slurped = false
   130  				arg = split[0]
   131  				next = &split[1]
   132  			} else {
   133  				slurped = true
   134  				if i+1 < len(args) {
   135  					next = &args[i+1]
   136  				} else {
   137  					next = nil
   138  				}
   139  			}
   140  			consumed, err = parseFlag(arg[2:], next, len(split) == 2)
   141  			if err != nil {
   142  				return
   143  			}
   144  			if !slurped {
   145  				consumed = false
   146  			}
   147  
   148  		case strings.HasPrefix(arg, "-") && arg != "-":
   149  			// args is one or more flags in short form, followed by an optional argument
   150  			// all flags except the last one have type bool
   151  			for arg = arg[1:]; len(arg) != 0; arg = arg[1:] {
   152  				var rest *string
   153  				var slurped bool
   154  				mustUse := false
   155  				if len(arg) > 1 {
   156  					slurped = false
   157  					str := arg[1:]
   158  					if len(str) > 0 && str[0] == '=' {
   159  						str = str[1:]
   160  						mustUse = true
   161  					}
   162  					rest = &str
   163  				} else {
   164  					slurped = true
   165  					if i+1 < len(args) {
   166  						rest = &args[i+1]
   167  					} else {
   168  						rest = nil
   169  					}
   170  				}
   171  				var end bool
   172  				end, err = parseFlag(arg[0:1], rest, mustUse)
   173  				if err != nil {
   174  					return
   175  				}
   176  				if end {
   177  					consumed = slurped
   178  					break
   179  				}
   180  			}
   181  
   182  		default:
   183  			// arg is a sub-command or a positional argument
   184  			sub := cmd.Subcommand(arg)
   185  			if sub != nil {
   186  				cmd = sub
   187  				path = append(path, arg)
   188  				optDefs, err = root.GetOptions(path)
   189  				if err != nil {
   190  					return
   191  				}
   192  			} else {
   193  				stringVals = append(stringVals, arg)
   194  			}
   195  		}
   196  	}
   197  	return
   198  }
   199  
   200  func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursive bool, root *cmds.Command) ([]string, []files.File, error) {
   201  	// ignore stdin on Windows
   202  	if runtime.GOOS == "windows" {
   203  		stdin = nil
   204  	}
   205  
   206  	// check if stdin is coming from terminal or is being piped in
   207  	if stdin != nil {
   208  		if term, err := isTerminal(stdin); err != nil {
   209  			return nil, nil, err
   210  		} else if term {
   211  			stdin = nil // set to nil so we ignore it
   212  		}
   213  	}
   214  
   215  	// count required argument definitions
   216  	numRequired := 0
   217  	for _, argDef := range argDefs {
   218  		if argDef.Required {
   219  			numRequired++
   220  		}
   221  	}
   222  
   223  	// count number of values provided by user.
   224  	// if there is at least one ArgDef, we can safely trigger the inputs loop
   225  	// below to parse stdin.
   226  	numInputs := len(inputs)
   227  	if len(argDefs) > 0 && argDefs[len(argDefs)-1].SupportsStdin && stdin != nil {
   228  		numInputs += 1
   229  	}
   230  
   231  	// if we have more arg values provided than argument definitions,
   232  	// and the last arg definition is not variadic (or there are no definitions), return an error
   233  	notVariadic := len(argDefs) == 0 || !argDefs[len(argDefs)-1].Variadic
   234  	if notVariadic && len(inputs) > len(argDefs) {
   235  		suggestions := suggestUnknownCmd(inputs, root)
   236  
   237  		if len(suggestions) > 1 {
   238  			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"))
   239  		} else if len(suggestions) > 0 {
   240  			return nil, nil, fmt.Errorf("Unknown Command \"%s\"\n\nDid you mean this?\n\n\t%s", inputs[0], suggestions[0])
   241  		} else {
   242  			return nil, nil, fmt.Errorf("Unknown Command \"%s\"\n", inputs[0])
   243  		}
   244  	}
   245  
   246  	stringArgs := make([]string, 0, numInputs)
   247  	fileArgs := make([]files.File, 0, numInputs)
   248  
   249  	argDefIndex := 0 // the index of the current argument definition
   250  	for i := 0; i < numInputs; i++ {
   251  		argDef := getArgDef(argDefIndex, argDefs)
   252  
   253  		// skip optional argument definitions if there aren't sufficient remaining inputs
   254  		for numInputs-i <= numRequired && !argDef.Required {
   255  			argDefIndex++
   256  			argDef = getArgDef(argDefIndex, argDefs)
   257  		}
   258  		if argDef.Required {
   259  			numRequired--
   260  		}
   261  
   262  		var err error
   263  		if argDef.Type == cmds.ArgString {
   264  			if stdin == nil || !argDef.SupportsStdin {
   265  				// add string values
   266  				stringArgs, inputs = appendString(stringArgs, inputs)
   267  
   268  			} else {
   269  				if len(inputs) > 0 {
   270  					// don't use stdin if we have inputs
   271  					stdin = nil
   272  				} else {
   273  					// if we have a stdin, read it in and use the data as a string value
   274  					stringArgs, stdin, err = appendStdinAsString(stringArgs, stdin)
   275  					if err != nil {
   276  						return nil, nil, err
   277  					}
   278  				}
   279  			}
   280  		} else if argDef.Type == cmds.ArgFile {
   281  			if stdin == nil || !argDef.SupportsStdin {
   282  				// treat stringArg values as file paths
   283  				fileArgs, inputs, err = appendFile(fileArgs, inputs, argDef, recursive)
   284  				if err != nil {
   285  					return nil, nil, err
   286  				}
   287  
   288  			} else {
   289  				if len(inputs) > 0 {
   290  					// don't use stdin if we have inputs
   291  					stdin = nil
   292  				} else {
   293  					// if we have a stdin, create a file from it
   294  					fileArgs, stdin = appendStdinAsFile(fileArgs, stdin)
   295  				}
   296  			}
   297  		}
   298  
   299  		argDefIndex++
   300  	}
   301  
   302  	// check to make sure we didn't miss any required arguments
   303  	if len(argDefs) > argDefIndex {
   304  		for _, argDef := range argDefs[argDefIndex:] {
   305  			if argDef.Required {
   306  				return nil, nil, fmt.Errorf("Argument '%s' is required", argDef.Name)
   307  			}
   308  		}
   309  	}
   310  
   311  	return stringArgs, fileArgs, nil
   312  }
   313  
   314  func getArgDef(i int, argDefs []cmds.Argument) *cmds.Argument {
   315  	if i < len(argDefs) {
   316  		// get the argument definition (usually just argDefs[i])
   317  		return &argDefs[i]
   318  
   319  	} else if len(argDefs) > 0 {
   320  		// but if i > len(argDefs) we use the last argument definition)
   321  		return &argDefs[len(argDefs)-1]
   322  	}
   323  
   324  	// only happens if there aren't any definitions
   325  	return nil
   326  }
   327  
   328  func appendString(args, inputs []string) ([]string, []string) {
   329  	return append(args, inputs[0]), inputs[1:]
   330  }
   331  
   332  func appendStdinAsString(args []string, stdin *os.File) ([]string, *os.File, error) {
   333  	buf := new(bytes.Buffer)
   334  
   335  	_, err := buf.ReadFrom(stdin)
   336  	if err != nil {
   337  		return nil, nil, err
   338  	}
   339  
   340  	input := strings.TrimSpace(buf.String())
   341  	return append(args, strings.Split(input, "\n")...), nil, nil
   342  }
   343  
   344  func appendFile(args []files.File, inputs []string, argDef *cmds.Argument, recursive bool) ([]files.File, []string, error) {
   345  	fpath := inputs[0]
   346  
   347  	if fpath == "." {
   348  		cwd, err := os.Getwd()
   349  		if err != nil {
   350  			return nil, nil, err
   351  		}
   352  		fpath = cwd
   353  	}
   354  	stat, err := os.Lstat(fpath)
   355  	if err != nil {
   356  		return nil, nil, err
   357  	}
   358  
   359  	if stat.IsDir() {
   360  		if !argDef.Recursive {
   361  			err = fmt.Errorf("Invalid path '%s', argument '%s' does not support directories",
   362  				fpath, argDef.Name)
   363  			return nil, nil, err
   364  		}
   365  		if !recursive {
   366  			err = fmt.Errorf("'%s' is a directory, use the '-%s' flag to specify directories",
   367  				fpath, cmds.RecShort)
   368  			return nil, nil, err
   369  		}
   370  	}
   371  
   372  	arg, err := files.NewSerialFile(path.Base(fpath), fpath, stat)
   373  	if err != nil {
   374  		return nil, nil, err
   375  	}
   376  	return append(args, arg), inputs[1:], nil
   377  }
   378  
   379  func appendStdinAsFile(args []files.File, stdin *os.File) ([]files.File, *os.File) {
   380  	arg := files.NewReaderFile("", "", stdin, nil)
   381  	return append(args, arg), nil
   382  }
   383  
   384  // isTerminal returns true if stdin is a Stdin pipe (e.g. `cat file | ipfs`),
   385  // and false otherwise (e.g. nothing is being piped in, so stdin is
   386  // coming from the terminal)
   387  func isTerminal(stdin *os.File) (bool, error) {
   388  	stat, err := stdin.Stat()
   389  	if err != nil {
   390  		return false, err
   391  	}
   392  
   393  	// if stdin is a CharDevice, return true
   394  	return ((stat.Mode() & os.ModeCharDevice) != 0), nil
   395  }