forked from peak/s5cmd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
parse.go
381 lines (333 loc) · 10.4 KB
/
parse.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
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
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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
// Package core is the core package for s5cmd.
package core
import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/peak/s5cmd/opt"
"github.com/peak/s5cmd/url"
)
const (
// GlobCharacters is valid glob characters for local files
GlobCharacters string = "?*["
)
var (
// cmd && success-cmd || fail-cmd
regexCmdAndOr = regexp.MustCompile(`^\s*(.+?)\s*&&\s*(.+?)\s*\|\|\s*(.+?)\s*$`)
// cmd && success-cmd
regexCmdAnd = regexp.MustCompile(`^\s*(.+?)\s*&&\s*(.+?)\s*$`)
// cmd || fail-cmd
regexCmdOr = regexp.MustCompile(`^\s*(.+?)\s*\|\|\s*(.+?)\s*$`)
)
func isGlob(s string) bool {
return strings.ContainsAny(s, GlobCharacters)
}
// parseArgumentByType attempts to parse an input string according to the given opt.ParamType and returns a JobArgument (or error)
// fnObj is the last/previous successfully parsed argument, used mainly to append the basenames of the source files to destination directories
func parseArgumentByType(s string, t opt.ParamType, fnObj *JobArgument) (*JobArgument, error) {
fnBase := ""
if (t == opt.S3ObjOrDir || t == opt.FileOrDir || t == opt.OptionalFileOrDir) && fnObj != nil {
fnBase = filepath.Base(fnObj.arg)
}
switch t {
case opt.Unchecked, opt.UncheckedOneOrMore:
return NewJobArgument(s, nil), nil
case opt.S3Obj, opt.S3ObjOrDir, opt.S3WildObj, opt.S3Dir, opt.S3SimpleObj:
uri, err := url.ParseS3Url(s)
if err != nil {
return nil, err
}
s = "s3://" + uri.Format() // rebuild s with formatted url
if (t == opt.S3Obj || t == opt.S3ObjOrDir || t == opt.S3SimpleObj) && url.HasWild(uri.Key) {
return nil, errors.New("S3 key cannot contain wildcards")
}
if t == opt.S3WildObj {
if !url.HasWild(uri.Key) {
return nil, errors.New("S3 key should contain wildcards")
}
if uri.Key == "" {
return nil, errors.New("S3 key should not be empty")
}
}
if t == opt.S3SimpleObj && uri.Key == "" {
return nil, errors.New("S3 key should not be empty")
}
endsInSlash := strings.HasSuffix(uri.Key, "/")
if endsInSlash {
if t == opt.S3Obj || t == opt.S3SimpleObj {
return nil, errors.New("S3 key should not end with /")
}
} else {
if t == opt.S3Dir && uri.Key != "" {
return nil, errors.New("S3 dir should end with /")
}
}
if t == opt.S3ObjOrDir && endsInSlash && fnBase != "" {
uri.Key += fnBase
s += fnBase
}
if t == opt.S3ObjOrDir && uri.Key == "" && fnBase != "" {
uri.Key += fnBase
s += "/" + fnBase
}
return NewJobArgument(s, uri), nil
case opt.OptionalFileOrDir, opt.OptionalDir:
if s == "" {
s = "."
}
fallthrough
case opt.FileObj, opt.FileOrDir, opt.Dir:
// check if we have s3 object
_, err := url.ParseS3Url(s)
if err == nil {
return nil, errors.New("File param resembles s3 object")
}
if s == "." {
s = "." + string(filepath.Separator)
}
endsInSlash := len(s) > 0 && s[len(s)-1] == filepath.Separator
if isGlob(s) {
return nil, errors.New("Param should not contain glob characters")
}
if t == opt.FileObj {
if endsInSlash {
return nil, errors.New("File param should not end with /")
}
st, err := os.Stat(s)
if err == nil && st.IsDir() {
return nil, errors.New("File param should not be a directory")
}
}
if (t == opt.FileOrDir || t == opt.OptionalFileOrDir) && endsInSlash && fnBase != "" {
s += fnBase
}
if (t == opt.FileOrDir || t == opt.OptionalFileOrDir) && !endsInSlash {
st, err := os.Stat(s)
if err != nil {
if !os.IsNotExist(err) {
return nil, errors.New("Could not stat")
}
} else {
if st.IsDir() {
s += string(filepath.Separator)
}
}
}
if (t == opt.Dir || t == opt.OptionalDir) && !endsInSlash {
st, err := os.Stat(s)
if err != nil {
if !os.IsNotExist(err) {
return nil, errors.New("Could not stat")
}
} else {
if !st.IsDir() {
return nil, errors.New("Dir param can not be file")
}
}
s += string(filepath.Separator)
}
return NewJobArgument(s, nil), nil
case opt.Glob:
_, err := url.ParseS3Url(s)
if err == nil {
return nil, errors.New("Glob param resembles s3 object")
}
if !isGlob(s) {
return nil, errors.New("Param does not look like a glob")
}
_, err = filepath.Match(s, "")
if err != nil {
return nil, err
}
return NewJobArgument(s, nil), nil
}
return nil, errors.New("Unhandled parseArgumentByType")
}
// ParseJob parses a job description and returns a *Job type, possibly with other *Job types in successCommand/failCommand
func ParseJob(jobdesc string) (*Job, error) {
jobdesc = strings.Split(jobdesc, " #")[0] // Get rid of comments
jobdesc = strings.TrimSpace(jobdesc)
// Get rid of double or more spaces
jobdesc = strings.Replace(jobdesc, " ", " ", -1)
jobdesc = strings.Replace(jobdesc, " ", " ", -1)
jobdesc = strings.Replace(jobdesc, " ", " ", -1)
var (
j, s, f *Job
err error
)
res := regexCmdAndOr.FindStringSubmatch(jobdesc)
if res != nil {
j, err = parseSingleJob(res[1])
if err != nil {
return nil, err
}
s, err = parseSingleJob(res[2])
if err != nil {
return nil, err
}
f, err = parseSingleJob(res[3])
if err != nil {
return nil, err
}
goto found
}
res = regexCmdAnd.FindStringSubmatch(jobdesc)
if res != nil {
j, err = parseSingleJob(res[1])
if err != nil {
return nil, err
}
s, err = parseSingleJob(res[2])
if err != nil {
return nil, err
}
goto found
}
res = regexCmdOr.FindStringSubmatch(jobdesc)
if res != nil {
j, err = parseSingleJob(res[1])
if err != nil {
return nil, err
}
f, err = parseSingleJob(res[2])
if err != nil {
return nil, err
}
goto found
}
j, err = parseSingleJob(jobdesc)
s = nil
f = nil
if err != nil {
return nil, err
}
found:
if j != nil {
j.successCommand = s
j.failCommand = f
}
return j, nil
}
// parseSingleJob attempts to parse a single job description to a standalone Job struct.
// It will loop through each accepted command-signature, trying to find the first one that fits.
func parseSingleJob(jobdesc string) (*Job, error) {
if jobdesc == "" || jobdesc[0] == '#' {
return nil, nil
}
if strings.Contains(jobdesc, "&&") {
return nil, errors.New("Nested commands are not supported")
}
if strings.Contains(jobdesc, "||") {
return nil, errors.New("Nested commands are not supported")
}
// Tokenize arguments
parts := strings.Split(jobdesc, " ")
var numSuccess, numFails, numAcceptableFails uint32
// Create a skeleton Job
ourJob := &Job{sourceDesc: jobdesc, numSuccess: &numSuccess, numFails: &numFails, numAcceptableFails: &numAcceptableFails}
found := -1
var parseArgErr error
for i, c := range Commands {
if parts[0] == c.Keyword { // The first token is the name of our command, "cp", "mv" etc.
found = i // Save the id of the last matching command, we will use this in our error message if needed
// Enrich our skeleton Job with default values for this specific command
ourJob.command = c.Keyword
ourJob.operation = c.Operation
ourJob.args = []*JobArgument{}
ourJob.opts = c.Opts
// Parse options below, until endOptParse
fileArgsStartPosition := 1 // Position where the real file/s3 arguments start. Before this comes the options/flags.
acceptedOpts := c.Operation.GetAcceptedOpts()
for k := 1; k < len(parts); k++ {
if parts[k][0] != '-' { // If it doesn't look like an option, end option parsing
fileArgsStartPosition = k
goto endOptParse
}
foundOpt := false
for _, p := range *acceptedOpts {
s := p.GetParam()
if parts[k] == s {
ourJob.opts = append(ourJob.opts, p)
foundOpt = true
}
}
if !foundOpt { // End option parsing if it looks like an option but isn't/doesn't match the list
fileArgsStartPosition = k
goto endOptParse
}
}
endOptParse:
// Don't parse args if we have the help option
if ourJob.opts.Has(opt.Help) {
return ourJob, nil
}
// Check number of arguments
suppliedParamCount := len(parts) - fileArgsStartPosition // Number of arguments/params (sans options and the command name itself)
minCount := len(c.Params) // Minimum number of parameters needed
maxCount := minCount // Maximum
if minCount > 0 && c.Params[minCount-1] == opt.UncheckedOneOrMore {
maxCount = -1 // Accept unlimited parameters if the last param is opt.UncheckedOneOrMore
}
if minCount > 0 && (c.Params[minCount-1] == opt.OptionalDir || c.Params[minCount-1] == opt.OptionalFileOrDir) {
minCount-- // Optional params are optional
}
if suppliedParamCount < minCount || (maxCount > -1 && suppliedParamCount > maxCount) { // Check if param counts are acceptable
// If the number of parameters does not match, try another command
continue
}
// Parse arguments into JobArguments
var a, fnObj *JobArgument
parseArgErr = nil
lastType := opt.UncheckedOneOrMore
maxI := fileArgsStartPosition
for i, t := range c.Params { // check if param types match
partVal := ""
if fileArgsStartPosition+i < len(parts) {
partVal = parts[fileArgsStartPosition+i]
}
a, parseArgErr = parseArgumentByType(partVal, t, fnObj)
if parseArgErr != nil {
verboseLog("Error parsing %s as %s: %s", partVal, t.String(), parseArgErr.Error())
break
}
verboseLog("Parsed %s as %s", partVal, t.String())
ourJob.args = append(ourJob.args, a)
if (t == opt.S3Obj || t == opt.S3SimpleObj || t == opt.FileObj) && fnObj == nil {
fnObj = a
}
maxI = i
lastType = t
}
if parseArgErr == nil && minCount != maxCount && maxCount == -1 { // If no error yet, and we have unlimited/repeating parameters...
for i, p := range parts {
if i <= maxI+1 {
continue
}
a, parseArgErr = parseArgumentByType(p, lastType, fnObj)
if parseArgErr != nil {
verboseLog("Error parsing %s as %s: %s", p, lastType.String(), parseArgErr.Error())
break
}
verboseLog("Parsed %s as %s", p, lastType.String())
ourJob.args = append(ourJob.args, a)
}
}
if parseArgErr != nil {
verboseLog("Our command doesn't look to be a %s", c.String())
continue // Not our command, try another
}
verboseLog("Our command looks to be a %s", c.String(ourJob.opts...))
return ourJob, nil
}
}
if found >= 0 {
if parseArgErr != nil {
return nil, fmt.Errorf(`Invalid parameters to "%s": %s`, Commands[found].Keyword, parseArgErr.Error())
}
return nil, fmt.Errorf(`Invalid parameters to "%s"`, parts[0])
}
return nil, fmt.Errorf(`Unknown command "%s"`, parts[0])
}