parse.go 7.12 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 134 135
	// check if stdin is coming from terminal or is being piped in
	if stdin != nil {
		stat, err := stdin.Stat()
		if err != nil {
136
			return nil, nil, err
137 138 139 140 141 142 143 144 145
		}

		// if stdin isn't a CharDevice, set it to nil
		// (this means it is coming from terminal and we want to ignore it)
		if (stat.Mode() & os.ModeCharDevice) != 0 {
			stdin = nil
		}
	}

146
	// count required argument definitions
147
	numRequired := 0
148
	for _, argDef := range argDefs {
149
		if argDef.Required {
150
			numRequired++
151
		}
152
	}
153

154 155
	// count number of values provided by user
	numInputs := len(inputs)
156
	if stdin != nil {
157
		numInputs += 1
158 159
	}

160 161 162 163 164 165 166
	// 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)
	}

167
	stringArgs := make([]string, 0, numInputs)
168
	fileArgs := make([]cmds.File, 0, numInputs)
169 170

	argDefIndex := 0 // the index of the current argument definition
171
	for i := 0; i < numInputs; i++ {
172 173
		// get the argument definiton (should be argDefs[argDefIndex],
		// but if argDefIndex > len(argDefs) we use the last argument definition)
174
		var argDef cmds.Argument
175 176 177 178
		if argDefIndex < len(argDefs) {
			argDef = argDefs[argDefIndex]
		} else if len(argDefs) > 0 {
			argDef = argDefs[len(argDefs)-1]
179
		}
180

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

188 189 190
		if argDef.Type == cmds.ArgString {
			if stdin == nil {
				// add string values
191
				stringArgs = append(stringArgs, inputs[0])
192
				inputs = inputs[1:]
193

194
			} else if argDef.SupportsStdin {
195 196 197
				// if we have a stdin, read it in and use the data as a string value
				var buf bytes.Buffer
				_, err := buf.ReadFrom(stdin)
198
				if err != nil {
199
					return nil, nil, err
200
				}
201
				stringArgs = append(stringArgs, buf.String())
202
				stdin = nil
203
			}
204 205 206 207

		} else if argDef.Type == cmds.ArgFile {
			if stdin == nil {
				// treat stringArg values as file paths
208
				path := inputs[0]
209 210 211
				inputs = inputs[1:]

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

216 217 218 219 220 221 222 223
				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",
224
							path, argDef.Name)
225 226 227 228
						return nil, nil, err
					}
					if !recursive {
						err = fmt.Errorf("'%s' is a directory, use the '-%s' flag to specify directories",
229
							path, cmds.RecShort)
230 231 232 233
						return nil, nil, err
					}
				}

234
				fileArg, err := getFile(file, path)
235 236 237 238
				if err != nil {
					return nil, nil, err
				}

239
				fileArgs = append(fileArgs, fileArg)
240

241
			} else if argDef.SupportsStdin {
242 243 244
				// if we have a stdin, create a file from it
				fileArg := &cmds.ReaderFile{"", stdin}
				fileArgs = append(fileArgs, fileArg)
245
				stdin = nil
246 247
			}
		}
248 249

		argDefIndex++
250 251
	}

252
	// check to make sure we didn't miss any required arguments
253 254 255 256 257
	if len(argDefs) > argDefIndex {
		for _, argDef := range argDefs[argDefIndex:] {
			if argDef.Required {
				return nil, nil, fmt.Errorf("Argument '%s' is required", argDef.Name)
			}
258 259 260
		}
	}

261
	return stringArgs, fileArgs, nil
262
}
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284

// recursively get file or directory contents as a cmds.File
func getFile(file *os.File, path string) (cmds.File, error) {
	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 {
285
		childPath := fp.Join(path, child.Name())
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
		childFile, err := os.Open(childPath)
		if err != nil {
			return nil, err
		}

		f, err := getFile(childFile, childPath)
		if err != nil {
			return nil, err
		}

		files = append(files, f)
	}

	return &cmds.SliceFile{path, files}, nil
}