-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathline_parser.go
137 lines (128 loc) · 3.63 KB
/
line_parser.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
// Copyright 2019 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
package datadriven
import (
"fmt"
"strings"
"unicode/utf8"
)
// ParseLine parses a datadriven directive line and returns the parsed command
// and CmdArgs.
//
// An input directive line is a command optionally followed by a list of
// arguments. Arguments may or may not have values and are specified with one of
// the forms:
// - <argname> # No values.
// - <argname>=<value> # Single value.
// - <argname>=(<value1>, <value2>, ...) # Multiple values.
//
// Note that in the last case, we allow the values to contain parens; the
// parsing will take nesting into account. For example:
// cmd exprs=(a + (b + c), d + f)
// is valid and produces the expected values for the argument.
//
func ParseLine(line string) (cmd string, cmdArgs []CmdArg, err error) {
line = strings.TrimSpace(line)
if line == "" {
return "", nil, nil
}
origLine := line
defer func() {
if r := recover(); r != nil {
if r == (parseError{}) {
column := len(origLine) - len(line) + 1
cmd = ""
cmdArgs = nil
err = fmt.Errorf("cannot parse directive at column %d: %s", column, origLine)
// Note: to debug an unexpected parsing error, this is a good place to
// add a debug.PrintStack().
} else {
panic(r)
}
}
}()
// until removes the prefix up to one of the given characters from line and
// returns the prefix.
until := func(chars string) string {
idx := strings.IndexAny(line, chars)
if idx == -1 {
idx = len(line)
}
res := line[:idx]
line = line[idx:]
return res
}
cmd = until(" ")
if cmd == "" {
panic(parseError{})
}
line = strings.TrimSpace(line)
for line != "" {
var arg CmdArg
arg.Key = until(" =")
if arg.Key == "" {
panic(parseError{})
}
if line != "" && line[0] == '=' {
// Skip the '='.
line = line[1:]
if line == "" || line[0] == ' ' {
// Empty value.
arg.Vals = []string{""}
} else if line[0] != '(' {
// Single value.
val := until(" ")
arg.Vals = []string{val}
} else {
// Skip the '('.
pos := 1
nestLevel := 1
lastValStart := pos
// Run through the characters for the values, being mindful of nested
// parens. When we find a top-level comma, we "cut" a value and append
// it to the array.
for nestLevel > 0 {
if pos == len(line) {
// The string ended before we found the final ')'.
panic(parseError{})
}
r, runeSize := utf8.DecodeRuneInString(line[pos:])
pos += runeSize
switch r {
case ',':
if nestLevel == 1 {
// Found a top-level comma.
arg.Vals = append(arg.Vals, line[lastValStart:pos-1])
// Skip any spaces after the comma.
for pos < len(line) && line[pos] == ' ' {
pos++
}
lastValStart = pos
}
case '(':
nestLevel++
case ')':
nestLevel--
}
}
arg.Vals = append(arg.Vals, line[lastValStart:pos-1])
line = strings.TrimSpace(line[pos:])
}
}
cmdArgs = append(cmdArgs, arg)
line = strings.TrimSpace(line)
}
return cmd, cmdArgs, nil
}
type parseError struct{}