parse.go 8.03 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
	recursive, _, err := req.Option(cmds.RecShort).Bool()
	if err != nil {
		return nil, nil, nil, u.ErrCast()
	}
	stringArgs, fileArgs, err := parseArgs(stringVals, stdin, cmd.Arguments, recursive)
54 55 56
	if err != nil {
		return nil, cmd, path, err
	}
57 58 59 60
	req.SetArguments(stringArgs)

	file := &cmds.SliceFile{"", fileArgs}
	req.SetFiles(file)
61 62 63

	err = cmd.CheckArguments(req)
	if err != nil {
64
		return req, cmd, path, err
65 66
	}

67
	return req, cmd, path, nil
68 69
}

70 71
// 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
72
func parsePath(input []string, root *cmds.Command) ([]string, []string, *cmds.Command) {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
73 74
	cmd := root
	i := 0
75

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
76 77 78 79
	for _, blob := range input {
		if strings.HasPrefix(blob, "-") {
			break
		}
80

81 82
		sub := cmd.Subcommand(blob)
		if sub == nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
83 84
			break
		}
85
		cmd = sub
86

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
87 88
		i++
	}
89

90
	return input[:i], input[i:], cmd
91 92
}

93
// parseOptions parses the raw string values of the given options
94
// returns the parsed options as strings, along with the CLI args
95
func parseOptions(input []string) (map[string]interface{}, []string, error) {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
96
	opts := make(map[string]interface{})
97
	args := []string{}
98

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

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
102 103 104
		if strings.HasPrefix(blob, "-") {
			name := blob[1:]
			value := ""
105

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
106 107 108 109
			// support single and double dash
			if strings.HasPrefix(name, "-") {
				name = name[1:]
			}
110

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
111 112 113 114 115
			if strings.Contains(name, "=") {
				split := strings.SplitN(name, "=", 2)
				name = split[0]
				value = split[1]
			}
116

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

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
121
			opts[name] = value
122

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
123 124 125 126
		} else {
			args = append(args, blob)
		}
	}
127

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
128
	return opts, args, nil
129
}
130

131
func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursive bool) ([]string, []cmds.File, error) {
132 133
	// check if stdin is coming from terminal or is being piped in
	if stdin != nil {
134
		if term, err := isTerminal(stdin); err != nil {
135
			return nil, nil, err
136 137
		} else if term {
			stdin = nil // set to nil so we ignore it
138 139 140
		}
	}

141
	// count required argument definitions
142
	numRequired := 0
143
	for _, argDef := range argDefs {
144
		if argDef.Required {
145
			numRequired++
146
		}
147
	}
148

149 150
	// count number of values provided by user
	numInputs := len(inputs)
151
	if stdin != nil {
152
		numInputs += 1
153 154
	}

155 156 157 158 159 160 161
	// 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)
	}

162
	stringArgs := make([]string, 0, numInputs)
163
	fileArgs := make([]cmds.File, 0, numInputs)
164 165

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

169 170
		// skip optional argument definitions if there aren't sufficient remaining inputs
		if numInputs-i <= numRequired && !argDef.Required {
171
			continue
172
		} else if argDef.Required {
173
			numRequired--
174
		}
175

176
		var err error
177 178 179
		if argDef.Type == cmds.ArgString {
			if stdin == nil {
				// add string values
180
				stringArgs, inputs = appendString(stringArgs, inputs)
181

182
			} else if argDef.SupportsStdin {
183
				// if we have a stdin, read it in and use the data as a string value
184
				stringArgs, stdin, err = appendStdinAsString(stringArgs, stdin)
185
				if err != nil {
186
					return nil, nil, err
187 188
				}
			}
189 190 191 192

		} else if argDef.Type == cmds.ArgFile {
			if stdin == nil {
				// treat stringArg values as file paths
193
				fileArgs, inputs, err = appendFile(fileArgs, inputs, argDef, recursive)
194 195 196 197
				if err != nil {
					return nil, nil, err
				}

198
			} else if argDef.SupportsStdin {
199
				// if we have a stdin, create a file from it
200
				fileArgs, stdin = appendStdinAsFile(fileArgs, stdin)
201 202
			}
		}
203 204

		argDefIndex++
205 206
	}

207
	// check to make sure we didn't miss any required arguments
208 209 210 211 212
	if len(argDefs) > argDefIndex {
		for _, argDef := range argDefs[argDefIndex:] {
			if argDef.Required {
				return nil, nil, fmt.Errorf("Argument '%s' is required", argDef.Name)
			}
213 214 215
		}
	}

216
	return stringArgs, fileArgs, nil
217
}
218

219 220 221 222 223 224 225 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
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
}

287
// recursively get file or directory contents as a cmds.File
288
func openPath(file *os.File, path string) (cmds.File, error) {
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
	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 {
308
		childPath := fp.Join(path, child.Name())
309 310 311 312 313
		childFile, err := os.Open(childPath)
		if err != nil {
			return nil, err
		}

314
		f, err := openPath(childFile, childPath)
315 316 317 318 319 320 321 322 323
		if err != nil {
			return nil, err
		}

		files = append(files, f)
	}

	return &cmds.SliceFile{path, files}, nil
}
324 325 326 327 328 329 330 331 332 333 334 335 336

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