]> Cypherpunks.ru repositories - gorecfile.git/blob - r.go
Faster key-value parsing
[gorecfile.git] / r.go
1 /*
2 recfile -- GNU recutils'es recfiles parser on pure Go
3 Copyright (C) 2020-2022 Sergey Matveev <stargrave@stargrave.org>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, version 3 of the License.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 package recfile
19
20 import (
21         "bufio"
22         "errors"
23         "io"
24         "strings"
25 )
26
27 type Reader struct {
28         scanner *bufio.Scanner
29 }
30
31 // Create Reader for iterating through the records. It uses
32 // bufio.Scanner, so can read more than currently parsed records take.
33 func NewReader(r io.Reader) *Reader {
34         return &Reader{bufio.NewScanner(r)}
35 }
36
37 func getKeyValue(text string) (string, string) {
38         cols := strings.SplitN(text, ":", 2)
39         if len(cols) != 2 {
40                 return "", ""
41         }
42         k := cols[0]
43         if len(k) == 0 {
44                 return "", ""
45         }
46         if !((k[0] == '%') ||
47                 ('a' <= k[0] && k[0] <= 'z') ||
48                 ('A' <= k[0] && k[0] <= 'Z')) {
49                 return "", ""
50         }
51         for _, c := range k {
52                 if !((c == '_') ||
53                         ('a' <= c && c <= 'z') ||
54                         ('A' <= c && c <= 'Z') ||
55                         ('0' <= c && c <= '9')) {
56                         return "", ""
57                 }
58         }
59         return k, strings.TrimPrefix(cols[1], " ")
60 }
61
62 // Get next record. Each record is just a collection of fields. io.EOF
63 // is returned if there is nothing to read more.
64 func (r *Reader) Next() ([]Field, error) {
65         fields := make([]Field, 0, 1)
66         var text string
67         var name string
68         var line string
69         lines := make([]string, 0)
70         continuation := false
71         var err error
72         for {
73                 if !r.scanner.Scan() {
74                         if err := r.scanner.Err(); err != nil {
75                                 return fields, err
76                         }
77                         err = io.EOF
78                         break
79                 }
80                 text = r.scanner.Text()
81
82                 if continuation {
83                         if text[len(text)-1] == '\\' {
84                                 lines = append(lines, text[:len(text)-1])
85                         } else {
86                                 lines = append(lines, text)
87                                 fields = append(fields, Field{name, strings.Join(lines, "")})
88                                 lines = make([]string, 0)
89                                 continuation = false
90                         }
91                         continue
92                 }
93
94                 if len(text) > 0 && text[0] == '#' {
95                         continue
96                 }
97
98                 if len(text) > 0 && text[0] == '+' {
99                         lines = append(lines, "\n")
100                         if len(text) > 1 {
101                                 if text[1] == ' ' {
102                                         lines = append(lines, text[2:])
103                                 } else {
104                                         lines = append(lines, text[1:])
105                                 }
106                         }
107                         continue
108                 }
109
110                 if len(lines) > 0 {
111                         fields = append(fields, Field{name, strings.Join(lines, "")})
112                         lines = make([]string, 0)
113                 }
114
115                 if text == "" {
116                         break
117                 }
118
119                 name, line = getKeyValue(text)
120                 if name == "" {
121                         return fields, errors.New("invalid field format")
122                 }
123
124                 if len(line) > 0 && line[len(line)-1] == '\\' {
125                         continuation = true
126                         lines = append(lines, line[:len(line)-1])
127                 } else {
128                         lines = append(lines, line)
129                 }
130         }
131         if continuation {
132                 return fields, errors.New("left continuation")
133         }
134         if len(lines) > 0 {
135                 fields = append(fields, Field{name, strings.Join(lines, "")})
136         }
137         if len(fields) == 0 {
138                 if err == nil {
139                         return r.Next()
140                 }
141                 return fields, err
142         }
143         return fields, nil
144 }
145
146 // Same as Next(), but with unique keys and last value.
147 func (r *Reader) NextMap() (map[string]string, error) {
148         fields, err := r.Next()
149         if err != nil {
150                 return nil, err
151         }
152         m := make(map[string]string, len(fields))
153         for _, f := range fields {
154                 m[f.Name] = f.Value
155         }
156         return m, nil
157 }
158
159 // Same as Next(), but with unique keys and slices of values.
160 func (r *Reader) NextMapWithSlice() (map[string][]string, error) {
161         fields, err := r.Next()
162         if err != nil {
163                 return nil, err
164         }
165         m := make(map[string][]string)
166         for _, f := range fields {
167                 m[f.Name] = append(m[f.Name], f.Value)
168         }
169         return m, nil
170 }