├── LICENSE ├── decode.go ├── decode_test.go ├── fold.go ├── go.mod ├── littype_string.go ├── node.go ├── parser.go ├── parser_test.go ├── readme.md ├── scanner.go ├── source.go ├── spec.md ├── testdata ├── array.conf ├── deprecated.conf ├── duration.conf ├── envvars.conf ├── include.conf ├── label.conf ├── map.conf ├── nogroup.conf ├── server.conf └── utf8.conf ├── token.go └── token_string.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Andrew Pillar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /decode.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | "time" 12 | "unicode/utf8" 13 | ) 14 | 15 | // DecodeError reports an error that occurred during decoding. 16 | type DecodeError struct { 17 | Pos Pos 18 | Param string 19 | Label string 20 | Type reflect.Type 21 | Field string 22 | } 23 | 24 | func (e *DecodeError) Error() string { 25 | param := e.Param 26 | 27 | if e.Label != "" { 28 | param += " " + e.Label 29 | } 30 | return fmt.Sprintf("config: %s - cannot decode %q into field %s of type %s", e.Pos, param, e.Field, e.Type) 31 | } 32 | 33 | var ( 34 | sizb int64 = 1 35 | sizkb int64 = sizb << 10 36 | sizmb int64 = sizkb << 10 37 | sizgb int64 = sizmb << 10 38 | siztb int64 = sizgb << 10 39 | 40 | siztab = map[string]int64{ 41 | "B": sizb, 42 | "KB": sizkb, 43 | "MB": sizmb, 44 | "GB": sizgb, 45 | "TB": siztb, 46 | } 47 | ) 48 | 49 | func (d *Decoder) interpolate(s string) (reflect.Value, error) { 50 | end := len(s) - 1 51 | 52 | interpolate := false 53 | 54 | val := make([]rune, 0, len(s)) 55 | expr := make([]rune, 0, len(s)) 56 | 57 | i := 0 58 | w := 1 59 | 60 | for i <= end { 61 | r := rune(s[i]) 62 | 63 | if r >= utf8.RuneSelf { 64 | r, w = utf8.DecodeRune([]byte(s[i:])) 65 | } 66 | 67 | i += w 68 | 69 | if r == '\\' { 70 | continue 71 | } 72 | 73 | if r == '$' && len(d.expands) > 0 { 74 | if i <= end && s[i] == '{' { 75 | interpolate = true 76 | i++ 77 | continue 78 | } 79 | } 80 | 81 | if r == '}' && interpolate { 82 | sexpr := string(expr) 83 | expand := expandEnvvar 84 | 85 | if i := strings.Index(sexpr, ":"); i > 0 { 86 | fn, ok := d.expands[sexpr[:i]] 87 | 88 | if !ok { 89 | return reflect.ValueOf(nil), errors.New("undefined variable expansion: " + sexpr[:i]) 90 | } 91 | 92 | sexpr = sexpr[i+1:] 93 | expand = fn 94 | } 95 | 96 | interpolate = false 97 | 98 | s, err := expand(sexpr) 99 | 100 | if err != nil { 101 | return reflect.ValueOf(nil), err 102 | } 103 | 104 | val = append(val, []rune(s)...) 105 | continue 106 | } 107 | 108 | if interpolate { 109 | expr = append(expr, r) 110 | continue 111 | } 112 | val = append(val, r) 113 | } 114 | return reflect.ValueOf(string(val)), nil 115 | } 116 | 117 | func (d *Decoder) decodeLiteral(rt reflect.Type, lit *lit) (reflect.Value, error) { 118 | var rv reflect.Value 119 | 120 | switch lit.Type { 121 | case StringLit: 122 | if kind := rt.Kind(); kind != reflect.String { 123 | return rv, lit.Err("cannot use string as " + kind.String()) 124 | } 125 | v, err := d.interpolate(lit.Value) 126 | 127 | if err != nil { 128 | return rv, lit.Err(err.Error()) 129 | } 130 | rv = v 131 | case IntLit: 132 | var bitSize int 133 | 134 | kind := rt.Kind() 135 | 136 | switch kind { 137 | case reflect.Int: 138 | bitSize = 32 139 | case reflect.Int8: 140 | bitSize = 8 141 | case reflect.Int16: 142 | bitSize = 16 143 | case reflect.Int32: 144 | bitSize = 32 145 | case reflect.Int64: 146 | bitSize = 64 147 | default: 148 | return rv, lit.Err("cannot use int as " + kind.String()) 149 | } 150 | 151 | i, _ := strconv.ParseInt(lit.Value, 10, bitSize) 152 | 153 | rv = reflect.ValueOf(i) 154 | case FloatLit: 155 | var bitSize int 156 | 157 | kind := rt.Kind() 158 | 159 | switch kind { 160 | case reflect.Float32: 161 | bitSize = 32 162 | case reflect.Float64: 163 | bitSize = 64 164 | default: 165 | return rv, lit.Err("cannot use float as " + kind.String()) 166 | } 167 | 168 | fl, _ := strconv.ParseFloat(lit.Value, bitSize) 169 | 170 | rv = reflect.ValueOf(fl) 171 | case BoolLit: 172 | if kind := rt.Kind(); kind != reflect.Bool { 173 | return rv, lit.Err("cannot use bool as " + kind.String()) 174 | } 175 | 176 | booltab := map[string]bool{ 177 | "true": true, 178 | "false": false, 179 | } 180 | 181 | rv = reflect.ValueOf(booltab[lit.Value]) 182 | case DurationLit: 183 | if kind := rt.Kind(); kind != reflect.Int64 { 184 | return rv, lit.Err("cannot use duration as " + kind.String()) 185 | } 186 | 187 | dur, err := time.ParseDuration(lit.Value) 188 | 189 | if err != nil { 190 | return rv, lit.Err(err.Error()) 191 | } 192 | rv = reflect.ValueOf(dur) 193 | case SizeLit: 194 | if kind := rt.Kind(); kind != reflect.Int64 { 195 | return rv, lit.Err("cannot use size as " + kind.String()) 196 | } 197 | 198 | end := len(lit.Value) - 1 199 | val := lit.Value[:end] 200 | 201 | unitBytes := make([]byte, 1) 202 | unitBytes[0] = lit.Value[end] 203 | 204 | if b := lit.Value[end-1]; b == 'K' || b == 'M' || b == 'G' || b == 'T' { 205 | val = lit.Value[:len(val)-1] 206 | 207 | unitBytes = append([]byte{b}, unitBytes[0]) 208 | } 209 | 210 | unit := string(unitBytes) 211 | siz, ok := siztab[unit] 212 | 213 | if !ok { 214 | return rv, lit.Err("unrecognized size " + unit) 215 | } 216 | 217 | i, _ := strconv.ParseInt(val, 10, 64) 218 | 219 | rv = reflect.ValueOf(i * siz) 220 | } 221 | return rv, nil 222 | } 223 | 224 | func (d *Decoder) decodeBlock(rt reflect.Type, b *block) (reflect.Value, error) { 225 | var rv reflect.Value 226 | 227 | kind := rt.Kind() 228 | 229 | if kind != reflect.Struct && kind != reflect.Map { 230 | return rv, errors.New("can only decode block into struct or map") 231 | } 232 | 233 | if kind == reflect.Map { 234 | rv = reflect.MakeMap(rt) 235 | 236 | if rt.Key().Kind() != reflect.String { 237 | return rv, errors.New("cannot decode into non-string key") 238 | } 239 | 240 | el := rt.Elem() 241 | 242 | var ( 243 | pv reflect.Value 244 | err error 245 | ) 246 | 247 | for _, p := range b.Params { 248 | switch v := p.Value.(type) { 249 | case *lit: 250 | pv, err = d.decodeLiteral(el, v) 251 | 252 | if err != nil { 253 | return rv, err 254 | } 255 | pv = pv.Convert(el) 256 | case *block: 257 | pv, err = d.decodeBlock(el, v) 258 | case *array: 259 | pv, err = d.decodeArray(el, v) 260 | } 261 | 262 | if err != nil { 263 | return rv, err 264 | } 265 | rv.SetMapIndex(reflect.ValueOf(p.Name.Value), pv) 266 | } 267 | return rv, nil 268 | } 269 | 270 | rv = reflect.New(rt).Elem() 271 | 272 | for _, p := range b.Params { 273 | if err := d.doDecode(rv, p); err != nil { 274 | return rv, err 275 | } 276 | } 277 | return rv, nil 278 | } 279 | 280 | func (d *Decoder) decodeArray(rt reflect.Type, arr *array) (reflect.Value, error) { 281 | var rv reflect.Value 282 | 283 | if kind := rt.Kind(); kind != reflect.Slice { 284 | return rv, arr.Err("cannot use slice as " + kind.String()) 285 | } 286 | 287 | rv = reflect.MakeSlice(rt, 0, len(arr.Items)) 288 | 289 | el := rt.Elem() 290 | 291 | for _, it := range arr.Items { 292 | val := reflect.New(el).Elem() 293 | 294 | switch v := it.(type) { 295 | case *lit: 296 | litrv, err := d.decodeLiteral(el, v) 297 | 298 | if err != nil { 299 | return rv, err 300 | } 301 | val.Set(litrv.Convert(el)) 302 | case *block: 303 | blockrv, err := d.decodeBlock(el, v) 304 | 305 | if err != nil { 306 | return rv, err 307 | } 308 | val.Set(blockrv) 309 | case *array: 310 | arrrv, err := d.decodeArray(el, v) 311 | 312 | if err != nil { 313 | return rv, err 314 | } 315 | val.Set(arrrv) 316 | } 317 | rv = reflect.Append(rv, val) 318 | } 319 | return rv, nil 320 | } 321 | 322 | type field struct { 323 | name string 324 | val reflect.Value 325 | fold func(s, t []byte) bool 326 | deprecated bool 327 | altname string // alternative field name if deprecated 328 | nogroup bool 329 | } 330 | 331 | type fields struct { 332 | arr []*field 333 | tab map[string]int 334 | } 335 | 336 | func (f *fields) get(name string) (*field, bool) { 337 | i, ok := f.tab[name] 338 | 339 | if ok { 340 | return f.arr[i], true 341 | } 342 | return nil, false 343 | } 344 | 345 | // Stderrh provides an implementation for the errh function that will write 346 | // each error to standard error. This is the default error handler used by the 347 | // decoder if none if otherwise configured. 348 | var Stderrh = func(pos Pos, msg string) { 349 | fmt.Fprintf(os.Stderr, "%s - %s\n", pos, msg) 350 | } 351 | 352 | // Option is a callback that is used to modify the behaviour of a Decoder. 353 | type Option func(d *Decoder) *Decoder 354 | 355 | // DefaultOptions is a slice of all the options that can be used when modifying 356 | // the behaviour of a Decoder. 357 | var DefaultOptions = []Option{ 358 | Includes, 359 | Envvars, 360 | ErrorHandler(Stderrh), 361 | } 362 | 363 | // Includes enables the inclusion of additional configuration files via the 364 | // include keyword. The value for an include must be either a string literal, 365 | // or an array of string literals. 366 | func Includes(d *Decoder) *Decoder { 367 | d.includes = true 368 | return d 369 | } 370 | 371 | func expandEnvvar(key string) (string, error) { 372 | return os.Getenv(key), nil 373 | } 374 | 375 | // Envvars enables the expansion of environment variables in configuration. 376 | // Environment variables are specified like so ${VARIABLE}. 377 | func Envvars(d *Decoder) *Decoder { 378 | return Expand("env", expandEnvvar)(d) 379 | } 380 | 381 | // ErrorHandler configures the error handler used during parsing of a 382 | // configuration file. 383 | func ErrorHandler(errh func(Pos, string)) Option { 384 | return func(d *Decoder) *Decoder { 385 | d.errh = errh 386 | return d 387 | } 388 | } 389 | 390 | type ExpandFunc func(key string) (string, error) 391 | 392 | // Expand registers an expansion mechanism for expanding a variable in a 393 | // string value for the given prefix, for example ${env:PASSWORD}. 394 | func Expand(prefix string, fn ExpandFunc) Option { 395 | return func(d *Decoder) *Decoder { 396 | if d.expands == nil { 397 | d.expands = make(map[string]ExpandFunc) 398 | } 399 | d.expands[prefix] = fn 400 | return d 401 | } 402 | } 403 | 404 | type Decoder struct { 405 | fields *fields 406 | 407 | name string 408 | 409 | includes bool 410 | expands map[string]ExpandFunc 411 | errh func(Pos, string) 412 | } 413 | 414 | // NewDecoder returns a new decoder configured with the given options. 415 | func NewDecoder(name string, opts ...Option) *Decoder { 416 | d := &Decoder{ 417 | name: name, 418 | errh: Stderrh, 419 | } 420 | 421 | for _, opt := range opts { 422 | d = opt(d) 423 | } 424 | return d 425 | } 426 | 427 | // DecodeFile decodes the file into the given interface. 428 | func DecodeFile(v interface{}, name string, opts ...Option) error { 429 | d := NewDecoder(name, opts...) 430 | 431 | f, err := os.Open(name) 432 | 433 | if err != nil { 434 | return err 435 | } 436 | 437 | defer f.Close() 438 | 439 | return d.Decode(v, f) 440 | } 441 | 442 | // Decode decodes the contents of the given reader into the given interface. 443 | func (d *Decoder) Decode(v interface{}, r io.Reader) error { 444 | rv := reflect.ValueOf(v) 445 | 446 | if kind := rv.Kind(); kind != reflect.Ptr || rv.IsNil() { 447 | return errors.New("cannot decode into " + kind.String()) 448 | } 449 | 450 | p := parser{ 451 | scanner: newScanner(newSource(d.name, r, d.errh)), 452 | includes: d.includes, 453 | inctab: make(map[string]string), 454 | } 455 | 456 | nn, err := p.parse() 457 | 458 | if err != nil { 459 | return err 460 | } 461 | 462 | el := rv.Elem() 463 | 464 | for _, n := range nn { 465 | param, ok := n.(*param) 466 | 467 | if !ok { 468 | panic("could not type assert to *Param") 469 | } 470 | 471 | if err := d.doDecode(el, param); err != nil { 472 | return err 473 | } 474 | } 475 | return nil 476 | } 477 | 478 | func (d *Decoder) loadFields(rv reflect.Value) { 479 | d.fields = &fields{ 480 | arr: make([]*field, 0), 481 | tab: make(map[string]int), 482 | } 483 | 484 | t := rv.Type() 485 | 486 | for i := 0; i < rv.NumField(); i++ { 487 | var ( 488 | deprecated bool 489 | altname string 490 | 491 | nogroup bool 492 | ) 493 | 494 | sf := t.Field(i) 495 | 496 | name := sf.Name 497 | 498 | if tag := sf.Tag.Get("config"); tag != "" { 499 | parts := strings.Split(tag, ",") 500 | 501 | name = parts[0] 502 | 503 | if name == "" { 504 | name = sf.Name 505 | } 506 | 507 | if len(parts) > 1 { 508 | for _, part := range parts[1:] { 509 | if strings.HasPrefix(part, "deprecated") { 510 | deprecated = true 511 | 512 | if i := strings.Index(part, ":"); i > 0 { 513 | altname = part[i+1:] 514 | } 515 | continue 516 | } 517 | 518 | if part == "nogroup" { 519 | nogroup = true 520 | } 521 | } 522 | } 523 | } 524 | 525 | if name == "-" { 526 | continue 527 | } 528 | 529 | d.fields.arr = append(d.fields.arr, &field{ 530 | name: name, 531 | val: rv.Field(i), 532 | fold: foldFunc([]byte(name)), 533 | deprecated: deprecated, 534 | altname: altname, 535 | nogroup: nogroup, 536 | }) 537 | d.fields.tab[name] = i 538 | } 539 | } 540 | 541 | func (d *Decoder) doDecode(rv reflect.Value, p *param) error { 542 | d.loadFields(rv) 543 | 544 | f, ok := d.fields.get(p.Name.Value) 545 | 546 | if !ok { 547 | // Lazily search across all fields using the fold function for case 548 | // comparison. 549 | for _, fld := range d.fields.arr { 550 | if fld.fold([]byte(fld.name), []byte(p.Name.Value)) { 551 | f = fld 552 | break 553 | } 554 | } 555 | } 556 | 557 | if f == nil { 558 | return nil 559 | } 560 | 561 | if f.deprecated { 562 | msg := p.Name.Value + " is deprecated" 563 | 564 | if f.altname != "" { 565 | msg += " use " + f.altname + " instead" 566 | } 567 | d.errh(p.Pos(), msg) 568 | } 569 | 570 | el := f.val.Type() 571 | 572 | if p.Label != nil { 573 | // We don't want to group the parameter under a label, so make sure 574 | // we're decoding into a struct, whereby the label would map to the 575 | // struct field. 576 | if f.nogroup { 577 | if f.val.Kind() != reflect.Struct { 578 | return &DecodeError{ 579 | Pos: p.Pos(), 580 | Param: p.Name.Value, 581 | Label: p.Label.Value, 582 | Type: el, 583 | Field: f.name, 584 | } 585 | } 586 | 587 | return d.doDecode(f.val, ¶m{ 588 | baseNode: p.baseNode, 589 | Name: p.Label, 590 | Value: p.Value, 591 | }) 592 | } 593 | 594 | if f.val.Kind() != reflect.Map { 595 | return &DecodeError{ 596 | Pos: p.Pos(), 597 | Param: p.Name.Value, 598 | Label: p.Label.Value, 599 | Type: el, 600 | Field: f.name, 601 | } 602 | } 603 | 604 | t := f.val.Type() 605 | el = t.Elem() 606 | 607 | if f.val.IsNil() { 608 | f.val.Set(reflect.MakeMap(t)) 609 | } 610 | } 611 | 612 | var ( 613 | pv reflect.Value 614 | err error 615 | ) 616 | 617 | switch v := p.Value.(type) { 618 | case *lit: 619 | pv, err = d.decodeLiteral(el, v) 620 | 621 | if err != nil { 622 | return &DecodeError{ 623 | Pos: p.Pos(), 624 | Param: p.Name.Value, 625 | Type: el, 626 | Field: f.name, 627 | } 628 | } 629 | pv = pv.Convert(el) 630 | case *block: 631 | pv, err = d.decodeBlock(el, v) 632 | case *array: 633 | pv, err = d.decodeArray(el, v) 634 | } 635 | 636 | if err != nil { 637 | return &DecodeError{ 638 | Pos: p.Pos(), 639 | Param: p.Name.Value, 640 | Type: el, 641 | Field: f.name, 642 | } 643 | } 644 | 645 | if p.Label != nil { 646 | f.val.SetMapIndex(reflect.ValueOf(p.Label.Value), pv) 647 | return nil 648 | } 649 | 650 | f.val.Set(pv) 651 | return nil 652 | } 653 | -------------------------------------------------------------------------------- /decode_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "reflect" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func Test_DecodeSimpleConfig(t *testing.T) { 12 | var cfg struct { 13 | Log map[string]string 14 | 15 | Net struct { 16 | Listen string 17 | 18 | TLS struct { 19 | Cert string 20 | Key string 21 | } 22 | } 23 | 24 | Drivers []string 25 | 26 | Cache struct { 27 | Redis struct { 28 | Addr string 29 | } 30 | 31 | CleanupInterval time.Duration `config:"cleanup_interval"` 32 | } 33 | 34 | Store map[string]struct { 35 | Type string 36 | Path string 37 | Limit int64 38 | } 39 | } 40 | 41 | if err := DecodeFile(&cfg, filepath.Join("testdata", "server.conf"), ErrorHandler(errh(t))); err != nil { 42 | t.Fatal(err) 43 | } 44 | } 45 | 46 | func Test_DecodeArrays(t *testing.T) { 47 | type Block struct { 48 | String string 49 | } 50 | 51 | var cfg struct { 52 | Strings []string 53 | Ints []int64 54 | Floats []float64 55 | Bools []bool 56 | Durations []time.Duration 57 | Sizes []int64 58 | Blocks []Block 59 | } 60 | 61 | if err := DecodeFile(&cfg, filepath.Join("testdata", "array.conf"), ErrorHandler(errh(t))); err != nil { 62 | t.Fatal(err) 63 | } 64 | 65 | Strings := []string{"one", "two", "three", "four", `"five"`} 66 | Ints := []int64{1, 2, 3, 4} 67 | Floats := []float64{1.2, 3.4, 5.6, 7.8} 68 | Bools := []bool{true, false} 69 | Durations := []time.Duration{time.Second, time.Minute * 2, time.Hour * 3} 70 | Sizes := []int64{1, 2048, 3145728, 4294967296, 5497558138880} 71 | Blocks := []Block{{"foo"}, {"bar"}, {"baz"}} 72 | 73 | for i, str := range Strings { 74 | if cfg.Strings[i] != str { 75 | t.Errorf("Strings[%d] - unexpected string, expected=%q, got=%q\n", i, str, cfg.Strings[i]) 76 | } 77 | } 78 | 79 | for i, i64 := range Ints { 80 | if cfg.Ints[i] != i64 { 81 | t.Errorf("Ints[%d] - unexpected int64, expected=%d, got=%d\n", i, i64, cfg.Ints[i]) 82 | } 83 | } 84 | 85 | for i, f64 := range Floats { 86 | if cfg.Floats[i] != f64 { 87 | t.Errorf("Floats[%d] - unexpected float64, expected=%f, got=%f\n", i, f64, cfg.Floats[i]) 88 | } 89 | } 90 | 91 | for i, b := range Bools { 92 | if cfg.Bools[i] != b { 93 | t.Errorf("Bools[%d] - unexpected bool, expected=%v, got=%v\n", i, b, cfg.Bools[i]) 94 | } 95 | } 96 | 97 | for i, dur := range Durations { 98 | if cfg.Durations[i] != dur { 99 | t.Errorf("Durations[%d] - unexpected time.Duration, expected=%v, got=%v\n", i, dur, cfg.Durations[i]) 100 | } 101 | } 102 | 103 | for i, siz := range Sizes { 104 | if cfg.Sizes[i] != siz { 105 | t.Errorf("Sizes[%d] - unexpected int64, expected=%d, got=%d\n", i, siz, cfg.Sizes[i]) 106 | } 107 | } 108 | 109 | for i, block := range Blocks { 110 | if cfg.Blocks[i].String != block.String { 111 | t.Errorf("Blocks[%d] - unexpected string, expected=%q, got=%q\n", i, block.String, cfg.Blocks[i].String) 112 | } 113 | } 114 | } 115 | 116 | func Test_DecodeNoGroupLabel(t *testing.T) { 117 | var cfg struct { 118 | Driver struct { 119 | SSH struct { 120 | Addr string 121 | 122 | Auth struct { 123 | Username string 124 | Identity string 125 | } 126 | } 127 | 128 | Docker struct { 129 | Host string 130 | Version string 131 | } 132 | 133 | QEMU struct { 134 | Disks string 135 | CPUs int64 136 | Memory int64 137 | } 138 | } `config:",nogroup"` 139 | } 140 | 141 | if err := DecodeFile(&cfg, filepath.Join("testdata", "nogroup.conf"), ErrorHandler(errh(t))); err != nil { 142 | t.Fatal(err) 143 | } 144 | t.Log(cfg.Driver) 145 | } 146 | 147 | func Test_DecodeLabel(t *testing.T) { 148 | type TLS struct { 149 | CA string 150 | } 151 | 152 | type Auth struct { 153 | Addr string 154 | 155 | TLS TLS 156 | } 157 | 158 | var cfg struct { 159 | Auth map[string]Auth 160 | 161 | Ports map[string][]string 162 | 163 | Provider map[string]struct { 164 | ClientID string `config:"client_id"` 165 | ClientSecret string `config:"client_secret"` 166 | } 167 | } 168 | 169 | if err := DecodeFile(&cfg, filepath.Join("testdata", "label.conf"), ErrorHandler(errh(t))); err != nil { 170 | t.Fatal(err) 171 | } 172 | 173 | expectedAuth := map[string]Auth{ 174 | "internal": { 175 | Addr: "postgres://localhost:5432", 176 | TLS: TLS{}, 177 | }, 178 | "ldap": { 179 | Addr: "ldap://example.com", 180 | TLS: TLS{CA: "/var/lib/ssl/ca.crt"}, 181 | }, 182 | "saml": { 183 | Addr: "https://idp.example.com", 184 | TLS: TLS{CA: "/var/lib/ssl/ca.crt"}, 185 | }, 186 | } 187 | 188 | for label, auth := range expectedAuth { 189 | cfg, ok := cfg.Auth[label] 190 | 191 | if !ok { 192 | t.Fatalf("could not find label %q\n", label) 193 | } 194 | 195 | if cfg.Addr != auth.Addr { 196 | t.Fatalf("unexpected Addr, expected=%q, got=%q\n", cfg.Addr, auth.Addr) 197 | } 198 | 199 | if cfg.TLS.CA != auth.TLS.CA { 200 | t.Fatalf("unexpected TLS.CA, expected=%q, got=%q\n", cfg.TLS.CA, auth.TLS.CA) 201 | } 202 | } 203 | 204 | expectedPorts := map[string][]string{ 205 | "open": {"8080", "8443"}, 206 | "close": {"80", "443"}, 207 | } 208 | 209 | for label, ports := range expectedPorts { 210 | cfg, ok := cfg.Ports[label] 211 | 212 | if !ok { 213 | t.Fatalf("could not find label %q\n", label) 214 | } 215 | 216 | for i := range cfg { 217 | if ports[i] != cfg[i] { 218 | t.Fatalf("unxepected ports[%d], expected=%q, got=%q\n", i, ports[i], cfg[i]) 219 | } 220 | } 221 | } 222 | 223 | expectedProviders := []string{"github", "gitlab"} 224 | 225 | for _, name := range expectedProviders { 226 | if _, ok := cfg.Provider[name]; !ok { 227 | t.Fatalf("expected provider %q in map\n", name) 228 | } 229 | } 230 | } 231 | 232 | func Test_DecodeUTF8(t *testing.T) { 233 | var cfg struct { 234 | Block map[string]struct { 235 | Strings []string 236 | } 237 | } 238 | 239 | if err := DecodeFile(&cfg, filepath.Join("testdata", "utf8.conf"), ErrorHandler(errh(t))); err != nil { 240 | t.Fatal(err) 241 | } 242 | 243 | label := "标签" 244 | 245 | block, ok := cfg.Block[label] 246 | 247 | if !ok { 248 | t.Fatalf("could not find label %q\n", label) 249 | } 250 | 251 | expected := "细绳" 252 | 253 | for i, s := range block.Strings { 254 | if s != expected { 255 | t.Fatalf("cfg.Block[%q].Strings[%d] - unexpected string, expected=%q, got=%q\n", label, i, expected, s) 256 | } 257 | } 258 | } 259 | 260 | func Test_DecodeDuration(t *testing.T) { 261 | var cfg struct { 262 | Hour time.Duration 263 | HourHalf time.Duration `config:"hour_half"` 264 | HourHalfSeconds time.Duration `config:"hour_half_seconds"` 265 | } 266 | 267 | if err := DecodeFile(&cfg, filepath.Join("testdata", "duration.conf"), ErrorHandler(errh(t))); err != nil { 268 | t.Fatal(err) 269 | } 270 | } 271 | 272 | func Test_DecodeInclude(t *testing.T) { 273 | var cfg struct { 274 | Block map[string]struct { 275 | Strings []string 276 | } 277 | 278 | Hour time.Duration 279 | HourHalf time.Duration `config:"hour_half"` 280 | HourHalfSeconds time.Duration `config:"hour_half_seconds"` 281 | } 282 | 283 | opts := []Option{ 284 | ErrorHandler(errh(t)), 285 | Includes, 286 | } 287 | 288 | if err := DecodeFile(&cfg, filepath.Join("testdata", "include.conf"), opts...); err != nil { 289 | t.Fatal(err) 290 | } 291 | t.Log(cfg) 292 | } 293 | 294 | func Test_DecodeEnvVars(t *testing.T) { 295 | var cfg struct { 296 | Database struct { 297 | Addr string 298 | Username string 299 | Password string 300 | 301 | TLS struct { 302 | KeyPassword string 303 | } 304 | } 305 | } 306 | 307 | os.Setenv("DB_USERNAME", "admin") 308 | os.Setenv("DB_PASSWORD", "secret") 309 | 310 | opts := []Option{ 311 | ErrorHandler(errh(t)), 312 | Envvars, 313 | Expand("vault", func(key string) (string, error) { 314 | m := map[string]string{ 315 | "/secrets/ssl/TLS_KEY_PASSWORD": "terces", 316 | } 317 | 318 | return m[key], nil 319 | }), 320 | } 321 | 322 | if err := DecodeFile(&cfg, filepath.Join("testdata", "envvars.conf"), opts...); err != nil { 323 | t.Fatal(err) 324 | } 325 | 326 | if cfg.Database.Username != "admin" { 327 | t.Fatalf("unexpected Database.Username, expected=%q, got=%q\n", "admin", cfg.Database.Username) 328 | } 329 | if cfg.Database.Password != "secret" { 330 | t.Fatalf("unexpected Database.Password, expected=%q, got=%q\n", "secret", cfg.Database.Password) 331 | } 332 | if cfg.Database.TLS.KeyPassword != "terces" { 333 | t.Fatalf("unexpected Database.TLS.Keypassword, expected=%q, got=%q\n", "terces", cfg.Database.Password) 334 | } 335 | } 336 | 337 | func Test_DecodeDeprecated(t *testing.T) { 338 | var cfg struct { 339 | TLS struct { 340 | CA string 341 | } 342 | 343 | SSL struct { 344 | CA string 345 | } `config:",deprecated:tls"` 346 | } 347 | 348 | errs := make([]string, 0, 1) 349 | 350 | errh := func(pos Pos, msg string) { 351 | errs = append(errs, msg) 352 | } 353 | 354 | if err := DecodeFile(&cfg, filepath.Join("testdata", "deprecated.conf"), ErrorHandler(errh)); err != nil { 355 | t.Fatal(err) 356 | } 357 | 358 | if errs[0] != "ssl is deprecated use tls instead" { 359 | t.Fatalf("could not find deprecated message") 360 | } 361 | } 362 | 363 | func Test_DecodeMap(t *testing.T) { 364 | type labelCfg struct { 365 | Labels map[string]map[string][]string 366 | } 367 | 368 | var cfg labelCfg 369 | 370 | if err := DecodeFile(&cfg, filepath.Join("testdata", "map.conf"), ErrorHandler(errh(t))); err != nil { 371 | t.Fatal(err) 372 | } 373 | 374 | expected := labelCfg{ 375 | Labels: map[string]map[string][]string{ 376 | "qemu": { 377 | "arch": {"x86_64", "aarch64"}, 378 | "os": {"debian", "alpine"}, 379 | }, 380 | "docker": { 381 | "programming": {"go", "js", "python"}, 382 | }, 383 | }, 384 | } 385 | 386 | if !reflect.DeepEqual(cfg, expected) { 387 | t.Fatalf("decoded configuration does not match\n\texpected =%v\n\tgot = %v\n", expected, cfg) 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /fold.go: -------------------------------------------------------------------------------- 1 | // Code taken from encoding/json/fold.go of the Go stdlib. 2 | // Copyright (c) 2009 The Go Authors. All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are 6 | // met: 7 | // 8 | // * Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer. 10 | // * Redistributions in binary form must reproduce the above 11 | // copyright notice, this list of conditions and the following disclaimer 12 | // in the documentation and/or other materials provided with the 13 | // distribution. 14 | // * Neither the name of Google Inc. nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package config 31 | 32 | import ( 33 | "bytes" 34 | "unicode/utf8" 35 | ) 36 | 37 | const ( 38 | caseMask = ^byte(0x20) // Mask to ignore case in ASCII. 39 | kelvin = '\u212a' 40 | smallLongEss = '\u017f' 41 | ) 42 | 43 | // foldFunc returns one of four different case folding equivalence 44 | // functions, from most general (and slow) to fastest: 45 | // 46 | // 1) bytes.EqualFold, if the key s contains any non-ASCII UTF-8 47 | // 2) equalFoldRight, if s contains special folding ASCII ('k', 'K', 's', 'S') 48 | // 3) asciiEqualFold, no special, but includes non-letters (including _) 49 | // 4) simpleLetterEqualFold, no specials, no non-letters. 50 | // 51 | // The letters S and K are special because they map to 3 runes, not just 2: 52 | // - S maps to s and to U+017F 'ſ' Latin small letter long s 53 | // - k maps to K and to U+212A 'K' Kelvin sign 54 | // 55 | // See https://play.golang.org/p/tTxjOc0OGo 56 | // 57 | // The returned function is specialized for matching against s and 58 | // should only be given s. It's not curried for performance reasons. 59 | func foldFunc(s []byte) func(s, t []byte) bool { 60 | nonLetter := false 61 | special := false // special letter 62 | for _, b := range s { 63 | if b >= utf8.RuneSelf { 64 | return bytes.EqualFold 65 | } 66 | upper := b & caseMask 67 | if upper < 'A' || upper > 'Z' { 68 | nonLetter = true 69 | } else if upper == 'K' || upper == 'S' { 70 | // See above for why these letters are special. 71 | special = true 72 | } 73 | } 74 | if special { 75 | return equalFoldRight 76 | } 77 | if nonLetter { 78 | return asciiEqualFold 79 | } 80 | return simpleLetterEqualFold 81 | } 82 | 83 | // equalFoldRight is a specialization of bytes.EqualFold when s is 84 | // known to be all ASCII (including punctuation), but contains an 's', 85 | // 'S', 'k', or 'K', requiring a Unicode fold on the bytes in t. 86 | // See comments on foldFunc. 87 | func equalFoldRight(s, t []byte) bool { 88 | for _, sb := range s { 89 | if len(t) == 0 { 90 | return false 91 | } 92 | tb := t[0] 93 | if tb < utf8.RuneSelf { 94 | if sb != tb { 95 | sbUpper := sb & caseMask 96 | if 'A' <= sbUpper && sbUpper <= 'Z' { 97 | if sbUpper != tb&caseMask { 98 | return false 99 | } 100 | } else { 101 | return false 102 | } 103 | } 104 | t = t[1:] 105 | continue 106 | } 107 | // sb is ASCII and t is not. t must be either kelvin 108 | // sign or long s; sb must be s, S, k, or K. 109 | tr, size := utf8.DecodeRune(t) 110 | switch sb { 111 | case 's', 'S': 112 | if tr != smallLongEss { 113 | return false 114 | } 115 | case 'k', 'K': 116 | if tr != kelvin { 117 | return false 118 | } 119 | default: 120 | return false 121 | } 122 | t = t[size:] 123 | 124 | } 125 | if len(t) > 0 { 126 | return false 127 | } 128 | return true 129 | } 130 | 131 | // asciiEqualFold is a specialization of bytes.EqualFold for use when 132 | // s is all ASCII (but may contain non-letters) and contains no 133 | // special-folding letters. 134 | // See comments on foldFunc. 135 | func asciiEqualFold(s, t []byte) bool { 136 | if len(s) != len(t) { 137 | return false 138 | } 139 | for i, sb := range s { 140 | tb := t[i] 141 | if sb == tb { 142 | continue 143 | } 144 | if ('a' <= sb && sb <= 'z') || ('A' <= sb && sb <= 'Z') { 145 | if sb&caseMask != tb&caseMask { 146 | return false 147 | } 148 | } else { 149 | return false 150 | } 151 | } 152 | return true 153 | } 154 | 155 | // simpleLetterEqualFold is a specialization of bytes.EqualFold for 156 | // use when s is all ASCII letters (no underscores, etc) and also 157 | // doesn't contain 'k', 'K', 's', or 'S'. 158 | // See comments on foldFunc. 159 | func simpleLetterEqualFold(s, t []byte) bool { 160 | if len(s) != len(t) { 161 | return false 162 | } 163 | for i, b := range s { 164 | if b&caseMask != t[i]&caseMask { 165 | return false 166 | } 167 | } 168 | return true 169 | } 170 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/andrewpillar/config 2 | -------------------------------------------------------------------------------- /littype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type LitType -linecomment"; DO NOT EDIT. 2 | 3 | package config 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[StringLit-1] 12 | _ = x[IntLit-2] 13 | _ = x[FloatLit-3] 14 | _ = x[BoolLit-4] 15 | _ = x[DurationLit-5] 16 | _ = x[SizeLit-6] 17 | } 18 | 19 | const _LitType_name = "stringintfloatbooldurationsize" 20 | 21 | var _LitType_index = [...]uint8{0, 6, 9, 14, 18, 26, 30} 22 | 23 | func (i LitType) String() string { 24 | i -= 1 25 | if i >= LitType(len(_LitType_index)-1) { 26 | return "LitType(" + strconv.FormatInt(int64(i+1), 10) + ")" 27 | } 28 | return _LitType_name[_LitType_index[i]:_LitType_index[i+1]] 29 | } 30 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type node interface { 4 | Pos() Pos 5 | 6 | Err(msg string) error 7 | } 8 | 9 | type baseNode struct { 10 | pos Pos 11 | } 12 | 13 | func (n baseNode) Pos() Pos { 14 | return n.pos 15 | } 16 | 17 | func (n baseNode) Err(msg string) error { 18 | return n.pos.Err(msg) 19 | } 20 | 21 | type name struct { 22 | baseNode 23 | 24 | Value string 25 | } 26 | 27 | type lit struct { 28 | baseNode 29 | 30 | Value string 31 | Type LitType 32 | } 33 | 34 | type param struct { 35 | baseNode 36 | 37 | Name *name 38 | Label *name 39 | Value node 40 | } 41 | 42 | type block struct { 43 | baseNode 44 | 45 | Params []*param 46 | } 47 | 48 | type array struct { 49 | baseNode 50 | 51 | Items []node 52 | } 53 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | type parser struct { 9 | *scanner 10 | 11 | errc int 12 | includes bool 13 | inctab map[string]string 14 | } 15 | 16 | func (p *parser) errAt(pos Pos, msg string) { 17 | p.errc++ 18 | p.scanner.source.errh(pos, msg) 19 | } 20 | 21 | func (p *parser) err(msg string) { 22 | p.errAt(p.pos, msg) 23 | } 24 | 25 | func (p *parser) expected(tok token) { 26 | p.err("expected " + tok.String()) 27 | } 28 | 29 | func (p *parser) unexpected(tok token) { 30 | p.err("unexpected " + tok.String()) 31 | } 32 | 33 | func (p *parser) got(tok token) bool { 34 | if p.tok == tok { 35 | p.next() 36 | return true 37 | } 38 | return false 39 | } 40 | 41 | func (p *parser) want(tok token) { 42 | if !p.got(tok) { 43 | p.expected(tok) 44 | } 45 | } 46 | 47 | func (p *parser) advance(follow ...token) { 48 | set := make(map[token]struct{}) 49 | 50 | for _, tok := range follow { 51 | set[tok] = struct{}{} 52 | } 53 | set[_EOF] = struct{}{} 54 | 55 | for { 56 | if _, ok := set[p.tok]; ok { 57 | break 58 | } 59 | p.next() 60 | } 61 | } 62 | 63 | func (p *parser) literal() *lit { 64 | if p.tok != _Literal { 65 | return nil 66 | } 67 | 68 | n := &lit{ 69 | baseNode: p.node(), 70 | Type: p.typ, 71 | Value: p.lit, 72 | } 73 | p.next() 74 | return n 75 | } 76 | 77 | func (p *parser) list(sep, end token, parse func()) { 78 | for p.tok != end && p.tok != _EOF { 79 | parse() 80 | 81 | if !p.got(sep) && p.tok != end { 82 | p.err("expected " + sep.String() + " or " + end.String()) 83 | p.next() 84 | } 85 | } 86 | p.want(end) 87 | } 88 | 89 | func (p *parser) block() *block { 90 | p.want(_Lbrace) 91 | 92 | n := &block{ 93 | baseNode: p.node(), 94 | } 95 | 96 | p.list(_Semi, _Rbrace, func() { 97 | if p.tok != _Name { 98 | p.expected(_Name) 99 | p.advance(_Rbrace, _Semi) 100 | return 101 | } 102 | n.Params = append(n.Params, p.param()) 103 | }) 104 | return n 105 | } 106 | 107 | func (p *parser) arr() *array { 108 | p.want(_Lbrack) 109 | 110 | n := &array{ 111 | baseNode: p.node(), 112 | } 113 | 114 | p.list(_Comma, _Rbrack, func() { 115 | n.Items = append(n.Items, p.operand()) 116 | }) 117 | return n 118 | } 119 | 120 | func (p *parser) operand() node { 121 | var n node 122 | 123 | switch p.tok { 124 | case _Literal: 125 | n = p.literal() 126 | case _Lbrace: 127 | n = p.block() 128 | case _Lbrack: 129 | n = p.arr() 130 | case _Name: 131 | name := p.name() 132 | 133 | if name.Value != "true" && name.Value != "false" { 134 | p.unexpected(_Name) 135 | p.advance(_Semi) 136 | break 137 | } 138 | 139 | n = &lit{ 140 | baseNode: name.baseNode, 141 | Type: BoolLit, 142 | Value: name.Value, 143 | } 144 | default: 145 | p.unexpected(p.tok) 146 | p.advance(_Semi) 147 | } 148 | return n 149 | } 150 | 151 | func (p *parser) node() baseNode { 152 | return baseNode{ 153 | pos: p.pos, 154 | } 155 | } 156 | 157 | func (p *parser) name() *name { 158 | if p.tok != _Name { 159 | return nil 160 | } 161 | 162 | n := &name{ 163 | baseNode: p.node(), 164 | Value: p.lit, 165 | } 166 | 167 | p.next() 168 | return n 169 | } 170 | 171 | func (p *parser) param() *param { 172 | if p.tok != _Name { 173 | p.unexpected(p.tok) 174 | p.advance(_Semi) 175 | return nil 176 | } 177 | 178 | n := ¶m{ 179 | baseNode: p.node(), 180 | Name: p.name(), 181 | } 182 | 183 | if p.tok == _Name { 184 | n.Label = p.name() 185 | 186 | if p.tok == _Semi { 187 | if n.Label.Value == "true" || n.Label.Value == "false" { 188 | n.Value = &lit{ 189 | baseNode: n.Label.baseNode, 190 | Type: BoolLit, 191 | Value: n.Label.Value, 192 | } 193 | n.Label = nil 194 | } 195 | return n 196 | } 197 | } 198 | 199 | n.Value = p.operand() 200 | 201 | return n 202 | } 203 | 204 | func (p *parser) include() []node { 205 | files := make([]string, 0) 206 | 207 | switch p.tok { 208 | case _Literal: 209 | if p.typ != StringLit { 210 | p.err("unexpected " + p.typ.String()) 211 | return nil 212 | } 213 | 214 | files = append(files, p.lit) 215 | p.literal() 216 | case _Lbrack: 217 | arr := p.arr() 218 | 219 | for _, it := range arr.Items { 220 | lit, ok := it.(*lit) 221 | 222 | if !ok { 223 | p.err("expected string literal in include array") 224 | break 225 | } 226 | 227 | if lit.Type != StringLit { 228 | p.err("expected string literal in include array") 229 | break 230 | } 231 | files = append(files, lit.Value) 232 | } 233 | default: 234 | p.unexpected(p.tok) 235 | return nil 236 | } 237 | 238 | nn := make([]node, 0) 239 | 240 | for _, file := range files { 241 | if file == p.scanner.name { 242 | p.err("cannot include self") 243 | break 244 | } 245 | 246 | if source, ok := p.inctab[file]; ok { 247 | p.err("already included from " + source) 248 | break 249 | } 250 | 251 | p.inctab[file] = p.scanner.name 252 | 253 | err := func(file string) error { 254 | f, err := os.Open(file) 255 | 256 | if err != nil { 257 | return err 258 | } 259 | 260 | defer f.Close() 261 | 262 | p := parser{ 263 | scanner: newScanner(newSource(f.Name(), f, p.errh)), 264 | includes: p.includes, 265 | inctab: p.inctab, 266 | } 267 | 268 | inc, err := p.parse() 269 | 270 | if err != nil { 271 | return err 272 | } 273 | 274 | nn = append(nn, inc...) 275 | return nil 276 | }(file) 277 | 278 | if err != nil { 279 | p.err(err.Error()) 280 | break 281 | } 282 | } 283 | return nn 284 | } 285 | 286 | func (p *parser) parse() ([]node, error) { 287 | nn := make([]node, 0) 288 | 289 | for p.tok != _EOF { 290 | if p.tok == _Semi { 291 | p.next() 292 | continue 293 | } 294 | 295 | if p.includes { 296 | if p.tok == _Name { 297 | if p.lit == "include" { 298 | p.next() 299 | nn = append(nn, p.include()...) 300 | continue 301 | } 302 | } 303 | } 304 | nn = append(nn, p.param()) 305 | } 306 | 307 | if p.errc > 0 { 308 | return nil, fmt.Errorf("parser encountered %d error(s)", p.errc) 309 | } 310 | return nn, nil 311 | } 312 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | func errh(t *testing.T) func(Pos, string) { 10 | return func(pos Pos, msg string) { 11 | t.Errorf("%s - %s\n", pos, msg) 12 | } 13 | } 14 | 15 | func checkname(t *testing.T, expected, actual *name) { 16 | if expected.Value != actual.Value { 17 | t.Errorf("%s - unexpected name.Value, expected=%q, got=%q\n", actual.Pos(), expected.Value, actual.Value) 18 | } 19 | } 20 | 21 | func checklit(t *testing.T, expected, actual *lit) { 22 | if expected.Value != actual.Value { 23 | t.Errorf("%s - unexpected lit.Value, expected=%q, got=%q\n", actual.Pos(), expected.Value, actual.Value) 24 | } 25 | 26 | if expected.Type != actual.Type { 27 | t.Errorf("%s - unexpected lit.Type, expected=%q, got=%q\n", actual.Pos(), expected.Type, actual.Type) 28 | } 29 | } 30 | 31 | func checkparam(t *testing.T, expected, actual *param) { 32 | checkname(t, expected.Name, actual.Name) 33 | 34 | if expected.Label != nil { 35 | if actual.Label == nil { 36 | t.Errorf("%s - expected param.Label to be non-nil\n", actual.Pos()) 37 | return 38 | } 39 | checkname(t, expected.Label, actual.Label) 40 | } 41 | checkNode(t, expected.Value, actual.Value) 42 | } 43 | 44 | func checkBlock(t *testing.T, expected, actual *block) { 45 | if l := len(expected.Params); l != len(actual.Params) { 46 | t.Errorf("%s - unexpected block.Params length, expected=%d, got=%d\n", actual.Pos(), l, len(actual.Params)) 47 | return 48 | } 49 | 50 | for i := range expected.Params { 51 | checkNode(t, expected.Params[i], actual.Params[i]) 52 | } 53 | } 54 | 55 | func checkArray(t *testing.T, expected, actual *array) { 56 | if l := len(expected.Items); l != len(actual.Items) { 57 | t.Errorf("%s - unexpected array.Items length, expected=%d, got=%d\n", actual.Pos(), l, len(actual.Items)) 58 | return 59 | } 60 | 61 | for i := range expected.Items { 62 | checkNode(t, expected.Items[i], actual.Items[i]) 63 | } 64 | } 65 | 66 | func checkNode(t *testing.T, expected, actual node) { 67 | switch v := expected.(type) { 68 | case *name: 69 | name, ok := actual.(*name) 70 | 71 | if !ok { 72 | t.Errorf("%s - unexpected node type, expected=%T, got=%T\n", actual.Pos(), v, actual) 73 | return 74 | } 75 | checkname(t, v, name) 76 | case *lit: 77 | lit, ok := actual.(*lit) 78 | 79 | if !ok { 80 | t.Errorf("%s - unexpected node type, expected=%T, got=%T\n", actual.Pos(), v, actual) 81 | return 82 | } 83 | checklit(t, v, lit) 84 | case *param: 85 | param, ok := actual.(*param) 86 | 87 | if !ok { 88 | t.Errorf("%s - unexpected node type, expected=%T, got=%T\n", actual.Pos(), v, actual) 89 | return 90 | } 91 | checkparam(t, v, param) 92 | case *block: 93 | block, ok := actual.(*block) 94 | 95 | if !ok { 96 | t.Errorf("%s - unexpected node type, expected=%T, got=%T\n", actual.Pos(), v, actual) 97 | return 98 | } 99 | checkBlock(t, v, block) 100 | case *array: 101 | array, ok := actual.(*array) 102 | 103 | if !ok { 104 | t.Errorf("%s - unexpected node type, expected=%T, got=%T\n", actual.Pos(), v, actual) 105 | return 106 | } 107 | checkArray(t, v, array) 108 | default: 109 | t.Errorf("%s - unknown node type=%T\n", actual.Pos(), v) 110 | } 111 | } 112 | 113 | func Test_Parser(t *testing.T) { 114 | f, err := os.Open(filepath.Join("testdata", "server.conf")) 115 | 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | 120 | defer f.Close() 121 | 122 | p := parser{ 123 | scanner: newScanner(newSource(f.Name(), f, errh(t))), 124 | inctab: make(map[string]string), 125 | } 126 | 127 | nn, err := p.parse() 128 | 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | 133 | expected := []node{ 134 | ¶m{ 135 | Name: &name{Value: "log"}, 136 | Label: &name{Value: "debug"}, 137 | Value: &lit{ 138 | Value: "/dev/stdout", 139 | Type: StringLit, 140 | }, 141 | }, 142 | ¶m{ 143 | Name: &name{Value: "net"}, 144 | Value: &block{ 145 | Params: []*param{ 146 | { 147 | Name: &name{Value: "listen"}, 148 | Value: &lit{ 149 | Value: "localhost:443", 150 | Type: StringLit, 151 | }, 152 | }, 153 | { 154 | Name: &name{Value: "tls"}, 155 | Value: &block{ 156 | Params: []*param{ 157 | { 158 | Name: &name{Value: "cert"}, 159 | Value: &lit{ 160 | Value: "/var/lib/ssl/server.crt", 161 | Type: StringLit, 162 | }, 163 | }, 164 | { 165 | Name: &name{Value: "key"}, 166 | Value: &lit{ 167 | Value: "/var/lib/ssl/server.key", 168 | Type: StringLit, 169 | }, 170 | }, 171 | }, 172 | }, 173 | }, 174 | }, 175 | }, 176 | }, 177 | ¶m{ 178 | Name: &name{Value: "drivers"}, 179 | Value: &array{ 180 | Items: []node{ 181 | &lit{ 182 | Value: "docker", 183 | Type: StringLit, 184 | }, 185 | &lit{ 186 | Value: "qemu-x86_64", 187 | Type: StringLit, 188 | }, 189 | }, 190 | }, 191 | }, 192 | ¶m{ 193 | Name: &name{Value: "cache"}, 194 | Value: &block{ 195 | Params: []*param{ 196 | { 197 | Name: &name{Value: "redis"}, 198 | Value: &block{ 199 | Params: []*param{ 200 | { 201 | Name: &name{Value: "addr"}, 202 | Value: &lit{ 203 | Value: "localhost:6379", 204 | Type: StringLit, 205 | }, 206 | }, 207 | }, 208 | }, 209 | }, 210 | { 211 | Name: &name{Value: "cleanup_interval"}, 212 | Value: &lit{ 213 | Value: "1h", 214 | Type: DurationLit, 215 | }, 216 | }, 217 | }, 218 | }, 219 | }, 220 | ¶m{ 221 | Name: &name{Value: "store"}, 222 | Label: &name{Value: "files"}, 223 | Value: &block{ 224 | Params: []*param{ 225 | { 226 | Name: &name{Value: "type"}, 227 | Value: &lit{ 228 | Value: "file", 229 | Type: StringLit, 230 | }, 231 | }, 232 | { 233 | Name: &name{Value: "path"}, 234 | Value: &lit{ 235 | Value: "/var/lib/files", 236 | Type: StringLit, 237 | }, 238 | }, 239 | { 240 | Name: &name{Value: "limit"}, 241 | Value: &lit{ 242 | Value: "50MB", 243 | Type: SizeLit, 244 | }, 245 | }, 246 | }, 247 | }, 248 | }, 249 | } 250 | 251 | if l := len(expected); l != len(nn) { 252 | t.Fatalf("unexpected number of nodes, expected=%d, got=%d\n", l, len(nn)) 253 | } 254 | 255 | for i, n := range nn { 256 | checkNode(t, expected[i], n) 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Config 2 | 3 | * [Overview](#overview) 4 | * [Options](#options) 5 | * [Error handling](#error-handling) 6 | * [Environment variables](#environment-variables) 7 | * [Custom variable expansion](#custom-variable-expansion) 8 | * [Includes](#includes) 9 | * [Struct tags](#struct-tags) 10 | * [Syntax](#syntax) 11 | * [Comments](#comments) 12 | * [String](#string) 13 | * [Number](#number) 14 | * [Bool](#bool) 15 | * [Duration](#duration) 16 | * [Size](#size) 17 | * [Array](#array) 18 | * [Block](#block) 19 | * [Label](#label) 20 | 21 | Config is a library for working with structured configuration files in Go. This 22 | library defines its own minimal structured configuration language. 23 | 24 | ## Overview 25 | 26 | The language organizes configuration into a list of parameters. Below is an 27 | example, 28 | 29 | # Example configuration file. 30 | 31 | net { 32 | listen ":https" 33 | 34 | tls { 35 | cert "/var/lib/ssl/server.crt" 36 | key "/var/lib/ssl/server.key" 37 | 38 | ciphers ["AES-128SHA256", "AES-256SHA256"] 39 | } 40 | } 41 | 42 | log access { 43 | level "info" 44 | file "/var/log/http/access.log" 45 | } 46 | 47 | body_limt 50MB 48 | 49 | timeout { 50 | read 10m 51 | write 10m 52 | } 53 | 54 | The above file would then be decoded like so in your Go program, 55 | 56 | package main 57 | 58 | import ( 59 | "fmt" 60 | "os" 61 | "time" 62 | 63 | "github.com/andrewpillar/config" 64 | ) 65 | 66 | type Config struct { 67 | Net struct { 68 | Listen string 69 | 70 | TLS struct { 71 | Cert string 72 | Key string 73 | Ciphers []string 74 | } 75 | } 76 | 77 | Log map[string]struct { 78 | Level string 79 | File string 80 | } 81 | 82 | BodyLimit int64 `config:"body_limit"` 83 | 84 | Timeout struct { 85 | Read time.Duration 86 | Write time.Duration 87 | } 88 | } 89 | 90 | func main() { 91 | var cfg Config 92 | 93 | if err := config.DecodeFile(&cfg, "server.conf"); err != nil { 94 | fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err) 95 | os.Exit(1) 96 | } 97 | } 98 | 99 | ## Options 100 | 101 | Options can be used to configure how a file is decoded. These are callbacks that 102 | can be passed to either the `NewDecoder` function or the `DecodeFile` function. 103 | 104 | ### Error handling 105 | 106 | A custom error handler can be configured via the `ErrorHandler` option. This 107 | takes a `func(pos Pos, msg string)` callback, which is called when an error 108 | occurs during parsing of a file. This is given the position at which the error 109 | occurred, and the message. If no handler is configured, then the `Stderrh` 110 | error handler is used by default. 111 | 112 | config.DecodeFile(&cfg, "file.conf", config.ErrorHandler(customHandler)) 113 | 114 | ### Environment variables 115 | 116 | Environment variables can be supported via the `Envvars` option. This will 117 | expand any `${VARIABLE}` that is found in a string literal in the configuration 118 | file into the respective environment variable. 119 | 120 | config.DecodeFile(&cfg, "file.conf", config.Envvars) 121 | 122 | Environment variables can also be referenced with the `env` prefix, should you 123 | refer more explicitness in your configuration files, 124 | 125 | password "${env:PASSWORD}" 126 | 127 | ### Custom variable expansion 128 | 129 | As previously demonstrated, by default any `${VARIABLE}` that is found in a 130 | string literal will be expanded into the resective environment variable. Custom 131 | variable expansion can be implemented via the `Expand` option, whereby you 132 | register an expansion function against a prefix, for example, 133 | 134 | expandSecret := func(key string) (string, error) { 135 | // Assume we have some kind of secret store, Vault, a keystore of some 136 | // kind, etc. 137 | return secretStore.Get(key) 138 | } 139 | 140 | config.DecodeFile(&cfg, "file.confg", config.Expand("secret", expandSecret)) 141 | 142 | in the configuration file we can then use the prefix of `secret` to tell the 143 | decoder from where the variable should be taken for expansion, 144 | 145 | password "${secret:PASSWORD}" 146 | 147 | ### Includes 148 | 149 | Includes can be configured via the `Includes` option. This will support the 150 | inclusion of configuration files via the `include` parameter. 151 | 152 | config.DecodeFile(&cfg, "file.conf", config.Includes) 153 | 154 | 155 | This expects to be given either a string literal or an array of string literals 156 | for the file(s) to include, 157 | 158 | include "database.conf" 159 | 160 | include [ 161 | "database.conf", 162 | "smtp.conf", 163 | ] 164 | 165 | ## Struct tags 166 | 167 | The decoding of each parameter can be configured via the `config` struct field 168 | tag. The name of the tag specifies the parameter to map the field to, and the 169 | subsequent comma separated list are additional options. 170 | 171 | The `deprecated` option marks a field as deprecated. This will emit an error 172 | to the error handler during decoding if the deprecated parameter is encountered. 173 | For example, assume you have an `ssl` configuration block that you want to 174 | deprecate, you would do the following, 175 | 176 | type TLSConfig struct { 177 | CA string 178 | Cert string 179 | Key string 180 | } 181 | 182 | type Config struct { 183 | TLS TLSConfig 184 | SSL TLSConfig `config:"ssl,deprecated"` 185 | } 186 | 187 | to specify the parameter that should replace the `ssl` parameter you would 188 | separate the name with a `:` in the option, 189 | 190 | type Config struct { 191 | TLS TLSConfig 192 | SSL TLSConfig `config:"ssl,deprecated:tls"` 193 | } 194 | 195 | The `nogroup` option prevents the grouping of labelled parameters into a map. 196 | This would be used in an instance where you want more explicit control over 197 | how labelled parameters are decoded. For example, consider the following 198 | configuration, 199 | 200 | store sftp { 201 | addr "sftp.example.com" 202 | 203 | auth { 204 | username "sftp" 205 | identity "/var/lib/ssh/id_rsa" 206 | } 207 | } 208 | 209 | store disk { 210 | path "/var/lib/files" 211 | } 212 | 213 | this defines two `store` blocks that are labelled. Both blocks vary with the 214 | parameters that they offer. We can decode the above into the below struct, 215 | 216 | type Config struct { 217 | Store struct { 218 | SFTP struct { 219 | Addr string 220 | 221 | Auth struct { 222 | Username string 223 | Identity string 224 | } 225 | } 226 | 227 | Disk struct { 228 | Path string 229 | } 230 | } `config:",nogroup"` 231 | } 232 | 233 | ## Syntax 234 | 235 | A configuration file is a plain text file with a list of parameters and their 236 | values. The value of a parameter can either be a literal, array, or a parameter 237 | block. Typically, the filename should be suffixed with the `.conf` file 238 | extension. 239 | 240 | ### Comments 241 | 242 | Comments start with `#` and end with a newline. This can either be on a full 243 | line, or inlined. 244 | 245 | # Full-line comment. 246 | temp 0.5 # Inline comment. 247 | 248 | ### String 249 | 250 | A string is a sequence of bytes wrapped between a pair of `"`. As of now, string 251 | literals are limited in their capability. 252 | 253 | string "this is a string literal" 254 | string2 "this is another \"string\" literal, with escapes" 255 | 256 | ### Number 257 | 258 | Integers and floats are supported. Integers are decoded into the `int64` type, 259 | and floats into the `float64` type. 260 | 261 | int 10 262 | float 10.25 263 | 264 | ### Bool 265 | 266 | A bool is a `true` or `false` value. 267 | 268 | bool true 269 | bool2 false 270 | 271 | ### Duration 272 | 273 | Duration is a duration of time. This is a number literal suffixed with either 274 | `s`, `m`, or `h`, for second, minute, or hour respectively. Duration is decoded 275 | into the `time.Duration` type. 276 | 277 | seconds 10s 278 | minutes 10m 279 | hours 10h 280 | 281 | The duration units can also be combined for more explicit values, 282 | 283 | hour_half 1h30m 284 | 285 | ### Size 286 | 287 | Size is the amount of bytes. This is a number literal suffixed with the unit, 288 | either `B`, `KB`, `MB`, `GB`, or `TB`. Size is decoded into the `int64` type. 289 | 290 | byte 1B 291 | kilobyte 1KB 292 | megabyte 1MB 293 | gigabyte 1GB 294 | terabyte 1TB 295 | 296 | ### Array 297 | 298 | An array is a list of values, these can either be literals, or blocks wrapped 299 | in a pair of `[ ]`. Arrays are decoded into a slice of the respective type. 300 | 301 | strings ["str", "str2", "str3"] 302 | numbers [1, 2, 3, 4] 303 | 304 | arrays [ 305 | [1, 2, 3], 306 | [4, 5, 6], 307 | [7, 8, 9], 308 | ] 309 | 310 | blocks [{ 311 | x 1 312 | y 2 313 | z 3 314 | }, { 315 | x 4 316 | y 5 317 | z 6 318 | }, { 319 | x 7 320 | y 8 321 | z 9 322 | ]] 323 | 324 | ### Block 325 | 326 | A block is a list of parameters wrapped between a pair of `{ }`. Blocks are 327 | decoded into a struct. 328 | 329 | block { 330 | param "value" 331 | 332 | block2 { 333 | param 10 334 | } 335 | } 336 | 337 | ### Label 338 | 339 | A label can be used to distinguish between parameters of the same name. This 340 | can be useful when you have similar configuration parameters that you want to 341 | distinguish between. A labelled parameter is decoded into a map, where the 342 | key of the map is a string, the label itself, and the value of the map is 343 | the type for the parameter. This is not the case if the `nogroup` parameter is 344 | specified, in which case the label itself will be mapped to a field in a struct. 345 | 346 | auth ldap { 347 | addr "ldap://example.com" 348 | 349 | tls { 350 | cert "/var/lib/ssl/client.crt" 351 | key "/var/lib/ssl/client.key" 352 | } 353 | } 354 | 355 | auth saml { 356 | addr "https://idp.example.com" 357 | 358 | tls { 359 | ca "/var/lib/ssl/ca.crt" 360 | } 361 | } 362 | 363 | Labels aren't just limited to blocks, they can be applied to any other 364 | parameter type, 365 | 366 | ports open ["8080", "8443"] 367 | 368 | ports close ["80", "443"] 369 | -------------------------------------------------------------------------------- /scanner.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "unicode" 6 | ) 7 | 8 | type scanner struct { 9 | *source 10 | 11 | nlsemi bool 12 | pos Pos 13 | tok token 14 | typ LitType 15 | lit string 16 | } 17 | 18 | func newScanner(src *source) *scanner { 19 | sc := &scanner{ 20 | source: src, 21 | } 22 | sc.next() 23 | return sc 24 | } 25 | 26 | func isLetter(r rune) bool { 27 | return 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || r == '_' || unicode.IsLetter(r) 28 | } 29 | 30 | func isDigit(r rune) bool { 31 | return '0' <= r && r <= '9' 32 | } 33 | 34 | func (sc *scanner) ident() { 35 | sc.startLit() 36 | 37 | r := sc.get() 38 | 39 | for isLetter(r) || isDigit(r) { 40 | r = sc.get() 41 | } 42 | sc.unget() 43 | 44 | sc.nlsemi = true 45 | sc.tok = _Name 46 | sc.lit = sc.stopLit() 47 | } 48 | 49 | func (sc *scanner) number() { 50 | sc.startLit() 51 | 52 | isFloat := false 53 | typ := IntLit 54 | 55 | r := sc.get() 56 | 57 | for { 58 | if !isDigit(r) { 59 | if r == '.' { 60 | if isFloat { 61 | sc.err("invalid point in float") 62 | break 63 | } 64 | 65 | isFloat = true 66 | r = sc.get() 67 | continue 68 | } 69 | break 70 | } 71 | r = sc.get() 72 | } 73 | sc.unget() 74 | 75 | if isFloat { 76 | typ = FloatLit 77 | } 78 | 79 | sc.nlsemi = true 80 | sc.tok = _Literal 81 | sc.typ = typ 82 | sc.lit = sc.stopLit() 83 | } 84 | 85 | func (sc *scanner) string() { 86 | sc.startLit() 87 | 88 | r := sc.get() 89 | 90 | for { 91 | if r == '"' { 92 | break 93 | } 94 | if r == '\\' { 95 | r = sc.get() 96 | 97 | if r == '"' { 98 | r = sc.get() 99 | } 100 | continue 101 | } 102 | if r == '\n' { 103 | sc.err("unexpected newline in string") 104 | break 105 | } 106 | r = sc.get() 107 | } 108 | 109 | lit := sc.stopLit() 110 | 111 | sc.nlsemi = true 112 | sc.tok = _Literal 113 | sc.typ = StringLit 114 | sc.lit = lit[1 : len(lit)-1] 115 | } 116 | 117 | func (sc *scanner) duration(r rune) { 118 | lit := []rune(sc.lit) 119 | 120 | for r == 's' || r == 'm' || r == 'h' { 121 | lit = append(lit, r) 122 | 123 | r = sc.get() 124 | 125 | if isDigit(r) { 126 | sc.number() 127 | lit = append(lit, []rune(sc.lit)...) 128 | r = sc.get() 129 | } 130 | } 131 | sc.unget() 132 | 133 | sc.typ = DurationLit 134 | sc.lit = string(lit) 135 | } 136 | 137 | func (sc *scanner) next() { 138 | nlsemi := sc.nlsemi 139 | sc.nlsemi = false 140 | 141 | redo: 142 | sc.tok = token(0) 143 | sc.lit = sc.lit[0:0] 144 | sc.typ = LitType(0) 145 | 146 | r := sc.get() 147 | 148 | for r == ' ' || r == '\t' || r == '\r' || r == '\n' && !nlsemi { 149 | r = sc.get() 150 | } 151 | 152 | if r == '#' { 153 | for r != '\n' { 154 | r = sc.get() 155 | } 156 | goto redo 157 | } 158 | 159 | sc.pos = sc.getpos() 160 | 161 | if isLetter(r) { 162 | sc.ident() 163 | return 164 | } 165 | 166 | if isDigit(r) || r == '-' { 167 | sc.number() 168 | 169 | r = sc.get() 170 | 171 | lit := []rune(sc.lit) 172 | 173 | // Check if we have a suffix for a duration or size literal. 174 | switch r { 175 | case 's', 'm', 'h': 176 | sc.duration(r) 177 | case 'B': 178 | lit = append(lit, r) 179 | sc.typ = SizeLit 180 | sc.lit = string(lit) 181 | case 'K', 'M', 'G', 'T': 182 | lit = append(lit, r) 183 | 184 | if r = sc.get(); r == 'B' { 185 | lit = append(lit, r) 186 | sc.typ = SizeLit 187 | sc.lit = string(lit) 188 | break 189 | } 190 | sc.unget() 191 | default: 192 | sc.unget() 193 | } 194 | return 195 | } 196 | 197 | switch r { 198 | case -1: 199 | sc.tok = _EOF 200 | case '\n', ';': 201 | sc.tok = _Semi 202 | case ',': 203 | sc.tok = _Comma 204 | case '{': 205 | sc.tok = _Lbrace 206 | case '}': 207 | sc.nlsemi = true 208 | sc.tok = _Rbrace 209 | case '[': 210 | sc.tok = _Lbrack 211 | case ']': 212 | sc.nlsemi = true 213 | sc.tok = _Rbrack 214 | case '"': 215 | sc.string() 216 | default: 217 | sc.err(fmt.Sprintf("unexpected token %U", r)) 218 | goto redo 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /source.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "strconv" 7 | "unicode/utf8" 8 | ) 9 | 10 | type Pos struct { 11 | File string 12 | Line int 13 | Col int 14 | } 15 | 16 | func (p Pos) String() string { 17 | s := p.File 18 | 19 | if p.Line > 0 { 20 | s += "," + strconv.FormatInt(int64(p.Line), 10) 21 | 22 | if p.Col > 0 { 23 | s += ":" + strconv.FormatInt(int64(p.Col), 10) 24 | } 25 | } 26 | return s 27 | } 28 | 29 | func (p Pos) Err(msg string) error { 30 | return errors.New(p.String() + " - " + msg) 31 | } 32 | 33 | // source represents a source file being parsed for tokens. This records the 34 | // current and previous position in the buffer using pos and pos0 respectively 35 | // as well ass line0, line and col0, col for for explicit positional information 36 | // for error reporting. 37 | // 38 | // eof denotes where in the buffer the EOF occurs. 39 | // 40 | // lit denotes the start position of a literal that we want to copy from the 41 | // underlying buffer. If lit is < 0 when a copy of a literal is made then the 42 | // a panic will happen. 43 | // 44 | // The errh callback is called to handle the reporting of errors that may occur 45 | // during parsing of a file. 46 | type source struct { 47 | name string 48 | r io.Reader 49 | pos0, pos int 50 | eof int 51 | line0, line int 52 | col0, col int 53 | errh func(Pos, string) 54 | buf []byte 55 | lit int 56 | } 57 | 58 | // newSource returns a new source for the given reader. The name of the source 59 | // should be used to uniquely identify it, for example a filename. 60 | func newSource(name string, r io.Reader, errh func(Pos, string)) *source { 61 | return &source{ 62 | name: name, 63 | r: r, 64 | line: 1, 65 | errh: errh, 66 | lit: -1, 67 | buf: make([]byte, 4096), 68 | } 69 | } 70 | 71 | // getpos returns the current position in the source. 72 | func (s *source) getpos() Pos { 73 | return Pos{ 74 | File: s.name, 75 | Line: s.line, 76 | Col: s.col, 77 | } 78 | } 79 | 80 | func (s *source) err(msg string) { 81 | s.errh(s.getpos(), msg) 82 | } 83 | 84 | // get returns the next rune in the source. If EOF has been reached then -1 85 | // is returned. If a fatal error occurs when reading from the underlying 86 | // source, then an error is recorded via errh and -1 is returned. 87 | func (s *source) get() rune { 88 | redo: 89 | s.pos0, s.line0, s.col0 = s.pos, s.line, s.col 90 | 91 | if s.pos == 0 || s.pos >= len(s.buf) { 92 | if s.lit >= 0 { 93 | buf := s.buf[s.lit:s.pos] 94 | 95 | s.buf = make([]byte, len(s.buf)+len(buf)) 96 | copy(s.buf, buf) 97 | } 98 | 99 | n, err := s.r.Read(s.buf) 100 | 101 | if err != nil { 102 | if !errors.Is(err, io.EOF) { 103 | s.err("io error: " + err.Error()) 104 | } 105 | return -1 106 | } 107 | 108 | s.pos = 0 109 | s.eof = n 110 | } 111 | 112 | if s.pos == s.eof { 113 | return -1 114 | } 115 | 116 | b := s.buf[s.pos] 117 | 118 | if b >= utf8.RuneSelf { 119 | r, w := utf8.DecodeRune(s.buf[s.pos:]) 120 | 121 | s.pos += w 122 | s.col += w 123 | 124 | return r 125 | } 126 | 127 | s.pos++ 128 | s.col++ 129 | 130 | if b == 0 { 131 | s.err("invalid NUL byte") 132 | goto redo 133 | } 134 | 135 | if b == '\n' { 136 | s.line++ 137 | s.col = 0 138 | } 139 | return rune(b) 140 | } 141 | 142 | // unget moves the position of the source back one. This cannot be called 143 | // multiple subsequent times. 144 | func (s *source) unget() { 145 | s.pos, s.line, s.col = s.pos0, s.line0, s.col0 146 | } 147 | 148 | // startLit sets the literal position to pos0, the previous position in the 149 | // buffer. 150 | func (s *source) startLit() { 151 | s.lit = s.pos0 152 | } 153 | 154 | // stopLit returns the literal being scanned from the buffer. If the literal 155 | // position is < 0 then this panics with "syntax: negative literal position". 156 | func (s *source) stopLit() string { 157 | if s.lit < 0 { 158 | panic("syntax: negative literal position") 159 | } 160 | 161 | lit := s.buf[s.lit:s.pos] 162 | s.lit = -1 163 | 164 | return string(lit) 165 | } 166 | -------------------------------------------------------------------------------- /spec.md: -------------------------------------------------------------------------------- 1 | # Spec 2 | 3 | This is the specification for the configuration language defined by this 4 | library. The language was inspired by the NGINX configuration language, 5 | primarily. 6 | 7 | The full spec of the language is below in Extended Backus-Naur Form, 8 | 9 | digit = "0" ... "9" . 10 | letter = "a" ... "z" | "A" ... "Z" | "_" | unicode_letter . 11 | 12 | identifier = letter { letter | digit } . 13 | 14 | float_literal = digit "." { digit } . 15 | int_literal = { digit } . 16 | number_literal = int_literal | float_literal . 17 | 18 | duration_unit = "s" | "m" | "h" . 19 | duration_literal = number_literal { number_literal | duration_unit } . 20 | 21 | size_unit = "B" | "KB" | "MB" | "GB" | "TB" . 22 | size_literal = int_literal size_unit . 23 | 24 | string_literal = `"` { letter } `"` . 25 | 26 | bool_literal = "true" | "false" . 27 | 28 | literal = bool_literal | string_literal | number_literal | duration_literal | size_literal . 29 | 30 | block = "{" [ parameter ";" ] "}" . 31 | array = "[" [ operand "," ] "]" . 32 | operand = literal | array | block . 33 | 34 | parameter = identifier [ identifier ] operand . 35 | 36 | file = { parameter ";" } . 37 | -------------------------------------------------------------------------------- /testdata/array.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | strings ["one", "two", "three", "four", "\"five\""] 4 | 5 | ints [1, 2, 3, 4] 6 | 7 | floats [1.2, 3.4, 5.6, 7.8] 8 | 9 | bools [true, false] 10 | 11 | durations [1s, 2m, 3h] 12 | 13 | sizes [1B, 2KB, 3MB, 4GB, 5TB] 14 | 15 | blocks [{ 16 | string "foo" 17 | }, { 18 | string "bar" 19 | }, { 20 | string "baz" 21 | }] 22 | -------------------------------------------------------------------------------- /testdata/deprecated.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | ssl { 4 | ca "/var/lib/ssl/ca.crt" 5 | } 6 | -------------------------------------------------------------------------------- /testdata/duration.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | hour 1h 4 | hour_half 1h30m 5 | hour_half_seconds 1h30m45s 6 | -------------------------------------------------------------------------------- /testdata/envvars.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | database { 4 | addr "localhost:5432" 5 | username "${env:DB_USERNAME}" 6 | password "${DB_PASSWORD}" 7 | 8 | tls { 9 | key "/var/lib/ssl/client.key" 10 | keypassword "${vault:/secrets/ssl/TLS_KEY_PASSWORD}" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /testdata/include.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | include "testdata/utf8.conf" 4 | 5 | include [ 6 | "testdata/duration.conf", 7 | ] 8 | -------------------------------------------------------------------------------- /testdata/label.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | auth internal { 4 | addr "postgres://localhost:5432" 5 | } 6 | 7 | auth ldap { 8 | addr "ldap://example.com" 9 | 10 | tls { 11 | ca "/var/lib/ssl/ca.crt" 12 | } 13 | } 14 | 15 | auth saml { 16 | addr "https://idp.example.com" 17 | 18 | tls { 19 | ca "/var/lib/ssl/ca.crt" 20 | } 21 | } 22 | 23 | ports open ["8080", "8443"] 24 | 25 | ports close ["80", "443"] 26 | 27 | provider github {} 28 | provider gitlab {} 29 | -------------------------------------------------------------------------------- /testdata/map.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | labels qemu { 4 | arch [ 5 | "x86_64", 6 | "aarch64", 7 | ] 8 | 9 | os [ 10 | "debian", 11 | "alpine", 12 | ] 13 | } 14 | 15 | labels docker { 16 | programming [ 17 | "go", 18 | "js", 19 | "python", 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /testdata/nogroup.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | driver ssh { 4 | addr "example.com" 5 | 6 | auth { 7 | username "root" 8 | identity "/var/lib/ssh/id_rsa" 9 | } 10 | } 11 | 12 | driver docker { 13 | host "unix:///var/run/docker.sock" 14 | 15 | version "1.40" 16 | } 17 | 18 | driver qemu { 19 | disks "/var/lib/djinn/images" 20 | 21 | cpus 1 22 | 23 | memory 2KB 24 | } 25 | -------------------------------------------------------------------------------- /testdata/server.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | log debug "/dev/stdout" 4 | 5 | net { 6 | listen "localhost:443" 7 | 8 | tls { 9 | cert "/var/lib/ssl/server.crt" 10 | key "/var/lib/ssl/server.key" 11 | } 12 | } 13 | 14 | drivers [ 15 | "docker", 16 | "qemu-x86_64", 17 | ] 18 | 19 | cache { 20 | redis { 21 | addr "localhost:6379" 22 | } 23 | 24 | cleanup_interval 1h 25 | } 26 | 27 | store files { 28 | type "file" 29 | path "/var/lib/files" 30 | limit 50MB 31 | } 32 | -------------------------------------------------------------------------------- /testdata/utf8.conf: -------------------------------------------------------------------------------- 1 | # Translations pulled from Google translate, not expecting them to be 100% 2 | # accurate. 3 | 4 | block 标签 { 5 | strings [ 6 | "细绳", 7 | "细绳", 8 | "细绳", 9 | "细绳", 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /token.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type token uint 4 | 5 | //go:generate stringer -type token -linecomment 6 | const ( 7 | _EOF token = iota + 1 // eof 8 | 9 | _Name // name 10 | _Literal // literal 11 | 12 | _Semi // newline 13 | _Comma // comma 14 | 15 | _Lbrace // { 16 | _Rbrace // } 17 | _Lbrack // [ 18 | _Rbrack // ] 19 | ) 20 | 21 | type LitType uint 22 | 23 | //go:generate stringer -type LitType -linecomment 24 | const ( 25 | StringLit LitType = iota + 1 // string 26 | IntLit // int 27 | FloatLit // float 28 | BoolLit // bool 29 | DurationLit // duration 30 | SizeLit // size 31 | ) 32 | -------------------------------------------------------------------------------- /token_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type token -linecomment"; DO NOT EDIT. 2 | 3 | package config 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[_EOF-1] 12 | _ = x[_Name-2] 13 | _ = x[_Literal-3] 14 | _ = x[_Semi-4] 15 | _ = x[_Comma-5] 16 | _ = x[_Lbrace-6] 17 | _ = x[_Rbrace-7] 18 | _ = x[_Lbrack-8] 19 | _ = x[_Rbrack-9] 20 | } 21 | 22 | const _token_name = "eofnameliteralnewlinecomma{}[]" 23 | 24 | var _token_index = [...]uint8{0, 3, 7, 14, 21, 26, 27, 28, 29, 30} 25 | 26 | func (i token) String() string { 27 | i -= 1 28 | if i >= token(len(_token_index)-1) { 29 | return "token(" + strconv.FormatInt(int64(i+1), 10) + ")" 30 | } 31 | return _token_name[_token_index[i]:_token_index[i+1]] 32 | } 33 | --------------------------------------------------------------------------------