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