├── .travis.yml ├── LICENSE ├── README.md ├── config.go ├── config_test.go └── examples ├── good.conf ├── invalid.conf └── simple.conf /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2022 Stovepipe Studios, Inc. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-toml-config 2 | ============== 3 | 4 | [![Build Status](https://travis-ci.org/stvp/go-toml-config.png?branch=master)](https://travis-ci.org/stvp/go-toml-config) 5 | 6 | go-toml-config is a simple [TOML](https://github.com/mojombo/toml)-based 7 | configuration package for Golang apps that allows you to easily load 8 | configuration files and set defaults. It's a simple wrapper around 9 | [`flag.FlagSet`](http://golang.org/pkg/flag/), so you can use it in pretty much 10 | the same exact way. 11 | 12 | [API documentation](http://godoc.org/github.com/stvp/go-toml-config) 13 | 14 | Example 15 | -------- 16 | 17 | With `my_app.conf`: 18 | 19 | ```toml 20 | country = "USA" 21 | 22 | [atlanta] 23 | enabled = true 24 | population = 432427 25 | temperature = 99.6 26 | ``` 27 | 28 | Use: 29 | 30 | ```go 31 | import "github.com/stvp/go-toml-config" 32 | 33 | var ( 34 | country = config.String("country", "Unknown") 35 | atlantaEnabled = config.Bool("atlanta.enabled", false) 36 | alantaPopulation = config.Int("atlanta.population", 0) 37 | atlantaTemperature = config.Float64("atlanta.temperature", 0) 38 | ) 39 | 40 | func main() { 41 | config.Parse("/path/to/my_app.conf") 42 | } 43 | ``` 44 | 45 | You can also create different ConfigSets to manage different logical groupings 46 | of config variables: 47 | 48 | ```go 49 | networkConfig = config.NewConfigSet("network settings", config.ExitOnError) 50 | networkConfig.String("host", "localhost") 51 | networkConfig.Int("port", 8080) 52 | networkConfig.Parse("/path/to/network.conf") 53 | ``` 54 | 55 | Contributors 56 | ------------ 57 | 58 | Thanks all! 59 | 60 | * @tysonmote 61 | * @matrixik 62 | * @fwang2002 63 | * @shanks 64 | * @xboston 65 | * @tgulacsi 66 | 67 | 68 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package config implements simple TOML-based configuration variables, based on 3 | the flag package in the standard Go library (In fact, it's just a simple 4 | wrapper around flag.FlagSet). It is used in a similar manner, minus the usage 5 | strings and other command-line specific bits. 6 | 7 | Usage: 8 | 9 | Given the following TOML file: 10 | 11 | country = "USA" 12 | 13 | [atlanta] 14 | enabled = true 15 | population = 432427 16 | temperature = 99.6 17 | 18 | Define your config variables and give them defaults: 19 | 20 | import "github.com/stvp/go-toml-config" 21 | 22 | var ( 23 | country = config.String("country", "Unknown") 24 | atlantaEnabled = config.Bool("atlanta.enabled", false) 25 | alantaPopulation = config.Int("atlanta.population", 0) 26 | atlantaTemperature = config.Float("atlanta.temperature", 0) 27 | ) 28 | 29 | After all the config variables are defined, load the config file to overwrite 30 | the default values with the user-supplied config settings: 31 | 32 | if err := config.Parse("/path/to/myconfig.conf"); err != nil { 33 | panic(err) 34 | } 35 | 36 | You can also create separate ConfigSets for different config files: 37 | 38 | networkConfig = config.NewConfigSet("network settings", config.ExitOnError) 39 | networkConfig.String("host", "localhost") 40 | networkConfig.Int("port", 8080) 41 | networkConfig.Parse("/path/to/network.conf") 42 | */ 43 | package config 44 | 45 | import ( 46 | "errors" 47 | "flag" 48 | "fmt" 49 | "io/ioutil" 50 | "os" 51 | "regexp" 52 | "strings" 53 | "time" 54 | 55 | "github.com/pelletier/go-toml" 56 | ) 57 | 58 | // -- ConfigSet 59 | 60 | type ConfigSet struct { 61 | *flag.FlagSet 62 | } 63 | 64 | // BoolVar defines a bool config with a given name and default value for a ConfigSet. 65 | // The argument p points to a bool variable in which to store the value of the config. 66 | func (c *ConfigSet) BoolVar(p *bool, name string, value bool) { 67 | c.FlagSet.BoolVar(p, name, value, "") 68 | } 69 | 70 | // Bool defines a bool config variable with a given name and default value for 71 | // a ConfigSet. 72 | func (c *ConfigSet) Bool(name string, value bool) *bool { 73 | return c.FlagSet.Bool(name, value, "") 74 | } 75 | 76 | // IntVar defines a int config with a given name and default value for a ConfigSet. 77 | // The argument p points to a int variable in which to store the value of the config. 78 | func (c *ConfigSet) IntVar(p *int, name string, value int) { 79 | c.FlagSet.IntVar(p, name, value, "") 80 | } 81 | 82 | // Int defines a int config variable with a given name and default value for a 83 | // ConfigSet. 84 | func (c *ConfigSet) Int(name string, value int) *int { 85 | return c.FlagSet.Int(name, value, "") 86 | } 87 | 88 | // Int64Var defines a int64 config with a given name and default value for a ConfigSet. 89 | // The argument p points to a int64 variable in which to store the value of the config. 90 | func (c *ConfigSet) Int64Var(p *int64, name string, value int64) { 91 | c.FlagSet.Int64Var(p, name, value, "") 92 | } 93 | 94 | // Int64 defines a int64 config variable with a given name and default value 95 | // for a ConfigSet. 96 | func (c *ConfigSet) Int64(name string, value int64) *int64 { 97 | return c.FlagSet.Int64(name, value, "") 98 | } 99 | 100 | // UintVar defines a uint config with a given name and default value for a ConfigSet. 101 | // The argument p points to a uint variable in which to store the value of the config. 102 | func (c *ConfigSet) UintVar(p *uint, name string, value uint) { 103 | c.FlagSet.UintVar(p, name, value, "") 104 | } 105 | 106 | // Uint defines a uint config variable with a given name and default value for 107 | // a ConfigSet. 108 | func (c *ConfigSet) Uint(name string, value uint) *uint { 109 | return c.FlagSet.Uint(name, value, "") 110 | } 111 | 112 | // Uint64Var defines a uint64 config with a given name and default value for a ConfigSet. 113 | // The argument p points to a uint64 variable in which to store the value of the config. 114 | func (c *ConfigSet) Uint64Var(p *uint64, name string, value uint64) { 115 | c.FlagSet.Uint64Var(p, name, value, "") 116 | } 117 | 118 | // Uint64 defines a uint64 config variable with a given name and default value 119 | // for a ConfigSet. 120 | func (c *ConfigSet) Uint64(name string, value uint64) *uint64 { 121 | return c.FlagSet.Uint64(name, value, "") 122 | } 123 | 124 | // StringVar defines a string config with a given name and default value for a ConfigSet. 125 | // The argument p points to a string variable in which to store the value of the config. 126 | func (c *ConfigSet) StringVar(p *string, name string, value string) { 127 | c.FlagSet.StringVar(p, name, value, "") 128 | } 129 | 130 | // String defines a string config variable with a given name and default value 131 | // for a ConfigSet. 132 | func (c *ConfigSet) String(name string, value string) *string { 133 | return c.FlagSet.String(name, value, "") 134 | } 135 | 136 | // Float64Var defines a float64 config with a given name and default value for a ConfigSet. 137 | // The argument p points to a float64 variable in which to store the value of the config. 138 | func (c *ConfigSet) Float64Var(p *float64, name string, value float64) { 139 | c.FlagSet.Float64Var(p, name, value, "") 140 | } 141 | 142 | // Float64 defines a float64 config variable with a given name and default 143 | // value for a ConfigSet. 144 | func (c *ConfigSet) Float64(name string, value float64) *float64 { 145 | return c.FlagSet.Float64(name, value, "") 146 | } 147 | 148 | // DurationVar defines a time.Duration config with a given name and default value for a ConfigSet. 149 | // The argument p points to a time.Duration variable in which to store the value of the config. 150 | func (c *ConfigSet) DurationVar(p *time.Duration, name string, value time.Duration) { 151 | c.FlagSet.DurationVar(p, name, value, "") 152 | } 153 | 154 | // Duration defines a time.Duration config variable with a given name and 155 | // default value. 156 | func (c *ConfigSet) Duration(name string, value time.Duration) *time.Duration { 157 | return c.FlagSet.Duration(name, value, "") 158 | } 159 | 160 | // Parse takes a path to a TOML file and loads it. This must be called after 161 | // all the config flags in the ConfigSet have been defined but before the flags 162 | // are accessed by the program. 163 | func (c *ConfigSet) Parse(path string) error { 164 | configBytes, err := ioutil.ReadFile(path) 165 | if err != nil { 166 | return err 167 | } 168 | 169 | tomlTree, err := toml.Load(string(configBytes)) 170 | if err != nil { 171 | errorString := fmt.Sprintf("%s is not a valid TOML file. See https://github.com/mojombo/toml", path) 172 | return errors.New(errorString) 173 | } 174 | 175 | err = c.loadTomlTree(tomlTree, []string{}) 176 | if err != nil { 177 | return err 178 | } 179 | 180 | return nil 181 | } 182 | 183 | // loadTomlTree recursively loads a toml.Tree into this ConfigSet's config 184 | // variables. 185 | func (c *ConfigSet) loadTomlTree(tree *toml.Tree, path []string) error { 186 | for _, key := range tree.Keys() { 187 | fullPath := append(path, key) 188 | value := tree.Get(key) 189 | if subtree, isTree := value.(*toml.Tree); isTree { 190 | err := c.loadTomlTree(subtree, fullPath) 191 | if err != nil { 192 | return err 193 | } 194 | } else { 195 | fullPath := strings.Join(append(path, key), ".") 196 | err := c.Set(fullPath, fmt.Sprintf("%v", value)) 197 | if err != nil { 198 | return buildLoadError(fullPath, err) 199 | } 200 | } 201 | } 202 | return nil 203 | } 204 | 205 | // buildLoadError takes an error from flag.FlagSet#Set and makes it a bit more 206 | // readable, if it recognizes the format. 207 | func buildLoadError(path string, err error) error { 208 | missingFlag := regexp.MustCompile(`^no such flag -([^\s]+)`) 209 | invalidSyntax := regexp.MustCompile(`^.+ parsing "(.+)": invalid syntax$`) 210 | errorString := err.Error() 211 | 212 | if missingFlag.MatchString(errorString) { 213 | errorString = missingFlag.ReplaceAllString(errorString, "$1 is not a valid config setting") 214 | } else if invalidSyntax.MatchString(errorString) { 215 | errorString = "The value for " + path + " is invalid" 216 | } 217 | 218 | return errors.New(errorString) 219 | } 220 | 221 | const ( 222 | ContinueOnError flag.ErrorHandling = flag.ContinueOnError 223 | ExitOnError flag.ErrorHandling = flag.ExitOnError 224 | PanicOnError flag.ErrorHandling = flag.PanicOnError 225 | ) 226 | 227 | // NewConfigSet returns a new ConfigSet with the given name and error handling 228 | // policy. The three valid error handling policies are: flag.ContinueOnError, 229 | // flag.ExitOnError, and flag.PanicOnError. 230 | func NewConfigSet(name string, errorHandling flag.ErrorHandling) *ConfigSet { 231 | return &ConfigSet{ 232 | flag.NewFlagSet(name, errorHandling), 233 | } 234 | } 235 | 236 | // -- globalConfig 237 | 238 | var globalConfig = NewConfigSet(os.Args[0], flag.ExitOnError) 239 | 240 | // BoolVar defines a bool config with a given name and default value. 241 | // The argument p points to a bool variable in which to store the value of the config. 242 | func BoolVar(p *bool, name string, value bool) { 243 | globalConfig.BoolVar(p, name, value) 244 | } 245 | 246 | // Bool defines a bool config variable with a given name and default value. 247 | func Bool(name string, value bool) *bool { 248 | return globalConfig.Bool(name, value) 249 | } 250 | 251 | // IntVar defines a int config with a given name and default value. 252 | // The argument p points to a int variable in which to store the value of the config. 253 | func IntVar(p *int, name string, value int) { 254 | globalConfig.IntVar(p, name, value) 255 | } 256 | 257 | // Int defines a int config variable with a given name and default value. 258 | func Int(name string, value int) *int { 259 | return globalConfig.Int(name, value) 260 | } 261 | 262 | // Int64Var defines a int64 config with a given name and default value. 263 | // The argument p points to a int64 variable in which to store the value of the config. 264 | func Int64Var(p *int64, name string, value int64) { 265 | globalConfig.Int64Var(p, name, value) 266 | } 267 | 268 | // Int64 defines a int64 config variable with a given name and default value. 269 | func Int64(name string, value int64) *int64 { 270 | return globalConfig.Int64(name, value) 271 | } 272 | 273 | // UintVar defines a uint config with a given name and default value. 274 | // The argument p points to a uint variable in which to store the value of the config. 275 | func UintVar(p *uint, name string, value uint) { 276 | globalConfig.UintVar(p, name, value) 277 | } 278 | 279 | // Uint defines a uint config variable with a given name and default value. 280 | func Uint(name string, value uint) *uint { 281 | return globalConfig.Uint(name, value) 282 | } 283 | 284 | // Uint64Var defines a uint64 config with a given name and default value. 285 | // The argument p points to a uint64 variable in which to store the value of the config. 286 | func Uint64Var(p *uint64, name string, value uint64) { 287 | globalConfig.Uint64Var(p, name, value) 288 | } 289 | 290 | // Uint64 defines a uint64 config variable with a given name and default value. 291 | func Uint64(name string, value uint64) *uint64 { 292 | return globalConfig.Uint64(name, value) 293 | } 294 | 295 | // StringVar defines a string config with a given name and default value. 296 | // The argument p points to a string variable in which to store the value of the config. 297 | func StringVar(p *string, name string, value string) { 298 | globalConfig.StringVar(p, name, value) 299 | } 300 | 301 | // String defines a string config variable with a given name and default value. 302 | func String(name string, value string) *string { 303 | return globalConfig.String(name, value) 304 | } 305 | 306 | // Float64Var defines a float64 config with a given name and default value. 307 | // The argument p points to a float64 variable in which to store the value of the config. 308 | func Float64Var(p *float64, name string, value float64) { 309 | globalConfig.Float64Var(p, name, value) 310 | } 311 | 312 | // Float64 defines a float64 config variable with a given name and default 313 | // value. 314 | func Float64(name string, value float64) *float64 { 315 | return globalConfig.Float64(name, value) 316 | } 317 | 318 | // DurationVar defines a time.Duration config with a given name and default value. 319 | // The argument p points to a time.Duration variable in which to store the value of the config. 320 | func DurationVar(p *time.Duration, name string, value time.Duration) { 321 | globalConfig.DurationVar(p, name, value) 322 | } 323 | 324 | // Duration defines a time.Duration config variable with a given name and 325 | // default value. 326 | func Duration(name string, value time.Duration) *time.Duration { 327 | return globalConfig.Duration(name, value) 328 | } 329 | 330 | // Parse takes a path to a TOML file and loads it into the global ConfigSet. 331 | // This must be called after all config flags have been defined but before the 332 | // flags are accessed by the program. 333 | func Parse(path string) error { 334 | return globalConfig.Parse(path) 335 | } 336 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | const ( 11 | GOOD_CONFIG_PATH = "examples/good.conf" 12 | SIMPLE_CONFIG_PATH = "examples/simple.conf" 13 | INVALID_CONFIG_PATH = "examples/invalid.conf" 14 | MISSING_CONFIG_PATH = "examples/nope.conf" 15 | ) 16 | 17 | func TestBuildLoadError(t *testing.T) { 18 | testValues := map[string]string{ 19 | "strconv.ParseInt: parsing \"foo bar\": invalid syntax": "The value for foo.bar is invalid", 20 | "no such flag -my_bool": "my_bool is not a valid config setting", 21 | } 22 | 23 | for given, expected := range testValues { 24 | err := errors.New(given) 25 | if got := buildLoadError("foo.bar", err).Error(); got != expected { 26 | t.Errorf("Error message should have been: %#v, but was: %#v", expected, got) 27 | } 28 | } 29 | } 30 | 31 | func testBadParse(t *testing.T, c *ConfigSet) { 32 | // Missing path 33 | err := c.Parse(MISSING_CONFIG_PATH) 34 | if err == nil || err.Error() != "open examples/nope.conf: no such file or directory" { 35 | t.Error("Expected error when loading missing TOML file, got", err) 36 | } 37 | 38 | // TOML syntax error 39 | err = c.Parse(INVALID_CONFIG_PATH) 40 | if err == nil || err.Error() != "examples/invalid.conf is not a valid TOML file. See https://github.com/mojombo/toml" { 41 | t.Error("Expected error when loading missing TOML file, got", err) 42 | } 43 | 44 | // Type mismatch 45 | c.Int("cool", 10) 46 | c.Float64("neat.terrific", 10.1) 47 | err = c.Parse(SIMPLE_CONFIG_PATH) 48 | if err == nil { 49 | t.Error("Expected an error but didn't get one.") 50 | } 51 | if err.Error() != "The value for cool is invalid" && err.Error() != "neat.terrific.rad is not a valid config setting" { 52 | t.Error(err) 53 | } 54 | 55 | // Extraneous config vars 56 | err = c.Parse(GOOD_CONFIG_PATH) 57 | if err == nil || !strings.HasSuffix(err.Error(), " is not a valid config setting") { 58 | t.Error(err) 59 | } 60 | } 61 | 62 | func testGoodParse(t *testing.T, c *ConfigSet) { 63 | boolSetting := c.Bool("my_bool", false) 64 | intSetting := c.Int("my_int", 0) 65 | int64Setting := c.Int64("my_bigint", 0) 66 | uintSetting := c.Uint("my_uint", 0) 67 | uint64Setting := c.Uint64("my_biguint", 0) 68 | stringSetting := c.String("my_string", "nope") 69 | float64Setting := c.Float64("my_bigfloat", 0) 70 | nestedSetting := c.String("section.name", "") 71 | deepNestedSetting := c.String("places.california.name", "") 72 | 73 | err := c.Parse(GOOD_CONFIG_PATH) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | 78 | if *boolSetting != true { 79 | t.Error("bool setting should be true, is", *boolSetting) 80 | } 81 | if *intSetting != 22 { 82 | t.Error("int setting should be 22, is", *intSetting) 83 | } 84 | if *int64Setting != int64(-23) { 85 | t.Error("int64 setting should be -23, is", *int64Setting) 86 | } 87 | if *uintSetting != 24 { 88 | t.Error("uint setting should be 24, is", *uintSetting) 89 | } 90 | if *uint64Setting != uint64(25) { 91 | t.Error("uint64 setting should be 25, is", *uint64Setting) 92 | } 93 | if *stringSetting != "ok" { 94 | t.Error("string setting should be \"ok\", is", *stringSetting) 95 | } 96 | if *float64Setting != float64(26.1) { 97 | t.Error("float64 setting should be 26.1, is", *float64Setting) 98 | } 99 | if *nestedSetting != "cool dude" { 100 | t.Error("nested setting should be \"cool dude\", is", *nestedSetting) 101 | } 102 | if *deepNestedSetting != "neat dude" { 103 | t.Error("deepNested setting should be \"neat dude\", is", *deepNestedSetting) 104 | } 105 | } 106 | 107 | func TestParse(t *testing.T) { 108 | testBadParse(t, globalConfig) 109 | testBadParse(t, NewConfigSet("App Config", flag.ExitOnError)) 110 | testGoodParse(t, globalConfig) 111 | testGoodParse(t, NewConfigSet("App Config", flag.ExitOnError)) 112 | } 113 | -------------------------------------------------------------------------------- /examples/good.conf: -------------------------------------------------------------------------------- 1 | # Global vars 2 | my_bool = true 3 | my_int = 22 4 | my_bigint = -23 5 | my_uint = 24 6 | my_biguint = 25 7 | my_string = "ok" 8 | my_bigfloat = 26.1 9 | 10 | # A config section 11 | [section] 12 | name = "cool dude" 13 | 14 | # A deep section 15 | [places.california] 16 | name = "neat dude" 17 | 18 | -------------------------------------------------------------------------------- /examples/invalid.conf: -------------------------------------------------------------------------------- 1 | broken :( 2 | -------------------------------------------------------------------------------- /examples/simple.conf: -------------------------------------------------------------------------------- 1 | cool = true 2 | [neat.terrific] 3 | rad = "almost" 4 | --------------------------------------------------------------------------------