parse.go 7.11 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"
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
8
	"strings"
9

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

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

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

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

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

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

48 49 50 51 52
	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)
53 54 55
	if err != nil {
		return nil, cmd, path, err
	}
56 57 58 59
	req.SetArguments(stringArgs)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

130
func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursive bool) ([]string, []cmds.File, error) {
131 132 133 134
	// check if stdin is coming from terminal or is being piped in
	if stdin != nil {
		stat, err := stdin.Stat()
		if err != nil {
135
			return nil, nil, err
136 137 138 139 140 141 142 143 144
		}

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

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

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

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

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

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

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

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

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

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

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

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

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

238
				fileArgs = append(fileArgs, fileArg)
239

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

		argDefIndex++
249 250
	}

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

260
	return stringArgs, fileArgs, nil
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

// 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 {
		childPath := fmt.Sprintf("%s/%s", path, child.Name())
		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
}