parse.go 8.44 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/filepath"
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
9
	"strings"
10

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

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

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

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

36 37 38 39 40 41 42 43
	// 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
		}
	}

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

49 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 {
			return nil, nil, nil, u.ErrCast()
		}
58
	}
59

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

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

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

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

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

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

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

99
	return path, input2, cmd
100 101
}

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

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

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

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

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

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

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

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

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

140
func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursive bool) ([]string, []cmds.File, error) {
141 142
	// check if stdin is coming from terminal or is being piped in
	if stdin != nil {
143
		if term, err := isTerminal(stdin); err != nil {
144
			return nil, nil, err
145 146
		} else if term {
			stdin = nil // set to nil so we ignore it
147 148 149
		}
	}

150
	// count required argument definitions
151
	numRequired := 0
152
	for _, argDef := range argDefs {
153
		if argDef.Required {
154
			numRequired++
155
		}
156
	}
157

158 159
	// count number of values provided by user
	numInputs := len(inputs)
160
	if stdin != nil {
161
		numInputs += 1
162 163
	}

164 165 166 167 168 169 170
	// 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)
	}

171
	stringArgs := make([]string, 0, numInputs)
172
	fileArgs := make([]cmds.File, 0, numInputs)
173 174

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

178 179
		// skip optional argument definitions if there aren't sufficient remaining inputs
		if numInputs-i <= numRequired && !argDef.Required {
180
			continue
181
		} else if argDef.Required {
182
			numRequired--
183
		}
184

185
		var err error
186 187 188
		if argDef.Type == cmds.ArgString {
			if stdin == nil {
				// add string values
189
				stringArgs, inputs = appendString(stringArgs, inputs)
190

191
			} else if argDef.SupportsStdin {
192
				// if we have a stdin, read it in and use the data as a string value
193
				stringArgs, stdin, err = appendStdinAsString(stringArgs, stdin)
194
				if err != nil {
195
					return nil, nil, err
196 197
				}
			}
198 199 200 201

		} else if argDef.Type == cmds.ArgFile {
			if stdin == nil {
				// treat stringArg values as file paths
202
				fileArgs, inputs, err = appendFile(fileArgs, inputs, argDef, recursive)
203 204 205 206
				if err != nil {
					return nil, nil, err
				}

207
			} else if argDef.SupportsStdin {
208
				// if we have a stdin, create a file from it
209
				fileArgs, stdin = appendStdinAsFile(fileArgs, stdin)
210 211
			}
		}
212 213

		argDefIndex++
214 215
	}

216
	// check to make sure we didn't miss any required arguments
217 218 219 220 221
	if len(argDefs) > argDefIndex {
		for _, argDef := range argDefs[argDefIndex:] {
			if argDef.Required {
				return nil, nil, fmt.Errorf("Argument '%s' is required", argDef.Name)
			}
222 223 224
		}
	}

225
	return stringArgs, fileArgs, nil
226
}
227

228 229 230 231 232 233 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 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
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
}

296
// recursively get file or directory contents as a cmds.File
297
func openPath(file *os.File, path string) (cmds.File, error) {
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
	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
	}

	files := make([]cmds.File, 0, len(contents))

	for _, child := range contents {
317
		childPath := fp.Join(path, child.Name())
318 319 320 321 322
		childFile, err := os.Open(childPath)
		if err != nil {
			return nil, err
		}

323
		f, err := openPath(childFile, childPath)
324 325 326 327 328 329 330 331 332
		if err != nil {
			return nil, err
		}

		files = append(files, f)
	}

	return &cmds.SliceFile{path, files}, nil
}
333 334 335 336 337 338 339 340 341 342 343 344 345

// 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
}