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