parse.go 8.93 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
	u "github.com/jbenet/go-ipfs/util"
15 16
)

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

28
	opts, stringVals, err := parseOptions(input)
29
	if err != nil {
30
		return nil, cmd, 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 42 43 44 45
	// 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
		}
	}

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

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

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

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

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

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

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

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

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

101
	return path, input2, cmd
102 103
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		argDefIndex++
223 224
	}

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

234
	return stringArgs, fileArgs, nil
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 262 263 264 265 266 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 292 293 294 295 296 297 298 299 300 301 302 303 304
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
}

func appendFile(args []cmds.File, inputs []string, argDef *cmds.Argument, recursive bool) ([]cmds.File, []string, error) {
	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
}

func appendStdinAsFile(args []cmds.File, stdin *os.File) ([]cmds.File, *os.File) {
	arg := &cmds.ReaderFile{"", stdin}
	return append(args, arg), nil
}

305
// recursively get file or directory contents as a cmds.File
306
func openPath(file *os.File, path string) (cmds.File, error) {
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
	stat, err := file.Stat()
	if err != nil {
		return nil, err
	}

	// for non-directories, return a ReaderFile
	if !stat.IsDir() {
		return &cmds.ReaderFile{path, file}, nil
	}

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

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

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

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

		files = append(files, f)
	}

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

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

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() }