]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | // Copyright 2014 Unknwon |
2 | // | |
3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | // not use this file except in compliance with the License. You may obtain | |
5 | // a copy of the License at | |
6 | // | |
7 | // http://www.apache.org/licenses/LICENSE-2.0 | |
8 | // | |
9 | // Unless required by applicable law or agreed to in writing, software | |
10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | // License for the specific language governing permissions and limitations | |
13 | // under the License. | |
14 | ||
15 | // Package ini provides INI file read and write functionality in Go. | |
16 | package ini | |
17 | ||
18 | import ( | |
19 | "bytes" | |
20 | "errors" | |
21 | "fmt" | |
22 | "io" | |
23 | "io/ioutil" | |
24 | "os" | |
25 | "regexp" | |
26 | "runtime" | |
27 | "strconv" | |
28 | "strings" | |
29 | "sync" | |
30 | "time" | |
31 | ) | |
32 | ||
33 | const ( | |
34 | // Name for default section. You can use this constant or the string literal. | |
35 | // In most of cases, an empty string is all you need to access the section. | |
36 | DEFAULT_SECTION = "DEFAULT" | |
37 | ||
38 | // Maximum allowed depth when recursively substituing variable names. | |
39 | _DEPTH_VALUES = 99 | |
15c0b25d | 40 | _VERSION = "1.25.4" |
bae9f6d2 JC |
41 | ) |
42 | ||
43 | // Version returns current package version literal. | |
44 | func Version() string { | |
45 | return _VERSION | |
46 | } | |
47 | ||
48 | var ( | |
49 | // Delimiter to determine or compose a new line. | |
50 | // This variable will be changed to "\r\n" automatically on Windows | |
51 | // at package init time. | |
52 | LineBreak = "\n" | |
53 | ||
54 | // Variable regexp pattern: %(variable)s | |
55 | varPattern = regexp.MustCompile(`%\(([^\)]+)\)s`) | |
56 | ||
57 | // Indicate whether to align "=" sign with spaces to produce pretty output | |
58 | // or reduce all possible spaces for compact format. | |
59 | PrettyFormat = true | |
60 | ||
61 | // Explicitly write DEFAULT section header | |
62 | DefaultHeader = false | |
63 | ) | |
64 | ||
65 | func init() { | |
66 | if runtime.GOOS == "windows" { | |
67 | LineBreak = "\r\n" | |
68 | } | |
69 | } | |
70 | ||
71 | func inSlice(str string, s []string) bool { | |
72 | for _, v := range s { | |
73 | if str == v { | |
74 | return true | |
75 | } | |
76 | } | |
77 | return false | |
78 | } | |
79 | ||
80 | // dataSource is an interface that returns object which can be read and closed. | |
81 | type dataSource interface { | |
82 | ReadCloser() (io.ReadCloser, error) | |
83 | } | |
84 | ||
85 | // sourceFile represents an object that contains content on the local file system. | |
86 | type sourceFile struct { | |
87 | name string | |
88 | } | |
89 | ||
90 | func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) { | |
91 | return os.Open(s.name) | |
92 | } | |
93 | ||
94 | type bytesReadCloser struct { | |
95 | reader io.Reader | |
96 | } | |
97 | ||
98 | func (rc *bytesReadCloser) Read(p []byte) (n int, err error) { | |
99 | return rc.reader.Read(p) | |
100 | } | |
101 | ||
102 | func (rc *bytesReadCloser) Close() error { | |
103 | return nil | |
104 | } | |
105 | ||
106 | // sourceData represents an object that contains content in memory. | |
107 | type sourceData struct { | |
108 | data []byte | |
109 | } | |
110 | ||
111 | func (s *sourceData) ReadCloser() (io.ReadCloser, error) { | |
112 | return ioutil.NopCloser(bytes.NewReader(s.data)), nil | |
113 | } | |
114 | ||
115 | // sourceReadCloser represents an input stream with Close method. | |
116 | type sourceReadCloser struct { | |
117 | reader io.ReadCloser | |
118 | } | |
119 | ||
120 | func (s *sourceReadCloser) ReadCloser() (io.ReadCloser, error) { | |
121 | return s.reader, nil | |
122 | } | |
123 | ||
124 | // File represents a combination of a or more INI file(s) in memory. | |
125 | type File struct { | |
126 | // Should make things safe, but sometimes doesn't matter. | |
127 | BlockMode bool | |
128 | // Make sure data is safe in multiple goroutines. | |
129 | lock sync.RWMutex | |
130 | ||
131 | // Allow combination of multiple data sources. | |
132 | dataSources []dataSource | |
133 | // Actual data is stored here. | |
134 | sections map[string]*Section | |
135 | ||
136 | // To keep data in order. | |
137 | sectionList []string | |
138 | ||
139 | options LoadOptions | |
140 | ||
141 | NameMapper | |
142 | ValueMapper | |
143 | } | |
144 | ||
145 | // newFile initializes File object with given data sources. | |
146 | func newFile(dataSources []dataSource, opts LoadOptions) *File { | |
147 | return &File{ | |
148 | BlockMode: true, | |
149 | dataSources: dataSources, | |
150 | sections: make(map[string]*Section), | |
151 | sectionList: make([]string, 0, 10), | |
152 | options: opts, | |
153 | } | |
154 | } | |
155 | ||
156 | func parseDataSource(source interface{}) (dataSource, error) { | |
157 | switch s := source.(type) { | |
158 | case string: | |
159 | return sourceFile{s}, nil | |
160 | case []byte: | |
161 | return &sourceData{s}, nil | |
162 | case io.ReadCloser: | |
163 | return &sourceReadCloser{s}, nil | |
164 | default: | |
165 | return nil, fmt.Errorf("error parsing data source: unknown type '%s'", s) | |
166 | } | |
167 | } | |
168 | ||
169 | type LoadOptions struct { | |
170 | // Loose indicates whether the parser should ignore nonexistent files or return error. | |
171 | Loose bool | |
172 | // Insensitive indicates whether the parser forces all section and key names to lowercase. | |
173 | Insensitive bool | |
174 | // IgnoreContinuation indicates whether to ignore continuation lines while parsing. | |
175 | IgnoreContinuation bool | |
176 | // AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing. | |
177 | // This type of keys are mostly used in my.cnf. | |
178 | AllowBooleanKeys bool | |
15c0b25d AP |
179 | // AllowShadows indicates whether to keep track of keys with same name under same section. |
180 | AllowShadows bool | |
bae9f6d2 JC |
181 | // Some INI formats allow group blocks that store a block of raw content that doesn't otherwise |
182 | // conform to key/value pairs. Specify the names of those blocks here. | |
183 | UnparseableSections []string | |
184 | } | |
185 | ||
186 | func LoadSources(opts LoadOptions, source interface{}, others ...interface{}) (_ *File, err error) { | |
187 | sources := make([]dataSource, len(others)+1) | |
188 | sources[0], err = parseDataSource(source) | |
189 | if err != nil { | |
190 | return nil, err | |
191 | } | |
192 | for i := range others { | |
193 | sources[i+1], err = parseDataSource(others[i]) | |
194 | if err != nil { | |
195 | return nil, err | |
196 | } | |
197 | } | |
198 | f := newFile(sources, opts) | |
199 | if err = f.Reload(); err != nil { | |
200 | return nil, err | |
201 | } | |
202 | return f, nil | |
203 | } | |
204 | ||
205 | // Load loads and parses from INI data sources. | |
206 | // Arguments can be mixed of file name with string type, or raw data in []byte. | |
207 | // It will return error if list contains nonexistent files. | |
208 | func Load(source interface{}, others ...interface{}) (*File, error) { | |
209 | return LoadSources(LoadOptions{}, source, others...) | |
210 | } | |
211 | ||
212 | // LooseLoad has exactly same functionality as Load function | |
213 | // except it ignores nonexistent files instead of returning error. | |
214 | func LooseLoad(source interface{}, others ...interface{}) (*File, error) { | |
215 | return LoadSources(LoadOptions{Loose: true}, source, others...) | |
216 | } | |
217 | ||
218 | // InsensitiveLoad has exactly same functionality as Load function | |
219 | // except it forces all section and key names to be lowercased. | |
220 | func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) { | |
221 | return LoadSources(LoadOptions{Insensitive: true}, source, others...) | |
222 | } | |
223 | ||
15c0b25d AP |
224 | // InsensitiveLoad has exactly same functionality as Load function |
225 | // except it allows have shadow keys. | |
226 | func ShadowLoad(source interface{}, others ...interface{}) (*File, error) { | |
227 | return LoadSources(LoadOptions{AllowShadows: true}, source, others...) | |
228 | } | |
229 | ||
bae9f6d2 JC |
230 | // Empty returns an empty file object. |
231 | func Empty() *File { | |
232 | // Ignore error here, we sure our data is good. | |
233 | f, _ := Load([]byte("")) | |
234 | return f | |
235 | } | |
236 | ||
237 | // NewSection creates a new section. | |
238 | func (f *File) NewSection(name string) (*Section, error) { | |
239 | if len(name) == 0 { | |
240 | return nil, errors.New("error creating new section: empty section name") | |
241 | } else if f.options.Insensitive && name != DEFAULT_SECTION { | |
242 | name = strings.ToLower(name) | |
243 | } | |
244 | ||
245 | if f.BlockMode { | |
246 | f.lock.Lock() | |
247 | defer f.lock.Unlock() | |
248 | } | |
249 | ||
250 | if inSlice(name, f.sectionList) { | |
251 | return f.sections[name], nil | |
252 | } | |
253 | ||
254 | f.sectionList = append(f.sectionList, name) | |
255 | f.sections[name] = newSection(f, name) | |
256 | return f.sections[name], nil | |
257 | } | |
258 | ||
259 | // NewRawSection creates a new section with an unparseable body. | |
260 | func (f *File) NewRawSection(name, body string) (*Section, error) { | |
261 | section, err := f.NewSection(name) | |
262 | if err != nil { | |
263 | return nil, err | |
264 | } | |
265 | ||
266 | section.isRawSection = true | |
267 | section.rawBody = body | |
268 | return section, nil | |
269 | } | |
270 | ||
271 | // NewSections creates a list of sections. | |
272 | func (f *File) NewSections(names ...string) (err error) { | |
273 | for _, name := range names { | |
274 | if _, err = f.NewSection(name); err != nil { | |
275 | return err | |
276 | } | |
277 | } | |
278 | return nil | |
279 | } | |
280 | ||
281 | // GetSection returns section by given name. | |
282 | func (f *File) GetSection(name string) (*Section, error) { | |
283 | if len(name) == 0 { | |
284 | name = DEFAULT_SECTION | |
285 | } else if f.options.Insensitive { | |
286 | name = strings.ToLower(name) | |
287 | } | |
288 | ||
289 | if f.BlockMode { | |
290 | f.lock.RLock() | |
291 | defer f.lock.RUnlock() | |
292 | } | |
293 | ||
294 | sec := f.sections[name] | |
295 | if sec == nil { | |
296 | return nil, fmt.Errorf("section '%s' does not exist", name) | |
297 | } | |
298 | return sec, nil | |
299 | } | |
300 | ||
301 | // Section assumes named section exists and returns a zero-value when not. | |
302 | func (f *File) Section(name string) *Section { | |
303 | sec, err := f.GetSection(name) | |
304 | if err != nil { | |
305 | // Note: It's OK here because the only possible error is empty section name, | |
306 | // but if it's empty, this piece of code won't be executed. | |
307 | sec, _ = f.NewSection(name) | |
308 | return sec | |
309 | } | |
310 | return sec | |
311 | } | |
312 | ||
313 | // Section returns list of Section. | |
314 | func (f *File) Sections() []*Section { | |
315 | sections := make([]*Section, len(f.sectionList)) | |
316 | for i := range f.sectionList { | |
317 | sections[i] = f.Section(f.sectionList[i]) | |
318 | } | |
319 | return sections | |
320 | } | |
321 | ||
322 | // SectionStrings returns list of section names. | |
323 | func (f *File) SectionStrings() []string { | |
324 | list := make([]string, len(f.sectionList)) | |
325 | copy(list, f.sectionList) | |
326 | return list | |
327 | } | |
328 | ||
329 | // DeleteSection deletes a section. | |
330 | func (f *File) DeleteSection(name string) { | |
331 | if f.BlockMode { | |
332 | f.lock.Lock() | |
333 | defer f.lock.Unlock() | |
334 | } | |
335 | ||
336 | if len(name) == 0 { | |
337 | name = DEFAULT_SECTION | |
338 | } | |
339 | ||
340 | for i, s := range f.sectionList { | |
341 | if s == name { | |
342 | f.sectionList = append(f.sectionList[:i], f.sectionList[i+1:]...) | |
343 | delete(f.sections, name) | |
344 | return | |
345 | } | |
346 | } | |
347 | } | |
348 | ||
349 | func (f *File) reload(s dataSource) error { | |
350 | r, err := s.ReadCloser() | |
351 | if err != nil { | |
352 | return err | |
353 | } | |
354 | defer r.Close() | |
355 | ||
356 | return f.parse(r) | |
357 | } | |
358 | ||
359 | // Reload reloads and parses all data sources. | |
360 | func (f *File) Reload() (err error) { | |
361 | for _, s := range f.dataSources { | |
362 | if err = f.reload(s); err != nil { | |
363 | // In loose mode, we create an empty default section for nonexistent files. | |
364 | if os.IsNotExist(err) && f.options.Loose { | |
365 | f.parse(bytes.NewBuffer(nil)) | |
366 | continue | |
367 | } | |
368 | return err | |
369 | } | |
370 | } | |
371 | return nil | |
372 | } | |
373 | ||
374 | // Append appends one or more data sources and reloads automatically. | |
375 | func (f *File) Append(source interface{}, others ...interface{}) error { | |
376 | ds, err := parseDataSource(source) | |
377 | if err != nil { | |
378 | return err | |
379 | } | |
380 | f.dataSources = append(f.dataSources, ds) | |
381 | for _, s := range others { | |
382 | ds, err = parseDataSource(s) | |
383 | if err != nil { | |
384 | return err | |
385 | } | |
386 | f.dataSources = append(f.dataSources, ds) | |
387 | } | |
388 | return f.Reload() | |
389 | } | |
390 | ||
391 | // WriteToIndent writes content into io.Writer with given indention. | |
392 | // If PrettyFormat has been set to be true, | |
393 | // it will align "=" sign with spaces under each section. | |
394 | func (f *File) WriteToIndent(w io.Writer, indent string) (n int64, err error) { | |
395 | equalSign := "=" | |
396 | if PrettyFormat { | |
397 | equalSign = " = " | |
398 | } | |
399 | ||
400 | // Use buffer to make sure target is safe until finish encoding. | |
401 | buf := bytes.NewBuffer(nil) | |
402 | for i, sname := range f.sectionList { | |
403 | sec := f.Section(sname) | |
404 | if len(sec.Comment) > 0 { | |
405 | if sec.Comment[0] != '#' && sec.Comment[0] != ';' { | |
406 | sec.Comment = "; " + sec.Comment | |
407 | } | |
408 | if _, err = buf.WriteString(sec.Comment + LineBreak); err != nil { | |
409 | return 0, err | |
410 | } | |
411 | } | |
412 | ||
413 | if i > 0 || DefaultHeader { | |
414 | if _, err = buf.WriteString("[" + sname + "]" + LineBreak); err != nil { | |
415 | return 0, err | |
416 | } | |
417 | } else { | |
418 | // Write nothing if default section is empty | |
419 | if len(sec.keyList) == 0 { | |
420 | continue | |
421 | } | |
422 | } | |
423 | ||
424 | if sec.isRawSection { | |
425 | if _, err = buf.WriteString(sec.rawBody); err != nil { | |
426 | return 0, err | |
427 | } | |
428 | continue | |
429 | } | |
430 | ||
431 | // Count and generate alignment length and buffer spaces using the | |
432 | // longest key. Keys may be modifed if they contain certain characters so | |
433 | // we need to take that into account in our calculation. | |
434 | alignLength := 0 | |
435 | if PrettyFormat { | |
436 | for _, kname := range sec.keyList { | |
437 | keyLength := len(kname) | |
438 | // First case will surround key by ` and second by """ | |
439 | if strings.ContainsAny(kname, "\"=:") { | |
440 | keyLength += 2 | |
441 | } else if strings.Contains(kname, "`") { | |
442 | keyLength += 6 | |
443 | } | |
444 | ||
445 | if keyLength > alignLength { | |
446 | alignLength = keyLength | |
447 | } | |
448 | } | |
449 | } | |
450 | alignSpaces := bytes.Repeat([]byte(" "), alignLength) | |
451 | ||
15c0b25d | 452 | KEY_LIST: |
bae9f6d2 JC |
453 | for _, kname := range sec.keyList { |
454 | key := sec.Key(kname) | |
455 | if len(key.Comment) > 0 { | |
456 | if len(indent) > 0 && sname != DEFAULT_SECTION { | |
457 | buf.WriteString(indent) | |
458 | } | |
459 | if key.Comment[0] != '#' && key.Comment[0] != ';' { | |
460 | key.Comment = "; " + key.Comment | |
461 | } | |
462 | if _, err = buf.WriteString(key.Comment + LineBreak); err != nil { | |
463 | return 0, err | |
464 | } | |
465 | } | |
466 | ||
467 | if len(indent) > 0 && sname != DEFAULT_SECTION { | |
468 | buf.WriteString(indent) | |
469 | } | |
470 | ||
471 | switch { | |
472 | case key.isAutoIncrement: | |
473 | kname = "-" | |
474 | case strings.ContainsAny(kname, "\"=:"): | |
475 | kname = "`" + kname + "`" | |
476 | case strings.Contains(kname, "`"): | |
477 | kname = `"""` + kname + `"""` | |
478 | } | |
bae9f6d2 | 479 | |
15c0b25d AP |
480 | for _, val := range key.ValueWithShadows() { |
481 | if _, err = buf.WriteString(kname); err != nil { | |
482 | return 0, err | |
483 | } | |
bae9f6d2 | 484 | |
15c0b25d AP |
485 | if key.isBooleanType { |
486 | if kname != sec.keyList[len(sec.keyList)-1] { | |
487 | buf.WriteString(LineBreak) | |
488 | } | |
489 | continue KEY_LIST | |
490 | } | |
bae9f6d2 | 491 | |
15c0b25d AP |
492 | // Write out alignment spaces before "=" sign |
493 | if PrettyFormat { | |
494 | buf.Write(alignSpaces[:alignLength-len(kname)]) | |
495 | } | |
496 | ||
497 | // In case key value contains "\n", "`", "\"", "#" or ";" | |
498 | if strings.ContainsAny(val, "\n`") { | |
499 | val = `"""` + val + `"""` | |
500 | } else if strings.ContainsAny(val, "#;") { | |
501 | val = "`" + val + "`" | |
502 | } | |
503 | if _, err = buf.WriteString(equalSign + val + LineBreak); err != nil { | |
504 | return 0, err | |
505 | } | |
bae9f6d2 JC |
506 | } |
507 | } | |
508 | ||
509 | // Put a line between sections | |
510 | if _, err = buf.WriteString(LineBreak); err != nil { | |
511 | return 0, err | |
512 | } | |
513 | } | |
514 | ||
515 | return buf.WriteTo(w) | |
516 | } | |
517 | ||
518 | // WriteTo writes file content into io.Writer. | |
519 | func (f *File) WriteTo(w io.Writer) (int64, error) { | |
520 | return f.WriteToIndent(w, "") | |
521 | } | |
522 | ||
523 | // SaveToIndent writes content to file system with given value indention. | |
524 | func (f *File) SaveToIndent(filename, indent string) error { | |
525 | // Note: Because we are truncating with os.Create, | |
526 | // so it's safer to save to a temporary file location and rename afte done. | |
527 | tmpPath := filename + "." + strconv.Itoa(time.Now().Nanosecond()) + ".tmp" | |
528 | defer os.Remove(tmpPath) | |
529 | ||
530 | fw, err := os.Create(tmpPath) | |
531 | if err != nil { | |
532 | return err | |
533 | } | |
534 | ||
535 | if _, err = f.WriteToIndent(fw, indent); err != nil { | |
536 | fw.Close() | |
537 | return err | |
538 | } | |
539 | fw.Close() | |
540 | ||
541 | // Remove old file and rename the new one. | |
542 | os.Remove(filename) | |
543 | return os.Rename(tmpPath, filename) | |
544 | } | |
545 | ||
546 | // SaveTo writes content to file system. | |
547 | func (f *File) SaveTo(filename string) error { | |
548 | return f.SaveToIndent(filename, "") | |
549 | } |