1package config 2 3import ( 4 "errors" 5 "strconv" 6 "strings" 7) 8 9// parseKVSlice parses a configuration string in the form 10// 11// key=val;key=val,key=val;key=val 12// 13// into a list of string maps. maps are separated by comma and key/value 14// pairs within a map are separated by semicolons. The first key/value 15// pair of a map can omit the key and its value will be stored under the 16// empty key. This allows support of legacy configuration formats which 17// are 18// 19// val;opt1=val1;opt2=val2;... 20func parseKVSlice(in string) ([]map[string]string, error) { 21 var keyOrFirstVal string 22 maps := []map[string]string{} 23 m := map[string]string{} 24 25 newMap := func() { 26 if len(m) > 0 { 27 maps = append(maps, m) 28 m = map[string]string{} 29 } 30 } 31 32 v := "" 33 s := []rune(in) 34 state := stateFirstKey 35 for { 36 if len(s) == 0 { 37 break 38 } 39 typ, val, n := lex(s) 40 s = s[n:] 41 // fmt.Println("parse:", "typ:", typ, "val:", val, "v:", v, "state:", string(state), "s:", string(s)) 42 switch state { 43 case stateFirstKey: 44 switch typ { 45 case itemText: 46 keyOrFirstVal = strings.TrimSpace(val) 47 state = stateAfterFirstKey 48 case itemComma, itemSemicolon: 49 continue 50 default: 51 return nil, errors.New(val) 52 } 53 54 // the first value is allowed to omit the key 55 // a=b;c=d and b;c=d are valid 56 case stateAfterFirstKey: 57 switch typ { 58 case itemEqual: 59 state = stateVal 60 case itemComma: 61 if keyOrFirstVal != "" { 62 m[""] = keyOrFirstVal 63 } 64 newMap() 65 state = stateFirstKey 66 case itemSemicolon: 67 if keyOrFirstVal != "" { 68 m[""] = keyOrFirstVal 69 } 70 state = stateKey 71 default: 72 return nil, errors.New(val) 73 } 74 75 case stateKey: 76 switch typ { 77 case itemText: 78 keyOrFirstVal = strings.TrimSpace(val) 79 state = stateEqual 80 case itemComma, itemSemicolon: 81 continue 82 default: 83 return nil, errors.New(val) 84 } 85 86 case stateEqual: 87 switch typ { 88 case itemEqual: 89 state = stateVal 90 default: 91 return nil, errors.New(val) 92 } 93 94 case stateVal: 95 switch typ { 96 case itemText, itemEqual: 97 v += val 98 case itemComma: 99 m[keyOrFirstVal] = v 100 v = "" 101 newMap() 102 state = stateFirstKey 103 case itemSemicolon: 104 m[keyOrFirstVal] = v 105 v = "" 106 state = stateKey 107 default: 108 return nil, errors.New(val) 109 } 110 } 111 } 112 switch state { 113 case stateVal: 114 m[keyOrFirstVal] = v 115 case stateAfterFirstKey: 116 if keyOrFirstVal != "" { 117 m[""] = keyOrFirstVal 118 } 119 } 120 if len(m) > 0 { 121 maps = append(maps, m) 122 } 123 if len(maps) == 0 { 124 return nil, nil 125 } 126 return maps, nil 127} 128 129type itemType string 130 131const ( 132 itemText itemType = "TEXT" 133 itemEqual = "EQUAL" 134 itemSemicolon = "SEMICOLON" 135 itemComma = "COMMA" 136 itemError = "ERROR" 137) 138 139func (t itemType) String() string { 140 return string(t) 141} 142 143type state string 144 145const ( 146 147 // lexer states 148 stateStart state = "start" 149 stateText = "text" 150 stateQText = "qtext" 151 stateQTextEnd = "qtextend" 152 stateQTextEsc = "qtextesc" 153 154 // parser states 155 stateFirstKey = "first-key" 156 stateKey = "key" 157 stateEqual = "equal" 158 stateVal = "val" 159 stateAfterFirstKey = "equal-comma-semicolon" 160) 161 162func lex(s []rune) (itemType, string, int) { 163 isComma := func(r rune) bool { return r == ',' } 164 isSemicolon := func(r rune) bool { return r == ';' } 165 isEqual := func(r rune) bool { return r == '=' } 166 isEscape := func(r rune) bool { return r == '\\' } 167 isQuote := func(r rune) bool { return r == '"' || r == '\'' } 168 169 var quote rune 170 state := stateStart 171 for i, r := range s { 172 // fmt.Println("lex:", "i:", i, "r:", string(r), "state:", string(state)) 173 switch state { 174 case stateStart: 175 switch { 176 case isComma(r): 177 return itemComma, string(r), 1 178 case isSemicolon(r): 179 return itemSemicolon, string(r), 1 180 case isEqual(r): 181 return itemEqual, string(r), 1 182 case isQuote(r): 183 quote = r 184 state = stateQText 185 default: 186 state = stateText 187 } 188 case stateText: 189 switch { 190 case isComma(r) || isSemicolon(r) || isEqual(r): 191 return itemText, string(s[:i]), i 192 default: 193 // state = stateText 194 } 195 case stateQText: 196 switch { 197 case r == quote: 198 state = stateQTextEnd 199 case isEscape(r): 200 state = stateQTextEsc 201 default: 202 // state = stateQText 203 } 204 205 case stateQTextEsc: 206 state = stateQText 207 208 case stateQTextEnd: 209 v, err := strconv.Unquote(string(s[:i])) 210 if err != nil { 211 return itemError, "invalid escape sequence", i 212 } 213 return itemText, v, i 214 } 215 } 216 217 // fmt.Println("lex:", "state:", string(state)) 218 switch state { 219 case stateQText: 220 return itemError, "unbalanced quotes", len(s) 221 case stateQTextEsc: 222 return itemError, "unterminated escape sequence", len(s) 223 case stateQTextEnd: 224 v, err := strconv.Unquote(string(s)) 225 if err != nil { 226 return itemError, "invalid escape sequence", len(s) 227 } 228 return itemText, v, len(s) 229 default: 230 return itemText, string(s), len(s) 231 } 232} 233