parse.go 9.05 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
	fp "path"
9
	"runtime"
10
	"sort"
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
11
	"strings"
12

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

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

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

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

39 40 41 42 43 44 45 46
	// 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
		}
	}

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

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

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

69
	file := &cmdsFiles.SliceFile{"", fileArgs}
70
	req.SetFiles(file)
71 72 73

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

77
	return req, cmd, path, nil
78 79
}

80 81
// 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
82
func parsePath(input []string, root *cmds.Command) ([]string, []string, *cmds.Command) {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
83
	cmd := root
84 85
	path := make([]string, 0, len(input))
	input2 := make([]string, 0, len(input))
86

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

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

102
	return path, input2, cmd
103 104
}

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

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

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

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

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

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

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
133
			opts[name] = value
134

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

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
140
	return opts, args, nil
141
}
142

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

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

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

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

172 173 174 175 176 177 178
	// 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)
	}

179
	stringArgs := make([]string, 0, numInputs)
180
	fileArgs := make([]cmdsFiles.File, 0, numInputs)
181 182

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

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

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

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

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

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

		argDefIndex++
224 225
	}

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

235
	return stringArgs, fileArgs, nil
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 262 263 264 265 266
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
	}

	return append(args, buf.String()), nil, nil
}

267
func appendFile(args []cmdsFiles.File, inputs []string, argDef *cmds.Argument, recursive bool) ([]cmdsFiles.File, []string, error) {
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
	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
		}
	}

	arg, err := openPath(file, path)
	if err != nil {
		return nil, nil, err
	}

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

301 302
func appendStdinAsFile(args []cmdsFiles.File, stdin *os.File) ([]cmdsFiles.File, *os.File) {
	arg := &cmdsFiles.ReaderFile{"", stdin}
303 304 305
	return append(args, arg), nil
}

306 307
// recursively get file or directory contents as a cmdsFiles.File
func openPath(file *os.File, path string) (cmdsFiles.File, error) {
308 309 310 311 312 313 314
	stat, err := file.Stat()
	if err != nil {
		return nil, err
	}

	// for non-directories, return a ReaderFile
	if !stat.IsDir() {
315
		return &cmdsFiles.ReaderFile{path, file}, nil
316 317 318 319 320 321 322 323
	}

	// for directories, recursively iterate though children then return as a SliceFile
	contents, err := file.Readdir(0)
	if err != nil {
		return nil, err
	}

324 325
	// make sure contents are sorted so -- repeatably -- we get the same inputs.
	sort.Sort(sortFIByName(contents))
326

327
	files := make([]cmdsFiles.File, 0, len(contents))
328
	for _, child := range contents {
329
		childPath := fp.Join(path, child.Name())
330 331 332 333 334
		childFile, err := os.Open(childPath)
		if err != nil {
			return nil, err
		}

335
		f, err := openPath(childFile, childPath)
336 337 338 339 340 341 342
		if err != nil {
			return nil, err
		}

		files = append(files, f)
	}

343
	return &cmdsFiles.SliceFile{path, files}, nil
344
}
345 346 347 348 349 350 351 352 353 354 355 356 357

// 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
}
358 359 360 361 362 363

type sortFIByName []os.FileInfo

func (es sortFIByName) Len() int           { return len(es) }
func (es sortFIByName) Swap(i, j int)      { es[i], es[j] = es[j], es[i] }
func (es sortFIByName) Less(i, j int) bool { return es[i].Name() < es[j].Name() }