parse.go 6.52 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, arguments []cmds.Argument, recursive bool) ([]interface{}, []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 arguments {
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
	stringArgs := make([]interface{}, 0, numInputs)
	fileArgs := make([]cmds.File, 0, numInputs)
161 162

	argDefIndex := 0 // the index of the current argument definition
163
	for i, input := range inputs {
164 165 166 167 168
		// get the argument definiton (should be arguments[argDefIndex],
		// but if argDefIndex > len(arguments) we use the last argument definition)
		var argDef cmds.Argument
		if argDefIndex < len(arguments) {
			argDef = arguments[argDefIndex]
169
		} else if len(arguments) > 0 {
170 171
			argDef = arguments[len(arguments)-1]
		}
172

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

180 181 182
		if argDef.Type == cmds.ArgString {
			if stdin == nil {
				// add string values
183 184
				stringArgs = append(stringArgs, input)
				inputs = inputs[1:]
185

186
			} else if argDef.SupportsStdin {
187 188 189
				// if we have a stdin, read it in and use the data as a string value
				var buf bytes.Buffer
				_, err := buf.ReadFrom(stdin)
190
				if err != nil {
191
					return nil, nil, err
192
				}
193
				stringArgs = append(stringArgs, buf.String())
194
				stdin = nil
195
			}
196 197 198 199

		} else if argDef.Type == cmds.ArgFile {
			if stdin == nil {
				// treat stringArg values as file paths
200 201 202 203
				path := input
				inputs = inputs[1:]

				file, err := os.Open(path)
204
				if err != nil {
205
					return nil, nil, err
206
				}
207

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

				fileArg, err := getFile(file, input)
				if err != nil {
					return nil, nil, err
				}

231
				fileArgs = append(fileArgs, fileArg)
232

233
			} else if argDef.SupportsStdin {
234 235 236
				// if we have a stdin, create a file from it
				fileArg := &cmds.ReaderFile{"", stdin}
				fileArgs = append(fileArgs, fileArg)
237
				stdin = nil
238 239
			}
		}
240 241

		argDefIndex++
242 243
	}

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

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