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

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

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

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
16 17 18
// ErrInvalidSubcmd signals when the parse error is not found
var ErrInvalidSubcmd = errors.New("subcommand not found")

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
19 20
// Parse parses the input commandline string (cmd, flags, and args).
// returns the corresponding command Request object.
21
func Parse(input []string, stdin *os.File, root *cmds.Command) (cmds.Request, *cmds.Command, []string, error) {
22 23
	path, input, cmd := parsePath(input, root)
	if len(path) == 0 {
24
		return nil, nil, path, ErrInvalidSubcmd
25 26
	}

27
	opts, stringVals, err := parseOptions(input)
28
	if err != nil {
29
		return nil, cmd, path, err
30 31
	}

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

37 38 39 40 41 42 43 44
	// check to make sure there aren't any undefined options
	for k := range opts {
		if _, found := optDefs[k]; !found {
			err = fmt.Errorf("Unrecognized option: -%s", k)
			return nil, cmd, path, err
		}
	}

45 46 47 48
	req, err := cmds.NewRequest(path, opts, nil, nil, cmd, optDefs)
	if err != nil {
		return nil, cmd, path, err
	}
49

50 51 52 53 54 55 56
	// 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 {
57
			return req, nil, nil, u.ErrCast()
58
		}
59
	}
60

61
	stringArgs, fileArgs, err := parseArgs(stringVals, stdin, cmd.Arguments, recursive)
62
	if err != nil {
63
		return req, cmd, path, err
64
	}
65 66
	req.SetArguments(stringArgs)

67
	file := files.NewSliceFile("", fileArgs)
68
	req.SetFiles(file)
69 70 71

	err = cmd.CheckArguments(req)
	if err != nil {
72
		return req, cmd, path, err
73 74
	}

75
	return req, cmd, path, nil
76 77
}

78 79
// parsePath separates the command path and the opts and args from a command string
// returns command path slice, rest slice, and the corresponding *cmd.Command
80
func parsePath(input []string, root *cmds.Command) ([]string, []string, *cmds.Command) {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
81
	cmd := root
82 83
	path := make([]string, 0, len(input))
	input2 := make([]string, 0, len(input))
84

85
	for i, blob := range input {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
86
		if strings.HasPrefix(blob, "-") {
87 88
			input2 = append(input2, blob)
			continue
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
89
		}
90

91 92
		sub := cmd.Subcommand(blob)
		if sub == nil {
93
			input2 = append(input2, input[i:]...)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
94 95
			break
		}
96
		cmd = sub
97
		path = append(path, blob)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
98
	}
99

100
	return path, input2, cmd
101 102
}

103
// parseOptions parses the raw string values of the given options
104
// returns the parsed options as strings, along with the CLI args
105
func parseOptions(input []string) (map[string]interface{}, []string, error) {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
106
	opts := make(map[string]interface{})
107
	args := []string{}
108

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
109 110
	for i := 0; i < len(input); i++ {
		blob := input[i]
111

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
112 113 114
		if strings.HasPrefix(blob, "-") {
			name := blob[1:]
			value := ""
115

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
116 117 118 119
			// support single and double dash
			if strings.HasPrefix(name, "-") {
				name = name[1:]
			}
120

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
121 122 123 124 125
			if strings.Contains(name, "=") {
				split := strings.SplitN(name, "=", 2)
				name = split[0]
				value = split[1]
			}
126

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
127 128 129
			if _, ok := opts[name]; ok {
				return nil, nil, fmt.Errorf("Duplicate values for option '%s'", name)
			}
130

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
131
			opts[name] = value
132

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
133 134 135 136
		} else {
			args = append(args, blob)
		}
	}
137

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
138
	return opts, args, nil
139
}
140

141
func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursive bool) ([]string, []files.File, error) {
142 143 144 145 146
	// ignore stdin on Windows
	if runtime.GOOS == "windows" {
		stdin = nil
	}

147 148
	// check if stdin is coming from terminal or is being piped in
	if stdin != nil {
149
		if term, err := isTerminal(stdin); err != nil {
150
			return nil, nil, err
151 152
		} else if term {
			stdin = nil // set to nil so we ignore it
153 154 155
		}
	}

156
	// count required argument definitions
157
	numRequired := 0
158
	for _, argDef := range argDefs {
159
		if argDef.Required {
160
			numRequired++
161
		}
162
	}
163

164 165
	// count number of values provided by user
	numInputs := len(inputs)
166
	if stdin != nil {
167
		numInputs += 1
168 169
	}

170 171 172 173 174 175 176
	// 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
	if notVariadic && numInputs > len(argDefs) {
		return nil, nil, fmt.Errorf("Expected %v arguments, got %v", len(argDefs), numInputs)
	}

177
	stringArgs := make([]string, 0, numInputs)
178
	fileArgs := make([]files.File, 0, numInputs)
179 180

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

184
		// skip optional argument definitions if there aren't sufficient remaining inputs
185 186 187 188 189
		for numInputs-i <= numRequired && !argDef.Required {
			argDefIndex++
			argDef = getArgDef(argDefIndex, argDefs)
		}
		if argDef.Required {
190
			numRequired--
191
		}
192

193
		var err error
194 195 196
		if argDef.Type == cmds.ArgString {
			if stdin == nil {
				// add string values
197
				stringArgs, inputs = appendString(stringArgs, inputs)
198

199
			} else if argDef.SupportsStdin {
200
				// if we have a stdin, read it in and use the data as a string value
201
				stringArgs, stdin, err = appendStdinAsString(stringArgs, stdin)
202
				if err != nil {
203
					return nil, nil, err
204 205
				}
			}
206 207 208 209

		} else if argDef.Type == cmds.ArgFile {
			if stdin == nil {
				// treat stringArg values as file paths
210
				fileArgs, inputs, err = appendFile(fileArgs, inputs, argDef, recursive)
211 212 213 214
				if err != nil {
					return nil, nil, err
				}

215
			} else if argDef.SupportsStdin {
216
				// if we have a stdin, create a file from it
217
				fileArgs, stdin = appendStdinAsFile(fileArgs, stdin)
218 219
			}
		}
220 221

		argDefIndex++
222 223
	}

224
	// check to make sure we didn't miss any required arguments
225 226 227 228 229
	if len(argDefs) > argDefIndex {
		for _, argDef := range argDefs[argDefIndex:] {
			if argDef.Required {
				return nil, nil, fmt.Errorf("Argument '%s' is required", argDef.Name)
			}
230 231 232
		}
	}

233
	return stringArgs, fileArgs, nil
234
}
235

236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
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) {
	var buf bytes.Buffer

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

262 263
	input := strings.TrimSpace(buf.String())
	return append(args, strings.Split(input, "\n")...), nil, nil
264 265
}

266
func appendFile(args []files.File, inputs []string, argDef *cmds.Argument, recursive bool) ([]files.File, []string, error) {
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
	path := inputs[0]

	file, err := os.Open(path)
	if err != nil {
		return nil, nil, err
	}

	stat, err := file.Stat()
	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",
				path, argDef.Name)
			return nil, nil, err
		}
		if !recursive {
			err = fmt.Errorf("'%s' is a directory, use the '-%s' flag to specify directories",
				path, cmds.RecShort)
			return nil, nil, err
		}
	}

292
	arg, err := files.NewSerialFile(path, file)
293 294 295 296 297 298 299
	if err != nil {
		return nil, nil, err
	}

	return append(args, arg), inputs[1:], nil
}

300
func appendStdinAsFile(args []files.File, stdin *os.File) ([]files.File, *os.File) {
301
	arg := files.NewReaderFile("", stdin, nil)
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
	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
}