├── MIT-LICENSE.txt ├── README ├── README.md ├── configfile.go └── configfile_test.go /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2009-2013 Miguel Branco 2 | msbranco@gmail.com 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | This package implements a parser for configuration files. 2 | This allows easy reading and writing of structured configuration files. 3 | 4 | Given a sample configuration file: 5 | 6 | [default] 7 | host=www.example.com 8 | protocol=http:// 9 | base-url=%(protocol)s%(host)s 10 | 11 | [service-1] 12 | url=%(base-url)s/some/path 13 | delegation : on 14 | maxclients=200 # do not set this higher 15 | comments=This is a multi-line 16 | entry ; And this is a comment 17 | 18 | To read this configuration file, do: 19 | 20 | c, err := configfile.ReadConfigFile("config.cfg"); 21 | // result is string :http://www.example.com/some/path" 22 | c.GetString("service-1", "url"); 23 | c.GetInt64("service-1", "maxclients"); // result is int 200 24 | c.GetBool("service-1", "delegation"); // result is bool true 25 | 26 | // result is string "This is a multi-line\nentry" 27 | c.GetString("service-1", "comments"); 28 | 29 | Note the support for unfolding variables (such as %(base-url)s), which are 30 | read from the special (reserved) section name [default]. 31 | 32 | A new configuration file can also be created with: 33 | c := configfile.NewConfigFile(); 34 | c.AddSection("section"); 35 | c.AddOption("section", "option", "value"); 36 | // use 0644 as file permission 37 | c.WriteConfigFile("config.cfg", 0644, "A header for this file"); 38 | 39 | This results in the file: 40 | 41 | # A header for this file 42 | [section] 43 | option=value 44 | 45 | Note that sections and options are case-insensitive (values are 46 | case-sensitive) and are converted to lowercase when saved to a file. 47 | 48 | The functionality and workflow is loosely based on the configparser.py 49 | package of the Python Standard Library. 50 | 51 | To install: 52 | 53 | go get "github.com/msbranco/goconfig" 54 | 55 | To test: 56 | 57 | go test 58 | 59 | To use: 60 | 61 | import "github.com/msbranco/goconfig" 62 | 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project has been deprecated. Please use the fork at [https://github.com/robfig/config](https://github.com/robfig/config). 2 | -------------------------------------------------------------------------------- /configfile.go: -------------------------------------------------------------------------------- 1 | // This package implements a parser for configuration files. 2 | // This allows easy reading and writing of structured configuration files. 3 | // 4 | // Given a sample configuration file: 5 | // 6 | // [default] 7 | // host=www.example.com 8 | // protocol=http:// 9 | // base-url=%(protocol)s%(host)s 10 | // 11 | // [service-1] 12 | // url=%(base-url)s/some/path 13 | // delegation : on 14 | // maxclients=200 # do not set this higher 15 | // comments=This is a multi-line 16 | // entry ; And this is a comment 17 | // 18 | // To read this configuration file, do: 19 | // 20 | // c, err := configfile.ReadConfigFile("config.cfg"); 21 | // 22 | // // result is string :http://www.example.com/some/path" 23 | // c.GetString("service-1", "url"); 24 | // 25 | // c.GetInt64("service-1", "maxclients"); // result is int 200 26 | // c.GetBool("service-1", "delegation"); // result is bool true 27 | // 28 | // // result is string "This is a multi-line\nentry" 29 | // c.GetString("service-1", "comments"); 30 | // 31 | // Note the support for unfolding variables (such as %(base-url)s), which are 32 | // read from the special (reserved) section name [default]. 33 | // 34 | // A new configuration file can also be created with: 35 | // 36 | // c := configfile.NewConfigFile(); 37 | // c.AddSection("section"); 38 | // c.AddOption("section", "option", "value"); 39 | // // use 0644 as file permission 40 | // c.WriteConfigFile("config.cfg", 0644, "A header for this file"); 41 | // 42 | // This results in the file: 43 | // 44 | // # A header for this file 45 | // [section] 46 | // option=value 47 | // 48 | // Note that sections and options are case-insensitive (values are 49 | // case-sensitive) and are converted to lowercase when saved to a file. 50 | // 51 | // The functionality and workflow is loosely based on the configparser.py 52 | // package of the Python Standard Library. 53 | package goconfig 54 | 55 | import ( 56 | "bufio" 57 | "errors" 58 | "fmt" 59 | "io" 60 | "os" 61 | "regexp" 62 | "strconv" 63 | "strings" 64 | ) 65 | 66 | // ConfigFile is the representation of configuration settings. 67 | // The public interface is entirely through methods. 68 | type ConfigFile struct { 69 | data map[string]map[string]string // Maps sections to options to values. 70 | } 71 | 72 | var ( 73 | DefaultSection = "default" // Default section name (must be lower-case). 74 | 75 | // Maximum allowed depth when recursively substituing variable names. 76 | DepthValues = 200 77 | 78 | // Strings accepted as bool. 79 | BoolStrings = map[string]bool{ 80 | "0": false, 81 | "1": true, 82 | "f": false, 83 | "false": false, 84 | "n": false, 85 | "no": false, 86 | "off": false, 87 | "on": true, 88 | "t": true, 89 | "true": true, 90 | "y": true, 91 | "yes": true, 92 | } 93 | 94 | varRegExp = regexp.MustCompile(`%\(([a-zA-Z0-9_.\-]+)\)s`) 95 | ) 96 | 97 | // AddSection adds a new section to the configuration. 98 | // It returns true if the new section was inserted, and false if the section 99 | // already existed. 100 | func (c *ConfigFile) AddSection(section string) bool { 101 | 102 | section = strings.ToLower(section) 103 | 104 | if _, ok := c.data[section]; ok { 105 | return false 106 | } 107 | c.data[section] = make(map[string]string) 108 | 109 | return true 110 | } 111 | 112 | // RemoveSection removes a section from the configuration. 113 | // It returns true if the section was removed, and false if section did not 114 | // exist. 115 | func (c *ConfigFile) RemoveSection(section string) bool { 116 | 117 | section = strings.ToLower(section) 118 | 119 | switch _, ok := c.data[section]; { 120 | case !ok: 121 | return false 122 | case section == DefaultSection: 123 | return false // default section cannot be removed 124 | default: 125 | for o, _ := range c.data[section] { 126 | delete(c.data[section], o) 127 | } 128 | delete(c.data, section) 129 | } 130 | 131 | return true 132 | } 133 | 134 | // AddOption adds a new option and value to the configuration. 135 | // It returns true if the option and value were inserted, and false if the 136 | // value was overwritten. 137 | // If the section does not exist in advance, it is created. 138 | func (c *ConfigFile) AddOption(section string, option string, value string) bool { 139 | 140 | c.AddSection(section) // make sure section exists 141 | 142 | section = strings.ToLower(section) 143 | option = strings.ToLower(option) 144 | 145 | _, ok := c.data[section][option] 146 | c.data[section][option] = value 147 | 148 | return !ok 149 | } 150 | 151 | // RemoveOption removes a option and value from the configuration. 152 | // It returns true if the option and value were removed, and false otherwise, 153 | // including if the section did not exist. 154 | func (c *ConfigFile) RemoveOption(section string, option string) bool { 155 | 156 | section = strings.ToLower(section) 157 | option = strings.ToLower(option) 158 | 159 | if _, ok := c.data[section]; !ok { 160 | return false 161 | } 162 | 163 | _, ok := c.data[section][option] 164 | delete(c.data[section], option) 165 | 166 | return ok 167 | } 168 | 169 | // NewConfigFile creates an empty configuration representation. 170 | // This representation can be filled with AddSection and AddOption and then 171 | // saved to a file using WriteConfigFile. 172 | func NewConfigFile() *ConfigFile { 173 | 174 | c := new(ConfigFile) 175 | c.data = make(map[string]map[string]string) 176 | 177 | c.AddSection(DefaultSection) // default section always exists 178 | 179 | return c 180 | } 181 | 182 | func stripComments(l string) string { 183 | 184 | // comments are preceded by space or TAB 185 | for _, c := range []string{" ;", "\t;", " #", "\t#"} { 186 | if i := strings.Index(l, c); i != -1 { 187 | l = l[0:i] 188 | } 189 | } 190 | 191 | return l 192 | } 193 | 194 | func firstIndex(s string, delim []byte) int { 195 | 196 | for i := 0; i < len(s); i++ { 197 | for j := 0; j < len(delim); j++ { 198 | if s[i] == delim[j] { 199 | return i 200 | } 201 | } 202 | } 203 | 204 | return -1 205 | } 206 | 207 | func (c *ConfigFile) read(buf *bufio.Reader) error { 208 | 209 | var section, option string 210 | 211 | for { 212 | l, err := buf.ReadString('\n') // parse line-by-line 213 | if err == io.EOF { 214 | if len(l) == 0 { 215 | break 216 | } 217 | } else if err != nil { 218 | return err 219 | } 220 | 221 | l = strings.TrimSpace(l) 222 | // switch written for readability (not performance) 223 | switch { 224 | case len(l) == 0: // empty line 225 | continue 226 | 227 | case l[0] == '#': // comment 228 | continue 229 | 230 | case l[0] == ';': // comment 231 | continue 232 | 233 | case len(l) >= 3 && strings.ToLower(l[0:3]) == "rem": 234 | // comment (for windows users) 235 | continue 236 | 237 | case l[0] == '[' && l[len(l)-1] == ']': // new section 238 | option = "" // reset multi-line value 239 | section = strings.TrimSpace(l[1 : len(l)-1]) 240 | c.AddSection(section) 241 | 242 | case section == "": // not new section and no section defined so far 243 | return errors.New("Section not found: must start with section") 244 | 245 | default: // other alternatives 246 | i := firstIndex(l, []byte{'=', ':'}) 247 | switch { 248 | case i > 0: // option and value 249 | i := firstIndex(l, []byte{'=', ':'}) 250 | option = strings.TrimSpace(l[0:i]) 251 | value := strings.TrimSpace(stripComments(l[i+1:])) 252 | c.AddOption(section, option, value) 253 | 254 | case section != "" && option != "": 255 | // continuation of multi-line value 256 | prev, _ := c.GetRawString(section, option) 257 | value := strings.TrimSpace(stripComments(l)) 258 | c.AddOption(section, option, prev+"\n"+value) 259 | 260 | default: 261 | return errors.New(fmt.Sprintf("Could not parse line: %s", l)) 262 | } 263 | } 264 | } 265 | 266 | return nil 267 | } 268 | 269 | // ReadConfigFile reads a file and returns a new configuration representation. 270 | // This representation can be queried with GetString, etc. 271 | func ReadConfigFile(fname string) (*ConfigFile, error) { 272 | 273 | file, err := os.Open(fname) 274 | if err != nil { 275 | return nil, err 276 | } 277 | 278 | c := NewConfigFile() 279 | if err := c.read(bufio.NewReader(file)); err != nil { 280 | return nil, err 281 | } 282 | 283 | if err := file.Close(); err != nil { 284 | return nil, err 285 | } 286 | 287 | return c, nil 288 | } 289 | 290 | func (c *ConfigFile) write(buf *bufio.Writer, header string) error { 291 | 292 | if header != "" { 293 | _, err := buf.WriteString(fmt.Sprintf("# %s\n", header)) 294 | if err != nil { 295 | return err 296 | } 297 | } 298 | 299 | for section, sectionmap := range c.data { 300 | 301 | if section == DefaultSection && len(sectionmap) == 0 { 302 | continue // skip default section if empty 303 | } 304 | 305 | _, err := buf.WriteString(fmt.Sprintf("[%s]\n", section)) 306 | if err != nil { 307 | return err 308 | } 309 | 310 | for option, value := range sectionmap { 311 | _, err := buf.WriteString(fmt.Sprintf("%s = %s\n", option, value)) 312 | if err != nil { 313 | return err 314 | } 315 | } 316 | 317 | if _, err := buf.WriteString("\n"); err != nil { 318 | return err 319 | } 320 | } 321 | 322 | return nil 323 | } 324 | 325 | // WriteConfigFile saves the configuration representation to a file. 326 | // The desired file permissions must be passed as in os.Open. 327 | // The header is a string that is saved as a comment in the first line of the file. 328 | func (c *ConfigFile) WriteConfigFile(fname string, perm uint32, header string) error { 329 | 330 | var file *os.File 331 | 332 | file, err := os.OpenFile(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(perm)) 333 | if err != nil { 334 | return err 335 | } 336 | 337 | buf := bufio.NewWriter(file) 338 | if err := c.write(buf, header); err != nil { 339 | return err 340 | } 341 | buf.Flush() 342 | 343 | return file.Close() 344 | } 345 | 346 | // GetSections returns the list of sections in the configuration. 347 | // (The default section always exists.) 348 | func (c *ConfigFile) GetSections() (sections []string) { 349 | 350 | sections = make([]string, len(c.data)) 351 | 352 | i := 0 353 | for s, _ := range c.data { 354 | sections[i] = s 355 | i++ 356 | } 357 | 358 | return sections 359 | } 360 | 361 | // HasSection checks if the configuration has the given section. 362 | // (The default section always exists.) 363 | func (c *ConfigFile) HasSection(section string) bool { 364 | 365 | _, ok := c.data[strings.ToLower(section)] 366 | 367 | return ok 368 | } 369 | 370 | // GetOptions returns the list of options available in the given section. 371 | // It returns an error if the section does not exist and an empty list if the 372 | // section is empty. 373 | // Options within the default section are also included. 374 | func (c *ConfigFile) GetOptions(section string) ([]string, error) { 375 | 376 | section = strings.ToLower(section) 377 | 378 | if _, ok := c.data[section]; !ok { 379 | return nil, errors.New( 380 | fmt.Sprintf("Section not found: %s", section), 381 | ) 382 | } 383 | 384 | options := make([]string, len(c.data[DefaultSection])+len(c.data[section])) 385 | 386 | i := 0 387 | for s, _ := range c.data[DefaultSection] { 388 | options[i] = s 389 | i++ 390 | } 391 | 392 | for s, _ := range c.data[section] { 393 | options[i] = s 394 | i++ 395 | } 396 | 397 | return options, nil 398 | } 399 | 400 | // HasOption checks if the configuration has the given option in the section. 401 | // It returns false if either the option or section do not exist. 402 | func (c *ConfigFile) HasOption(section string, option string) bool { 403 | 404 | section = strings.ToLower(section) 405 | option = strings.ToLower(option) 406 | 407 | if _, ok := c.data[section]; !ok { 408 | return false 409 | } 410 | 411 | _, okd := c.data[DefaultSection][option] 412 | _, oknd := c.data[section][option] 413 | 414 | return okd || oknd 415 | } 416 | 417 | // GetRawString gets the (raw) string value for the given option in the 418 | // section. 419 | // The raw string value is not subjected to unfolding, which was illustrated 420 | // in the beginning of this documentation. 421 | // It returns an error if either the section or the option do not exist. 422 | func (c *ConfigFile) GetRawString(section string, option string) (string, error) { 423 | 424 | section = strings.ToLower(section) 425 | option = strings.ToLower(option) 426 | 427 | if _, ok := c.data[section]; ok { 428 | 429 | if value, ok := c.data[section][option]; ok { 430 | return value, nil 431 | } 432 | 433 | return "", errors.New(fmt.Sprintf("Option not found: %s", option)) 434 | } 435 | 436 | return "", errors.New(fmt.Sprintf("Section not found: %s", section)) 437 | } 438 | 439 | // GetString gets the string value for the given option in the section. 440 | // If the value needs to be unfolded (see e.g. %(host)s example in the 441 | // beginning of this documentation), 442 | // then GetString does this unfolding automatically, up to DepthValues number 443 | // of iterations. 444 | // It returns an error if either the section or the option do not exist, or 445 | // the unfolding cycled. 446 | func (c *ConfigFile) GetString(section string, option string) (string, error) { 447 | 448 | value, err := c.GetRawString(section, option) 449 | if err != nil { 450 | return "", err 451 | } 452 | 453 | section = strings.ToLower(section) 454 | 455 | var i int 456 | 457 | for i = 0; i < DepthValues; i++ { // keep a sane depth 458 | 459 | vr := varRegExp.FindStringSubmatchIndex(value) 460 | if len(vr) == 0 { 461 | break 462 | } 463 | 464 | noption := value[vr[2]:vr[3]] 465 | noption = strings.ToLower(noption) 466 | 467 | // search variable in default section 468 | nvalue, _ := c.data[DefaultSection][noption] 469 | if _, ok := c.data[section][noption]; ok { 470 | nvalue = c.data[section][noption] 471 | } 472 | 473 | if nvalue == "" { 474 | return "", errors.New(fmt.Sprintf("Option not found: %s", noption)) 475 | } 476 | 477 | // substitute by new value and take off leading '%(' and trailing ')s' 478 | value = value[0:vr[2]-2] + nvalue + value[vr[3]+2:] 479 | } 480 | 481 | if i == DepthValues { 482 | return "", 483 | errors.New( 484 | fmt.Sprintf( 485 | "Possible cycle while unfolding variables: max depth of %d reached", 486 | strconv.Itoa(DepthValues), 487 | ), 488 | ) 489 | } 490 | 491 | return value, nil 492 | } 493 | 494 | // GetInt has the same behaviour as GetString but converts the response to int. 495 | func (c *ConfigFile) GetInt64(section string, option string) (int64, error) { 496 | 497 | sv, err := c.GetString(section, option) 498 | if err != nil { 499 | return 0, err 500 | } 501 | 502 | value, err := strconv.ParseInt(sv, 10, 64) 503 | if err != nil { 504 | return 0, err 505 | } 506 | 507 | return value, nil 508 | } 509 | 510 | // GetFloat has the same behaviour as GetString but converts the response to 511 | // float. 512 | func (c *ConfigFile) GetFloat(section string, option string) (float64, error) { 513 | 514 | sv, err := c.GetString(section, option) 515 | if err != nil { 516 | return float64(0), err 517 | } 518 | 519 | value, err := strconv.ParseFloat(sv, 64) 520 | if err != nil { 521 | return float64(0), err 522 | } 523 | 524 | return value, nil 525 | } 526 | 527 | // GetBool has the same behaviour as GetString but converts the response to 528 | // bool. 529 | // See constant BoolStrings for string values converted to bool. 530 | func (c *ConfigFile) GetBool(section string, option string) (bool, error) { 531 | 532 | sv, err := c.GetString(section, option) 533 | if err != nil { 534 | return false, err 535 | } 536 | 537 | value, ok := BoolStrings[strings.ToLower(sv)] 538 | if ok == false { 539 | return false, errors.New( 540 | fmt.Sprintf("Could not parse bool value: %s", sv), 541 | ) 542 | } 543 | 544 | return value, nil 545 | } 546 | -------------------------------------------------------------------------------- /configfile_test.go: -------------------------------------------------------------------------------- 1 | package goconfig_test 2 | 3 | import ( 4 | . "../goconfig" 5 | "bufio" 6 | "os" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func testGet(t *testing.T, c *ConfigFile, section string, option string, expected interface{}) { 12 | ok := false 13 | switch expected.(type) { 14 | case string: 15 | v, _ := c.GetString(section, option) 16 | if v == expected.(string) { 17 | ok = true 18 | } 19 | case int64: 20 | v, _ := c.GetInt64(section, option) 21 | if v == expected.(int64) { 22 | ok = true 23 | } 24 | case bool: 25 | v, _ := c.GetBool(section, option) 26 | if v == expected.(bool) { 27 | ok = true 28 | } 29 | default: 30 | t.Fatalf("Bad test case") 31 | } 32 | 33 | if !ok { 34 | t.Errorf("Get failure: expected different value for %s %s", section, option) 35 | } 36 | } 37 | 38 | // Create configuration representation and run multiple tests in-memory. 39 | func TestInMemory(t *testing.T) { 40 | c := NewConfigFile() 41 | 42 | // test empty structure 43 | if len(c.GetSections()) != 1 { // should be empty 44 | t.Errorf("GetSections failure: invalid length") 45 | } 46 | 47 | if c.HasSection("no-section") { // test presence of missing section 48 | t.Errorf("HasSection failure: invalid section") 49 | } 50 | 51 | _, err := c.GetOptions("no-section") // get options for missing section 52 | if err == nil { 53 | t.Errorf("GetOptions failure: invalid section") 54 | } 55 | 56 | if c.HasOption("no-section", "no-option") { 57 | // test presence of option for missing section 58 | t.Errorf("HasSection failure: invalid/section/option") 59 | } 60 | 61 | // get value from missing section/option 62 | _, err = c.GetString("no-section", "no-option") 63 | if err == nil { 64 | t.Errorf("GetString failure: got value for missing section/option") 65 | } 66 | 67 | // get value from missing section/option 68 | _, err = c.GetInt64("no-section", "no-option") 69 | if err == nil { 70 | t.Errorf("GetInt failure: got value for missing section/option") 71 | } 72 | 73 | if c.RemoveSection("no-section") { // remove missing section 74 | t.Errorf("RemoveSection failure: removed missing section") 75 | } 76 | 77 | if c.RemoveOption("no-section", "no-option") { 78 | // remove missing section/option 79 | t.Errorf("RemoveOption failure: removed missing section/option") 80 | } 81 | 82 | // fill up structure 83 | if !c.AddSection("section1") { // add section 84 | t.Errorf("AddSection failure: false on first insert") 85 | } 86 | 87 | if c.AddSection("section1") { // re-add same section 88 | t.Errorf("AddSection failure: true on second insert") 89 | } 90 | 91 | if c.AddSection(DefaultSection) { // default section always exists 92 | t.Errorf("AddSection failure: true on default section insert") 93 | } 94 | 95 | if !c.AddOption("section1", "option1", "value1") { // add option/value 96 | t.Errorf("AddOption failure: false on first insert") 97 | } 98 | testGet(t, c, "section1", "option1", "value1") // read it back 99 | 100 | if c.AddOption("section1", "option1", "value2") { // overwrite value 101 | t.Errorf("AddOption failure: true on second insert") 102 | } 103 | testGet(t, c, "section1", "option1", "value2") // read it back again 104 | 105 | if !c.RemoveOption("section1", "option1") { // remove option/value 106 | t.Errorf("RemoveOption failure: false on first remove") 107 | } 108 | 109 | if c.RemoveOption("section1", "option1") { // remove again 110 | t.Errorf("RemoveOption failure: true on second remove") 111 | } 112 | 113 | _, err = c.GetString("section1", "option1") // read it back again 114 | if err == nil { 115 | t.Errorf("GetString failure: got value for removed section/option") 116 | } 117 | 118 | if !c.RemoveSection("section1") { // remove existing section 119 | t.Errorf("RemoveSection failure: false on first remove") 120 | } 121 | 122 | if c.RemoveSection("section1") { // remove again 123 | t.Errorf("RemoveSection failure: true on second remove") 124 | } 125 | 126 | // test types 127 | if !c.AddSection("section2") { // add section 128 | t.Errorf("AddSection failure: false on first insert") 129 | } 130 | 131 | if !c.AddOption("section2", "test-number", "666") { // add number 132 | t.Errorf("AddOption failure: false on first insert") 133 | } 134 | testGet(t, c, "section2", "test-number", int64(666)) // read it back 135 | 136 | if !c.AddOption("section2", "test-yes", "yes") { // add 'yes' (bool) 137 | t.Errorf("AddOption failure: false on first insert") 138 | } 139 | testGet(t, c, "section2", "test-yes", true) // read it back 140 | 141 | if !c.AddOption("section2", "test-false", "false") { // add 'false' (bool) 142 | t.Errorf("AddOption failure: false on first insert") 143 | } 144 | testGet(t, c, "section2", "test-false", false) // read it back 145 | 146 | // test cycle 147 | c.AddOption(DefaultSection, "opt1", "%(opt2)s") 148 | c.AddOption(DefaultSection, "opt2", "%(opt1)s") 149 | 150 | _, err = c.GetString(DefaultSection, "opt1") 151 | if err == nil { 152 | t.Errorf("GetString failure: no error for cycle") 153 | } else if strings.Index(err.Error(), "cycle") < 0 { 154 | t.Errorf("GetString failure: incorrect error for cycle") 155 | } 156 | } 157 | 158 | // Create a 'tough' configuration file and test (read) parsing. 159 | func TestReadFile(t *testing.T) { 160 | 161 | tmp := os.TempDir() + "/__config_test.go__garbage" 162 | defer os.Remove(tmp) 163 | 164 | file, err := os.Create(tmp) 165 | if err != nil { 166 | t.Fatalf("Test cannot run because cannot write temporary file: " + tmp) 167 | } 168 | 169 | buf := bufio.NewWriter(file) 170 | buf.WriteString("[section-1]\n") 171 | buf.WriteString(" option1=value1 ; This is a comment\n") 172 | buf.WriteString(" option2 : 2#Not a comment\t#Now this is a comment after a TAB\n") 173 | buf.WriteString(" # Let me put another comment\n") 174 | buf.WriteString(" option3= line1\nline2 \n\tline3 # Comment\n") 175 | buf.WriteString("; Another comment\n") 176 | buf.WriteString("[" + DefaultSection + "]\n") 177 | buf.WriteString("variable1=small\n") 178 | buf.WriteString("variable2=a_part_of_a_%(variable1)s_test\n") 179 | buf.WriteString("[secTION-2]\n") 180 | buf.WriteString("IS-flag-TRUE=Yes\n") 181 | buf.WriteString("[section-1]\n") // continue again [section-1] 182 | buf.WriteString("option4=this_is_%(variable2)s.") 183 | buf.Flush() 184 | file.Close() 185 | 186 | c, err := ReadConfigFile(tmp) 187 | if err != nil { 188 | t.Fatalf("ReadConfigFile failure: " + err.Error()) 189 | } 190 | 191 | if len(c.GetSections()) != 3 { // check number of sections 192 | t.Errorf("GetSections failure: wrong number of sections") 193 | } 194 | 195 | opts, err := c.GetOptions("section-1") // check number of options 196 | if len(opts) != 6 { // 4 of [section-1] plus 2 of [default] 197 | t.Errorf("GetOptions failure: wrong number of options") 198 | } 199 | 200 | testGet(t, c, "section-1", "option1", "value1") 201 | testGet(t, c, "section-1", "option2", "2#Not a comment") 202 | testGet(t, c, "section-1", "option3", "line1\nline2\nline3") 203 | testGet(t, c, "section-1", "option4", "this_is_a_part_of_a_small_test.") 204 | testGet(t, c, "SECtion-2", "is-FLAG-true", true) // case-insensitive 205 | } 206 | 207 | // Test writing and reading back a configuration file. 208 | func TestWriteReadFile(t *testing.T) { 209 | tmp := os.TempDir() + "/__config_test.go__garbage" 210 | defer os.Remove(tmp) 211 | 212 | cw := NewConfigFile() 213 | 214 | // write file; will test only read later on 215 | cw.AddSection("First-Section") 216 | cw.AddOption("First-Section", "option1", "value option1") 217 | cw.AddOption("First-Section", "option2", "2") 218 | 219 | cw.AddOption(DefaultSection, "host", "www.example.com") 220 | cw.AddOption(DefaultSection, "protocol", "https://") 221 | cw.AddOption(DefaultSection, "base-url", "%(protocol)s%(host)s") 222 | 223 | cw.AddOption("Another-Section", "useHTTPS", "y") 224 | cw.AddOption("Another-Section", "url", "%(base-url)s/some/path") 225 | 226 | cw.WriteConfigFile(tmp, 0644, "Test file for test-case") 227 | 228 | // read back file and test 229 | cr, err := ReadConfigFile(tmp) 230 | if err != nil { 231 | t.Fatalf("ReadConfigFile failure: " + err.Error()) 232 | } 233 | 234 | testGet(t, cr, "first-section", "option1", "value option1") 235 | testGet(t, cr, "first-section", "option2", int64(2)) 236 | testGet(t, cr, "Another-SECTION", "usehttps", true) 237 | testGet(t, cr, "another-section", "url", "https://www.example.com/some/path") 238 | } 239 | --------------------------------------------------------------------------------