parse.go 8.19 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
	cmd := root
74 75
	path := make([]string, 0, len(input))
	input2 := make([]string, 0, len(input))
76

77
	for i, blob := range input {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
78
		if strings.HasPrefix(blob, "-") {
79 80
			input2 = append(input2, blob)
			continue
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
81
		}
82

83 84
		sub := cmd.Subcommand(blob)
		if sub == nil {
85
			input2 = append(input2, input[i:]...)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
86 87
			break
		}
88
		cmd = sub
89
		path = append(path, blob)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
90
	}
91

92
	return path, input2, cmd
93 94
}

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

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

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

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

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

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

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
123
			opts[name] = value
124

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

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
130
	return opts, args, nil
131
}
132

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

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

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

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

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

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

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

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

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

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

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

		argDefIndex++
207 208
	}

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

218
	return stringArgs, fileArgs, nil
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 287 288
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
}

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

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

		files = append(files, f)
	}

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

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