├── testdata ├── emptydir │ └── .keep ├── test_dump_file.yaml ├── json_error.json ├── subdir │ ├── task.json │ └── subdata.json ├── ini_other.ini ├── toml_other.toml ├── yml_other.yml ├── json-decode-example.txt ├── yaml-v3-decode-example.txt ├── yaml-decode-example.txt ├── ini_base.other ├── ini_base.ini ├── hcl2_base.hcl ├── json_other.json ├── issues59.ini ├── issues_139.conf ├── config.bak.json ├── yml_base.yml ├── hcl_example.conf ├── json_base.json5 ├── hcl_base.hcl ├── json_base.json ├── hcl2_example.hcl └── toml_base.toml ├── TODO.md ├── .gitignore ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ └── bug_report.md ├── changelog.yml ├── workflows │ ├── lint.yml │ ├── go.yml │ ├── release.yml │ └── codeql.yml └── copilot-instructions.md ├── other ├── other.go └── other_test.go ├── yaml ├── yaml.go └── yaml_test.go ├── ini ├── ini.go └── ini_test.go ├── yamlv3 ├── yamlv3.go └── yamlv3_test.go ├── properties ├── properties.go └── properties_test.go ├── go.mod ├── json ├── json.go └── json_test.go ├── toml ├── toml.go └── toml_test.go ├── json5 ├── json5.go └── json5_test.go ├── LICENSE ├── _examples ├── toml.go ├── ini.go ├── json.go ├── yaml.go ├── yamlv2.go └── watch_file.go ├── write.go ├── go.sum ├── util.go ├── driver.go ├── write_test.go ├── export.go ├── options.go ├── config.go ├── export_test.go ├── load_test.go ├── read_test.go ├── config_test.go ├── load.go ├── README.zh-CN.md ├── read.go └── README.md /testdata/emptydir/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/test_dump_file.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/json_error.json: -------------------------------------------------------------------------------- 1 | "invalid" 2 | -------------------------------------------------------------------------------- /testdata/subdir/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "key01": "value in task.json", 3 | "key02": 250 4 | } -------------------------------------------------------------------------------- /testdata/subdir/subdata.json: -------------------------------------------------------------------------------- 1 | { 2 | "key01": "value in sub data", 3 | "key02": 230 4 | } -------------------------------------------------------------------------------- /testdata/ini_other.ini: -------------------------------------------------------------------------------- 1 | name = app2 2 | debug = false 3 | age = 12 4 | baseKey = value2 5 | 6 | [map1] 7 | key = val2 8 | key2 = val20 9 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - remote `etcd` `consul` 4 | - watch changed config files and reload 5 | - [x] set default value on binding struct. use tag `defalut` 6 | -------------------------------------------------------------------------------- /testdata/toml_other.toml: -------------------------------------------------------------------------------- 1 | name = "app2" 2 | age = 25 3 | Cats = [ "Cauchy", "Plato" ] 4 | Pi = 3.14 5 | Perfection = [ 6, 28, 496, 8128 ] 6 | DOB = 1987-07-05T05:45:00Z 7 | -------------------------------------------------------------------------------- /testdata/yml_other.yml: -------------------------------------------------------------------------------- 1 | name: app2 2 | debug: false 3 | age: 12 4 | baseKey: value2 5 | 6 | map1: 7 | key: val2 8 | key2: val20 9 | 10 | arr1: 11 | - val1 12 | - val21 13 | -------------------------------------------------------------------------------- /testdata/json-decode-example.txt: -------------------------------------------------------------------------------- 1 | map[string]interface {} { 2 | "lang": map[string]interface {} { 3 | "allowed": map[string]interface {} { 4 | "en": string("ddd"), 5 | }, 6 | }, 7 | }, 8 | -------------------------------------------------------------------------------- /testdata/yaml-v3-decode-example.txt: -------------------------------------------------------------------------------- 1 | map[string]interface {} { 2 | "lang": map[string]interface {} { 3 | "allowed": map[string]interface {} { 4 | "en": string("ddd"), 5 | }, 6 | }, 7 | }, 8 | -------------------------------------------------------------------------------- /testdata/yaml-decode-example.txt: -------------------------------------------------------------------------------- 1 | map[string]interface {} { 2 | "lang": map[interface {}]interface {} { 3 | "allowed": map[interface {}]interface {} { 4 | "en": string("666"), 5 | }, 6 | }, 7 | }, 8 | -------------------------------------------------------------------------------- /testdata/ini_base.other: -------------------------------------------------------------------------------- 1 | name = app 2 | debug = false 3 | baseKey = value 4 | age = 123 5 | envKey = ${SHELL} 6 | envKey1 = ${NotExist|defValue} 7 | 8 | [map1] 9 | key = val 10 | key1 = val1 11 | key2 = val2 12 | -------------------------------------------------------------------------------- /testdata/ini_base.ini: -------------------------------------------------------------------------------- 1 | name = app 2 | debug = false 3 | baseKey = value 4 | age = 123 5 | envKey = ${SHELL} 6 | envKey1 = ${NotExist|defValue} 7 | multiWords = hello world 8 | 9 | [map1] 10 | key = val 11 | key1 = val1 12 | key2 = val2 13 | -------------------------------------------------------------------------------- /testdata/hcl2_base.hcl: -------------------------------------------------------------------------------- 1 | io_mode = "async" 2 | pkg_name = "config" 3 | 4 | service "http" { 5 | listen_addr = "127.0.0.1:9999" 6 | 7 | process "main" { 8 | command = [ 9 | "/usr/local/bin/awesome-app", 10 | "server"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /testdata/json_other.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app2", 3 | "debug": false, 4 | "age": 12, 5 | "baseKey": "value2", 6 | "map1": { 7 | "key": "val2", 8 | "key2": "val20" 9 | }, 10 | "arr1": [ 11 | "val1", 12 | "val21" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /testdata/issues59.ini: -------------------------------------------------------------------------------- 1 | ; exported at 2025-08-20 14:11:56 2 | 3 | age = 123 4 | baseKey = value 5 | debug = false 6 | envKey = ${SHELL} 7 | envKey1 = ${NotExist|defValue} 8 | multiWords = hello world 9 | name = app 10 | 11 | [map1] 12 | key1 = val1 13 | key2 = val2 14 | key = val 15 | 16 | 17 | -------------------------------------------------------------------------------- /testdata/issues_139.conf: -------------------------------------------------------------------------------- 1 | ; exported at 2023-06-11 13:30:01 2 | 3 | age = 123 4 | baseKey = value 5 | debug = false 6 | envKey = ${SHELL} 7 | envKey1 = ${NotExist|defValue} 8 | multiWords = hello world 9 | name = app 10 | 11 | [map1] 12 | key1 = val1 13 | key2 = val2 14 | key = val 15 | 16 | 17 | -------------------------------------------------------------------------------- /testdata/config.bak.json: -------------------------------------------------------------------------------- 1 | {"age":123,"arr1":["val","val1","val2"],"baseKey":"value","debug":true,"envKey":"${SHELL}","envKey1":"${NotExist|defValue}","invalidEnvKey":"${noClose","map1":{"key":"val","key1":"val1","key2":"val2","key3":"${SHELL}","key4":"230"},"name":"app","new-key":"new-value","tagsStr":"php,go"} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.swp 3 | .idea 4 | *.patch 5 | ### Go template 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, build with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | .DS_Store 19 | vendor 20 | #go.sum -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: gomod 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | open-pull-requests-limit: 10 9 | 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | # Check for updates to GitHub Actions every weekday 14 | interval: "daily" 15 | -------------------------------------------------------------------------------- /testdata/yml_base.yml: -------------------------------------------------------------------------------- 1 | name: app 2 | debug: false 3 | baseKey: value 4 | age: 123 5 | envKey: ${SHELL} 6 | envKey1: ${NotExist|defValue} 7 | 8 | map1: 9 | key: val 10 | key1: val1 11 | key2: val2 12 | 13 | arr1: 14 | - val 15 | - val1 16 | - val2 17 | 18 | lang: 19 | dir: res/lang 20 | defLang: en 21 | allowed: 22 | en: val 23 | zh-CN: val2 24 | -------------------------------------------------------------------------------- /testdata/hcl_example.conf: -------------------------------------------------------------------------------- 1 | app { 2 | io_mode = "async" 3 | 4 | service "http" { 5 | listen_addr = "127.0.0.1:8080" 6 | listen_addr2 = "127.0.0.1:8090" 7 | 8 | process "main" { 9 | command = ["/usr/local/bin/awesome-app", "server"] 10 | } 11 | 12 | process "mgmt" { 13 | command = ["/usr/local/bin/awesome-app", "mgmt"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /other/other.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package other is an example of a custom driver 3 | */ 4 | package other 5 | 6 | import ( 7 | "github.com/gookit/config/v2" 8 | "github.com/gookit/config/v2/ini" 9 | ) 10 | 11 | // DriverName string 12 | const DriverName = "other" 13 | 14 | var ( 15 | // Encoder is the encoder for this driver 16 | Encoder = ini.Encoder 17 | // Decoder is the decoder for this driver 18 | Decoder = ini.Decoder 19 | // Driver is the exported symbol 20 | Driver = config.NewDriver(DriverName, Decoder, Encoder) 21 | ) 22 | -------------------------------------------------------------------------------- /yaml/yaml.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package yaml is a driver use YAML format content as config source 3 | 4 | Usage please see example: 5 | */ 6 | package yaml 7 | 8 | import ( 9 | "github.com/goccy/go-yaml" 10 | "github.com/gookit/config/v2" 11 | ) 12 | 13 | // Decoder the yaml content decoder 14 | var Decoder config.Decoder = yaml.Unmarshal 15 | 16 | // Encoder the yaml content encoder 17 | var Encoder config.Encoder = yaml.Marshal 18 | 19 | // Driver for yaml 20 | var Driver = config.NewDriver(config.Yaml, Decoder, Encoder).WithAliases(config.Yml) 21 | -------------------------------------------------------------------------------- /testdata/json_base.json5: -------------------------------------------------------------------------------- 1 | { 2 | name: "app", 3 | "debug": false, 4 | "baseKey": "value", 5 | "age": 123, // comments 6 | "envKey": "${SHELL}", 7 | "envKey1": "${NotExist|defValue}", 8 | "map1": { 9 | "key": "val", 10 | "key1": "val1", 11 | "key2": "val2" 12 | }, 13 | arr1: [ 14 | "val", 15 | "val1", 16 | "val2" 17 | ], 18 | "lang": { 19 | "dir": "res/lang", 20 | "defLang": "en", 21 | "allowed": { 22 | "en": "val", 23 | "zh-CN": "val2", // for comment 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /testdata/hcl_base.hcl: -------------------------------------------------------------------------------- 1 | 2 | job "binstore-storagelocker" { 3 | group "binsl" { 4 | task "binstore" { 5 | driver = "docker" 6 | 7 | artifact { 8 | source = "http://foo.com/bar" 9 | destination = "" 10 | 11 | options { 12 | foo = "bar" 13 | } 14 | } 15 | 16 | artifact { 17 | source = "http://foo.com/baz" 18 | } 19 | 20 | artifact { 21 | source = "http://foo.com/bam" 22 | destination = "var/foo" 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ini/ini.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package ini is driver use INI format content as config source 3 | 4 | about ini parse, please see https://github.com/gookit/ini/parser 5 | */ 6 | package ini 7 | 8 | import ( 9 | "github.com/gookit/config/v2" 10 | "github.com/gookit/ini/v2/parser" 11 | ) 12 | 13 | // Decoder the ini content decoder 14 | var Decoder config.Decoder = parser.Decode 15 | 16 | // Encoder encode data to ini content 17 | var Encoder config.Encoder = parser.Encode 18 | 19 | // Driver for ini 20 | var Driver = config.NewDriver(config.Ini, Decoder, Encoder) 21 | -------------------------------------------------------------------------------- /testdata/json_base.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "debug": false, 4 | "baseKey": "value", 5 | "key_in_json": "hello jsond", 6 | "age": 123, 7 | "envKey": "${SHELL}", 8 | "envKey1": "${NotExist|defValue}", 9 | "map1": { 10 | "key": "val", 11 | "key1": "val1", 12 | "key2": "val2" 13 | }, 14 | "arr1": [ 15 | "val", 16 | "val1", 17 | "val2" 18 | ], 19 | "lang": { 20 | "dir": "res/lang", 21 | "defLang": "en", 22 | "allowed": { 23 | "en": "val", 24 | "zh-CN": "val2" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /yamlv3/yamlv3.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package yamlv3 is a driver use YAML format content as config source 3 | 4 | Usage please see example. 5 | 6 | Deprecated: use ../yaml instead, yamlv3 package is deprecated 7 | */ 8 | package yamlv3 9 | 10 | import ( 11 | "github.com/goccy/go-yaml" 12 | "github.com/gookit/config/v2" 13 | ) 14 | 15 | // Decoder the yaml content decoder 16 | var Decoder config.Decoder = yaml.Unmarshal 17 | 18 | // Encoder the yaml content encoder 19 | var Encoder config.Encoder = yaml.Marshal 20 | 21 | // Driver for yaml 22 | var Driver = config.NewDriver(config.Yaml, Decoder, Encoder).WithAliases(config.Yml) 23 | -------------------------------------------------------------------------------- /properties/properties.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package properties is a driver use Java properties format content as config source 3 | 4 | Usage please see readme. 5 | 6 | */ 7 | package properties 8 | 9 | import ( 10 | "github.com/gookit/config/v2" 11 | "github.com/gookit/properties" 12 | ) 13 | 14 | // Name string 15 | const Name = "properties" 16 | 17 | var ( 18 | // Decoder the properties content decoder 19 | Decoder config.Decoder = properties.Decode 20 | 21 | // Encoder the properties content encoder 22 | Encoder config.Encoder = properties.Encode 23 | 24 | // Driver for properties 25 | Driver = config.NewDriver(Name, Decoder, Encoder) 26 | ) 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gookit/config/v2 2 | 3 | go 1.21.0 4 | 5 | toolchain go1.21.1 6 | 7 | require ( 8 | dario.cat/mergo v1.0.2 9 | github.com/BurntSushi/toml v1.5.0 10 | github.com/go-viper/mapstructure/v2 v2.4.0 11 | github.com/goccy/go-json v0.10.5 12 | github.com/goccy/go-yaml v1.18.0 13 | github.com/gookit/goutil v0.7.1 14 | github.com/gookit/ini/v2 v2.3.2 15 | github.com/gookit/properties v0.4.1 16 | github.com/titanous/json5 v1.0.0 17 | ) 18 | 19 | require ( 20 | golang.org/x/sync v0.11.0 // indirect 21 | golang.org/x/sys v0.30.0 // indirect 22 | golang.org/x/term v0.29.0 // indirect 23 | golang.org/x/text v0.22.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /json/json.go: -------------------------------------------------------------------------------- 1 | // Package json use the https://github.com/json-iterator/go for parse json 2 | package json 3 | 4 | import ( 5 | "github.com/goccy/go-json" 6 | "github.com/gookit/config/v2" 7 | "github.com/gookit/goutil/jsonutil" 8 | ) 9 | 10 | var ( 11 | // Decoder for json 12 | Decoder config.Decoder = func(data []byte, v any) (err error) { 13 | if config.JSONAllowComments { 14 | str := jsonutil.StripComments(string(data)) 15 | return json.Unmarshal([]byte(str), v) 16 | } 17 | return json.Unmarshal(data, v) 18 | } 19 | 20 | // Encoder for json 21 | Encoder config.Encoder = json.Marshal 22 | // Driver for json 23 | Driver = config.NewDriver(config.JSON, Decoder, Encoder) 24 | ) 25 | -------------------------------------------------------------------------------- /properties/properties_test.go: -------------------------------------------------------------------------------- 1 | package properties_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gookit/config/v2" 7 | "github.com/gookit/config/v2/properties" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestDriver(t *testing.T) { 12 | is := assert.New(t) 13 | is.Eq(properties.Name, properties.Driver.Name()) 14 | 15 | c := config.NewEmpty("test") 16 | is.False(c.HasDecoder(properties.Name)) 17 | c.AddDriver(properties.Driver) 18 | 19 | is.True(c.HasDecoder(properties.Name)) 20 | is.True(c.HasEncoder(properties.Name)) 21 | 22 | m := struct { 23 | N string 24 | }{} 25 | err := properties.Decoder([]byte(` 26 | // comments 27 | n=value 28 | `), &m) 29 | 30 | is.Nil(err) 31 | is.Eq("value", m.N) 32 | } 33 | -------------------------------------------------------------------------------- /testdata/hcl2_example.hcl: -------------------------------------------------------------------------------- 1 | io_mode = "async" 2 | 3 | service "http" { 4 | listen_addr = "127.0.0.1:8080" 5 | 6 | process "main" { 7 | command = ["/usr/local/bin/awesome-app", "server"] 8 | } 9 | 10 | process "mgmt" { 11 | command = ["/usr/local/bin/awesome-app", "mgmt"] 12 | } 13 | } 14 | 15 | /* 16 | output like: 17 | { 18 | "io_mode": "async", 19 | "service": { 20 | "http": { 21 | "web_proxy": { 22 | "listen_addr": "127.0.0.1:8080", 23 | "process": { 24 | "main": { 25 | "command": ["/usr/local/bin/awesome-app", "server"] 26 | }, 27 | "mgmt": { 28 | "command": ["/usr/local/bin/awesome-app", "mgmt"] 29 | }, 30 | } 31 | } 32 | } 33 | } 34 | } 35 | */ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: inhere 7 | 8 | --- 9 | 10 | **System (please complete the following information):** 11 | 12 | - OS: `linux` [e.g. linux, macOS] 13 | - GO Version: `1.13` [e.g. `1.13`] 14 | - Pkg Version: `1.1.1` [e.g. `1.1.1`] 15 | 16 | **Describe the bug** 17 | 18 | A clear and concise description of what the bug is. 19 | 20 | **To Reproduce** 21 | 22 | ```go 23 | // go code 24 | ``` 25 | 26 | **Expected behavior** 27 | 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Screenshots** 31 | 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Additional context** 35 | 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /toml/toml.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package toml is driver use TOML format content as config source. 3 | How to usage please see README and unit tests. 4 | */ 5 | package toml 6 | 7 | // see https://godoc.org/github.com/BurntSushi/toml 8 | import ( 9 | "bytes" 10 | 11 | "github.com/BurntSushi/toml" 12 | "github.com/gookit/config/v2" 13 | ) 14 | 15 | // Decoder the toml content decoder 16 | var Decoder config.Decoder = func(blob []byte, ptr any) (err error) { 17 | _, err = toml.Decode(string(blob), ptr) 18 | return 19 | } 20 | 21 | // Encoder the toml content encoder 22 | var Encoder config.Encoder = func(ptr any) (out []byte, err error) { 23 | buf := new(bytes.Buffer) 24 | err = toml.NewEncoder(buf).Encode(ptr) 25 | return buf.Bytes(), err 26 | } 27 | 28 | // Driver for toml format 29 | var Driver = config.NewDriver(config.Toml, Decoder, Encoder) 30 | -------------------------------------------------------------------------------- /json5/json5.go: -------------------------------------------------------------------------------- 1 | // Package json5 support for parse and load json5 2 | package json5 3 | 4 | import ( 5 | "encoding/json" 6 | 7 | "github.com/gookit/config/v2" 8 | "github.com/titanous/json5" 9 | ) 10 | 11 | // Name for driver 12 | const Name = "json5" 13 | 14 | // NAME for driver 15 | const NAME = Name 16 | 17 | // JSONMarshalIndent if not empty, will use json.MarshalIndent for encode data. 18 | var JSONMarshalIndent string 19 | 20 | var ( 21 | // Decoder for json 22 | Decoder config.Decoder = json5.Unmarshal 23 | 24 | // Encoder for json5 25 | Encoder config.Encoder = func(v any) (out []byte, err error) { 26 | if len(JSONMarshalIndent) == 0 { 27 | return json.Marshal(v) 28 | } 29 | return json.MarshalIndent(v, "", JSONMarshalIndent) 30 | } 31 | 32 | // Driver for json5 33 | Driver = config.NewDriver(Name, Decoder, Encoder) 34 | ) 35 | -------------------------------------------------------------------------------- /testdata/toml_base.toml: -------------------------------------------------------------------------------- 1 | # This is a TOML document. Boom. 2 | 3 | title = "TOML Example" 4 | name = "app" 5 | 6 | envKey = "${SHELL}" 7 | envKey1 = "${NotExist|defValue}" 8 | 9 | arr1 = [ 10 | "alpha", 11 | "omega" 12 | ] 13 | 14 | [map1] 15 | name = "inhere" 16 | org = "GitHub" 17 | 18 | [owner] 19 | name = "inhere" 20 | organization = "GitHub" 21 | bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." 22 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 23 | 24 | [database] 25 | server = "192.168.1.1" 26 | ports = [ 8001, 8001, 8002 ] 27 | connection_max = 5000 28 | enabled = true 29 | 30 | [servers] 31 | 32 | # You can indent as you please. Tabs or spaces. TOML don't care. 33 | [servers.alpha] 34 | ip = "10.0.0.1" 35 | dc = "eqdc10" 36 | 37 | [servers.beta] 38 | ip = "10.0.0.2" 39 | dc = "eqdc10" 40 | 41 | [clients] 42 | data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it 43 | 44 | # Line breaks are OK when inside arrays 45 | hosts = [ 46 | "alpha", 47 | "omega" 48 | ] 49 | -------------------------------------------------------------------------------- /.github/changelog.yml: -------------------------------------------------------------------------------- 1 | title: '## Change Log' 2 | # style allow: simple, markdown(mkdown), ghr(gh-release) 3 | style: gh-release 4 | # group names 5 | names: [Refactor, Fixed, Feature, Update, Other] 6 | # if empty will auto fetch by git remote 7 | #repo_url: https://github.com/gookit/goutil 8 | 9 | filters: 10 | # message length should >= 12 11 | - name: msg_len 12 | min_len: 12 13 | # message words should >= 3 14 | - name: words_len 15 | min_len: 3 16 | - name: keyword 17 | keyword: format code 18 | exclude: true 19 | - name: keywords 20 | keywords: format code, action test 21 | exclude: true 22 | 23 | # group match rules 24 | # not matched will use 'Other' group. 25 | rules: 26 | - name: Refactor 27 | start_withs: [refactor, break] 28 | contains: ['refactor:'] 29 | - name: Fixed 30 | start_withs: [fix] 31 | contains: ['fix:'] 32 | - name: Feature 33 | start_withs: [feat, new] 34 | contains: [feature, 'feat:'] 35 | - name: Update 36 | start_withs: [up] 37 | contains: ['update:', 'up:'] 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 inhere 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: CodeLinter 2 | on: 3 | pull_request: 4 | paths: 5 | - 'go.mod' 6 | - '**.go' 7 | - '**.yml' 8 | push: 9 | paths: 10 | - '**.go' 11 | - 'go.mod' 12 | - '**.yml' 13 | 14 | jobs: 15 | 16 | test: 17 | name: Code linter 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Check out code 22 | uses: actions/checkout@v6 23 | 24 | - name: Setup Go Faster 25 | uses: WillAbides/setup-go-faster@v1.14.0 26 | timeout-minutes: 3 27 | with: 28 | go-version: "*" 29 | 30 | - name: Revive lint check 31 | uses: docker://morphy/revive-action:v2 32 | with: 33 | # Exclude patterns, separated by semicolons (optional) 34 | exclude: "./_examples/...;./testdata/..." 35 | 36 | - name: Run static check 37 | uses: reviewdog/action-staticcheck@v1 38 | if: ${{ github.event_name == 'pull_request'}} 39 | with: 40 | github_token: ${{ secrets.github_token }} 41 | # Change reviewdog reporter if you need [github-pr-check,github-check,github-pr-review]. 42 | reporter: github-pr-check 43 | # Report all results. [added,diff_context,file,nofilter]. 44 | filter_mode: added 45 | # Exit with 1 when it find at least one finding. 46 | fail_on_error: true 47 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Unit-Tests 2 | on: 3 | pull_request: 4 | paths: 5 | - 'go.mod' 6 | - '**.go' 7 | - '**.yml' 8 | push: 9 | paths: 10 | - '**.go' 11 | - 'go.mod' 12 | - '**.yml' 13 | 14 | jobs: 15 | 16 | test: 17 | name: Test on go ${{ matrix.go_version }} 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | go_version: [1.22, 1.23, 1.24, stable] 22 | 23 | steps: 24 | - name: Check out code 25 | uses: actions/checkout@v6 26 | 27 | - name: Setup Go Faster 28 | uses: WillAbides/setup-go-faster@v1.14.0 29 | timeout-minutes: 3 30 | with: 31 | go-version: ${{ matrix.go_version }} 32 | 33 | - name: Run unit tests 34 | # run: go test -v -cover ./... 35 | # must add " for profile.cov on windows OS 36 | run: go test -coverprofile="profile.cov" ./... 37 | 38 | - name: Send coverage 39 | uses: shogo82148/actions-goveralls@v1 40 | with: 41 | path-to-profile: profile.cov 42 | flag-name: Go-${{ matrix.go_version }} 43 | parallel: true 44 | 45 | # notifies that all test jobs are finished. 46 | # https://github.com/shogo82148/actions-goveralls 47 | finish: 48 | needs: test 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: shogo82148/actions-goveralls@v1 52 | with: 53 | parallel-finished: true -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Tag-release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | name: Release new version 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v6 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup ENV 21 | # https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable 22 | run: | 23 | echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV 24 | echo "RELEASE_NAME=$GITHUB_WORKFLOW" >> $GITHUB_ENV 25 | 26 | - name: Generate changelog 27 | run: | 28 | curl https://github.com/gookit/gitw/releases/latest/download/chlog-linux-amd64 -L -o /usr/local/bin/chlog 29 | chmod a+x /usr/local/bin/chlog 30 | chlog -c .github/changelog.yml -o changelog.md prev last 31 | 32 | # https://github.com/softprops/action-gh-release 33 | - name: Create release and upload assets 34 | uses: softprops/action-gh-release@v2 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | with: 38 | name: ${{ env.RELEASE_TAG }} 39 | tag_name: ${{ env.RELEASE_TAG }} 40 | body_path: changelog.md 41 | token: ${{ secrets.GITHUB_TOKEN }} 42 | # files: macos-chlog.exe 43 | -------------------------------------------------------------------------------- /_examples/toml.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gookit/config/v2" 7 | "github.com/gookit/config/v2/toml" 8 | ) 9 | 10 | // go run ./examples/toml.go 11 | func main() { 12 | config.WithOptions(config.ParseEnv) 13 | 14 | // add Decoder and Encoder 15 | config.AddDriver(toml.Driver) 16 | 17 | err := config.LoadFiles("testdata/toml_base.toml") 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | fmt.Printf("config data: \n %#v\n", config.Data()) 23 | 24 | err = config.LoadFiles("testdata/toml_other.toml") 25 | // config.LoadFiles("testdata/toml_base.toml", "testdata/toml_other.toml") 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | fmt.Printf("config data: \n %#v\n", config.Data()) 31 | fmt.Print("get config example:\n") 32 | 33 | name := config.String("name") 34 | fmt.Printf("- get string\n val: %v\n", name) 35 | 36 | arr1 := config.Strings("arr1") 37 | fmt.Printf("- get array\n val: %#v\n", arr1) 38 | 39 | val0 := config.String("arr1.0") 40 | fmt.Printf("- get sub-value by path 'arr.index'\n val: %v\n", val0) 41 | 42 | map1 := config.StringMap("map1") 43 | fmt.Printf("- get map\n val: %#v\n", map1) 44 | 45 | val0 = config.String("map1.name") 46 | fmt.Printf("- get sub-value by path 'map.key'\n val: %v\n", val0) 47 | 48 | // can parse env name(ParseEnv: true) 49 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 50 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 51 | 52 | } 53 | -------------------------------------------------------------------------------- /other/other_test.go: -------------------------------------------------------------------------------- 1 | package other 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gookit/config/v2" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestOtherDriver(t *testing.T) { 12 | is := assert.New(t) 13 | 14 | is.Eq("other", Driver.Name()) 15 | 16 | c := config.NewEmpty("test") 17 | is.False(c.HasDecoder("other")) 18 | 19 | c.AddDriver(Driver) 20 | is.True(c.HasDecoder("other")) 21 | is.True(c.HasEncoder("other")) 22 | 23 | _, err := Encoder(map[string]any{"k": "v"}) 24 | is.Nil(err) 25 | 26 | _, err = Encoder("invalid") 27 | is.Err(err) 28 | } 29 | 30 | func TestOtherLoader(t *testing.T) { 31 | config.AddDriver(Driver) 32 | 33 | err := config.LoadFiles("../testdata/ini_base.other") 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | fmt.Printf("get config example:\n") 39 | 40 | name := config.String("name") 41 | fmt.Printf("get string\n - val: %v\n", name) 42 | 43 | map1 := config.StringMap("map1") 44 | fmt.Printf("get map\n - val: %#v\n", map1) 45 | 46 | val0 := config.String("map1.key") 47 | fmt.Printf("get sub-value by path 'map.key'\n - val: %v\n", val0) 48 | 49 | // can parse env name(ParseEnv: true) 50 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 51 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 52 | 53 | // set value 54 | _ = config.Set("name", "new name") 55 | name = config.String("name") 56 | fmt.Printf("set string\n - val: %v\n", name) 57 | 58 | } 59 | -------------------------------------------------------------------------------- /_examples/ini.go: -------------------------------------------------------------------------------- 1 | // These are some sample code for YAML,TOML,JSON,INI,HCL 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/gookit/config/v2" 8 | "github.com/gookit/config/v2/ini" 9 | ) 10 | 11 | // go run ./examples/ini.go 12 | func main() { 13 | config.WithOptions(config.ParseEnv) 14 | 15 | // add Decoder and Encoder 16 | config.AddDriver(ini.Driver) 17 | // Or 18 | // config.SetEncoder(config.Ini, ini.Encoder) 19 | 20 | err := config.LoadFiles("testdata/ini_base.ini") 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | fmt.Printf("config data: \n %#v\n", config.Data()) 26 | 27 | err = config.LoadFiles("testdata/ini_other.ini") 28 | // config.LoadFiles("testdata/ini_base.ini", "testdata/ini_other.ini") 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | fmt.Printf("config data: \n %#v\n", config.Data()) 34 | fmt.Print("get config example:\n") 35 | 36 | name := config.String("name") 37 | fmt.Printf("- get string\n val: %v\n", name) 38 | 39 | // NOTICE: ini is not support array 40 | 41 | map1 := config.StringMap("map1") 42 | fmt.Printf("- get map\n val: %#v\n", map1) 43 | 44 | val0 := config.String("map1.key") 45 | fmt.Printf("- get sub-value by path 'map.key'\n val: %v\n", val0) 46 | 47 | // can parse env name(ParseEnv: true) 48 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 49 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 50 | 51 | // set value 52 | _ = config.Set("name", "new name") 53 | name = config.String("name") 54 | fmt.Printf("- set string\n val: %v\n", name) 55 | 56 | } 57 | -------------------------------------------------------------------------------- /write.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/gookit/goutil/maputil" 8 | ) 9 | 10 | // some common errors definitions 11 | var ( 12 | ErrReadonly = errors.New("the config instance in 'readonly' mode") 13 | ErrKeyIsEmpty = errors.New("the config key is cannot be empty") 14 | ErrNotFound = errors.New("this key does not exist in the config") 15 | ) 16 | 17 | // SetData for override the Config.Data 18 | func SetData(data map[string]any) { 19 | dc.SetData(data) 20 | } 21 | 22 | // SetData for override the Config.Data 23 | func (c *Config) SetData(data map[string]any) { 24 | c.lock.Lock() 25 | c.data = data 26 | c.lock.Unlock() 27 | 28 | c.fireHook(OnSetData) 29 | } 30 | 31 | // Set value by key. setByPath default is true 32 | func Set(key string, val any, setByPath ...bool) error { 33 | return dc.Set(key, val, setByPath...) 34 | } 35 | 36 | // Set a value by key string. setByPath default is true 37 | func (c *Config) Set(key string, val any, setByPath ...bool) (err error) { 38 | if c.opts.Readonly { 39 | return ErrReadonly 40 | } 41 | 42 | c.lock.Lock() 43 | defer c.lock.Unlock() 44 | 45 | sep := c.opts.Delimiter 46 | if key = formatKey(key, string(sep)); key == "" { 47 | return ErrKeyIsEmpty 48 | } 49 | 50 | defer c.fireHook(OnSetValue) 51 | if strings.IndexByte(key, sep) == -1 { 52 | c.data[key] = val 53 | return 54 | } 55 | 56 | // disable set by path. 57 | if len(setByPath) > 0 && !setByPath[0] { 58 | c.data[key] = val 59 | return 60 | } 61 | 62 | // set by path 63 | keys := strings.Split(key, string(sep)) 64 | return maputil.SetByKeys(&c.data, keys, val) 65 | } 66 | -------------------------------------------------------------------------------- /_examples/json.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gookit/config/v2" 7 | "github.com/gookit/config/v2/json" 8 | ) 9 | 10 | // go run ./examples/json.go 11 | func main() { 12 | config.WithOptions(config.ParseEnv) 13 | 14 | // add Decoder and Encoder 15 | config.AddDriver(json.Driver) 16 | 17 | err := config.LoadFiles("testdata/json_base.json") 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | fmt.Printf("config data: \n %#v\n", config.Data()) 23 | 24 | err = config.LoadFiles("testdata/json_other.json") 25 | // config.LoadFiles("testdata/json_base.json", "testdata/json_other.json") 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | fmt.Printf("config data: \n %#v\n", config.Data()) 31 | fmt.Print("get config example:\n") 32 | 33 | name := config.String("name") 34 | fmt.Printf("get string\n val: %v\n", name) 35 | 36 | arr1 := config.Strings("arr1") 37 | fmt.Printf("get array\n val: %#v\n", arr1) 38 | 39 | val0 := config.String("arr1.0") 40 | fmt.Printf("get sub-value by path 'arr.index'\n val: %#v\n", val0) 41 | 42 | map1 := config.StringMap("map1") 43 | fmt.Printf("get map\n val: %#v\n", map1) 44 | 45 | val0 = config.String("map1.key") 46 | fmt.Printf("get sub-value by path 'map.key'\n val: %#v\n", val0) 47 | 48 | // can parse env name(ParseEnv: true) 49 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 50 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 51 | 52 | // set value 53 | _ = config.Set("name", "new name") 54 | name = config.String("name") 55 | fmt.Printf("set string\n val: %v\n", name) 56 | 57 | // if you want export config data 58 | // buf := new(bytes.Buffer) 59 | // _, err = config.DumpTo(buf, config.JSON) 60 | // if err != nil { 61 | // panic(err) 62 | // } 63 | // fmt.Printf("export config:\n%s", buf.String()) 64 | } 65 | -------------------------------------------------------------------------------- /_examples/yaml.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gookit/config/v2" 7 | "github.com/gookit/config/v2/yamlv3" 8 | ) 9 | 10 | // go run ./examples/yaml.go 11 | func main() { 12 | config.WithOptions(config.ParseEnv) 13 | 14 | // only add decoder 15 | // config.SetDecoder(config.Yaml, yamlv3.Decoder) 16 | // Or 17 | config.AddDriver(yamlv3.Driver) 18 | 19 | err := config.LoadFiles("testdata/yml_base.yml") 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | fmt.Printf("config data: \n %#v\n", config.Data()) 25 | 26 | err = config.LoadFiles("testdata/yml_other.yml") 27 | // config.LoadFiles("testdata/yml_base.yml", "testdata/yml_other.yml") 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | fmt.Printf("config data: \n %#v\n", config.Data()) 33 | fmt.Print("get config example:\n") 34 | 35 | name := config.String("name") 36 | fmt.Printf("- get string\n val: %v\n", name) 37 | 38 | arr1 := config.Strings("arr1") 39 | fmt.Printf("- get array\n val: %#v\n", arr1) 40 | 41 | val0 := config.String("arr1.0") 42 | fmt.Printf("- get sub-value by path 'arr.index'\n val: %#v\n", val0) 43 | 44 | map1 := config.StringMap("map1") 45 | fmt.Printf("- get map\n val: %#v\n", map1) 46 | 47 | val0 = config.String("map1.key") 48 | fmt.Printf("- get sub-value by path 'map.key'\n val: %#v\n", val0) 49 | 50 | // can parse env name(ParseEnv: true) 51 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 52 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 53 | 54 | // set value 55 | config.Set("name", "new name") 56 | name = config.String("name") 57 | fmt.Printf("- set string\n val: %v\n", name) 58 | 59 | // if you want export config data 60 | // buf := new(bytes.Buffer) 61 | // _, err = config.DumpTo(buf, config.Yaml) 62 | // if err != nil { 63 | // panic(err) 64 | // } 65 | // fmt.Printf("export config:\n%s", buf.String()) 66 | } 67 | -------------------------------------------------------------------------------- /_examples/yamlv2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gookit/config/v2" 7 | "github.com/gookit/config/v2/yaml" 8 | ) 9 | 10 | // go run ./examples/yamlv2.go 11 | func main() { 12 | config.WithOptions(config.ParseEnv) 13 | 14 | // only add decoder 15 | // config.SetDecoder(config.Yaml, yaml.Decoder) 16 | // Or 17 | config.AddDriver(yaml.Driver) 18 | 19 | err := config.LoadFiles("testdata/yml_base.yml") 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | fmt.Printf("config data: \n %#v\n", config.Data()) 25 | 26 | err = config.LoadFiles("testdata/yml_other.yml") 27 | // config.LoadFiles("testdata/yml_base.yml", "testdata/yml_other.yml") 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | fmt.Printf("config data: \n %#v\n", config.Data()) 33 | fmt.Print("get config example:\n") 34 | 35 | name := config.String("name") 36 | fmt.Printf("- get string\n val: %v\n", name) 37 | 38 | arr1 := config.Strings("arr1") 39 | fmt.Printf("- get array\n val: %#v\n", arr1) 40 | 41 | val0 := config.String("arr1.0") 42 | fmt.Printf("- get sub-value by path 'arr.index'\n val: %#v\n", val0) 43 | 44 | map1 := config.StringMap("map1") 45 | fmt.Printf("- get map\n val: %#v\n", map1) 46 | 47 | val0 = config.String("map1.key") 48 | fmt.Printf("- get sub-value by path 'map.key'\n val: %#v\n", val0) 49 | 50 | // can parse env name(ParseEnv: true) 51 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 52 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 53 | 54 | // set value 55 | config.Set("name", "new name") 56 | name = config.String("name") 57 | fmt.Printf("- set string\n val: %v\n", name) 58 | 59 | // if you want export config data 60 | // buf := new(bytes.Buffer) 61 | // _, err = config.DumpTo(buf, config.Yaml) 62 | // if err != nil { 63 | // panic(err) 64 | // } 65 | // fmt.Printf("export config:\n%s", buf.String()) 66 | } 67 | -------------------------------------------------------------------------------- /ini/ini_test.go: -------------------------------------------------------------------------------- 1 | package ini 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gookit/config/v2" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func Example() { 12 | config.WithOptions(config.ParseEnv) 13 | 14 | // add Decoder and Encoder 15 | config.AddDriver(Driver) 16 | // Or 17 | // config.SetEncoder(config.Ini, ini.Encoder) 18 | 19 | err := config.LoadFiles("testdata/ini_base.ini") 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | // fmt.Printf("config data: \n %#v\n", config.Data()) 25 | 26 | err = config.LoadFiles("testdata/ini_other.ini") 27 | // config.LoadFiles("testdata/ini_base.ini", "testdata/ini_other.ini") 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | // fmt.Printf("config data: \n %#v\n", config.Data()) 33 | fmt.Print("get config example:\n") 34 | 35 | name := config.String("name") 36 | fmt.Printf("get string\n - val: %v\n", name) 37 | 38 | // NOTICE: ini is not support array 39 | 40 | map1 := config.StringMap("map1") 41 | fmt.Printf("get map\n - val: %#v\n", map1) 42 | 43 | val0 := config.String("map1.key") 44 | fmt.Printf("get sub-value by path 'map.key'\n - val: %v\n", val0) 45 | 46 | // can parse env name(ParseEnv: true) 47 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 48 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 49 | 50 | // set value 51 | _ = config.Set("name", "new name") 52 | name = config.String("name") 53 | fmt.Printf("set string\n - val: %v\n", name) 54 | } 55 | 56 | func TestDriver(t *testing.T) { 57 | st := assert.New(t) 58 | 59 | st.Eq("ini", Driver.Name()) 60 | // st.IsType(new(Encoder), JSONDriver.GetEncoder()) 61 | 62 | c := config.NewEmpty("test") 63 | st.False(c.HasDecoder(config.Ini)) 64 | 65 | c.AddDriver(Driver) 66 | st.True(c.HasDecoder(config.Ini)) 67 | st.True(c.HasEncoder(config.Ini)) 68 | 69 | _, err := Encoder(map[string]any{"k": "v"}) 70 | st.Nil(err) 71 | 72 | _, err = Encoder("invalid") 73 | st.Err(err) 74 | } 75 | -------------------------------------------------------------------------------- /_examples/watch_file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/fsnotify/fsnotify" 5 | "github.com/gookit/config/v2" 6 | "github.com/gookit/config/v2/yaml" 7 | "github.com/gookit/goutil" 8 | "github.com/gookit/goutil/cliutil" 9 | ) 10 | 11 | func main() { 12 | config.AddDriver(yaml.Driver) 13 | config.WithOptions( 14 | config.ParseEnv, 15 | config.WithHookFunc(func(event string, c *config.Config) { 16 | if event == config.OnReloadData { 17 | cliutil.Cyanln("config reloaded, you can do something ....") 18 | } 19 | }), 20 | ) 21 | 22 | // load app config files 23 | err := config.LoadFiles( 24 | "testdata/json_base.json", 25 | "testdata/yml_base.yml", 26 | "testdata/yml_other.yml", 27 | ) 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | // mock server running 33 | done := make(chan bool) 34 | 35 | // watch loaded config files 36 | err = watchConfigFiles(config.Default()) 37 | goutil.PanicErr(err) 38 | 39 | cliutil.Infoln("loaded config files is watching ...") 40 | <-done 41 | } 42 | 43 | func watchConfigFiles(cfg *config.Config) error { 44 | watcher, err := fsnotify.NewWatcher() 45 | if err != nil { 46 | return err 47 | } 48 | //noinspection GoUnhandledErrorResult 49 | defer watcher.Close() 50 | 51 | // get loaded files 52 | files := cfg.LoadedFiles() 53 | if len(files) == 0 { 54 | return nil 55 | } 56 | 57 | go func() { 58 | for { 59 | select { 60 | case event, ok := <-watcher.Events: 61 | if !ok { // 'Events' channel is closed 62 | cliutil.Infoln("'Events' channel is closed ...", event) 63 | return 64 | } 65 | 66 | // if event.Op > 0 { 67 | cliutil.Infof("file event: %s\n", event) 68 | 69 | if event.Op&fsnotify.Write == fsnotify.Write { 70 | cliutil.Infof("modified file: %s\n", event.Name) 71 | 72 | err := cfg.ReloadFiles() 73 | if err != nil { 74 | cliutil.Errorf("reload config error: %s\n", err.Error()) 75 | } 76 | } 77 | // } 78 | 79 | case err, ok := <-watcher.Errors: 80 | if ok { // 'Errors' channel is not closed 81 | cliutil.Errorf("watch file error: %s\n", err.Error()) 82 | } 83 | if err != nil { 84 | cliutil.Errorf("watch file error2: %s\n", err.Error()) 85 | } 86 | return 87 | } 88 | } 89 | }() 90 | 91 | for _, path := range files { 92 | cliutil.Infof("add watch file: %s\n", path) 93 | if err := watcher.Add(path); err != nil { 94 | return err 95 | } 96 | } 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /json/json_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gookit/config/v2" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func Example() { 12 | config.WithOptions(config.ParseEnv) 13 | 14 | // add Decoder and Encoder 15 | config.AddDriver(Driver) 16 | 17 | err := config.LoadFiles("testdata/json_base.json") 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | fmt.Printf("config data: \n %#v\n", config.Data()) 23 | 24 | err = config.LoadFiles("testdata/json_other.json") 25 | // config.LoadFiles("testdata/json_base.json", "testdata/json_other.json") 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | fmt.Printf("config data: \n %#v\n", config.Data()) 31 | fmt.Print("get config example:\n") 32 | 33 | name := config.String("name") 34 | fmt.Printf("get string\n - val: %v\n", name) 35 | 36 | arr1 := config.Strings("arr1") 37 | fmt.Printf("get array\n - val: %#v\n", arr1) 38 | 39 | val0 := config.String("arr1.0") 40 | fmt.Printf("get sub-value by path 'arr.index'\n - val: %#v\n", val0) 41 | 42 | map1 := config.StringMap("map1") 43 | fmt.Printf("get map\n - val: %#v\n", map1) 44 | 45 | val0 = config.String("map1.key") 46 | fmt.Printf("get sub-value by path 'map.key'\n - val: %#v\n", val0) 47 | 48 | // can parse env name(ParseEnv: true) 49 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 50 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 51 | 52 | // set value 53 | _ = config.Set("name", "new name") 54 | name = config.String("name") 55 | fmt.Printf("set string\n - val: %v\n", name) 56 | 57 | // if you want export config data 58 | // buf := new(bytes.Buffer) 59 | // _, err = config.DumpTo(buf, config.JSON) 60 | // if err != nil { 61 | // panic(err) 62 | // } 63 | // fmt.Printf("export config:\n%s", buf.String()) 64 | } 65 | 66 | func TestDriver(t *testing.T) { 67 | is := assert.New(t) 68 | 69 | is.Eq("json", Driver.Name()) 70 | 71 | c := config.NewEmpty("test") 72 | is.False(c.HasDecoder(config.JSON)) 73 | c.AddDriver(Driver) 74 | 75 | is.True(c.HasDecoder(config.JSON)) 76 | is.True(c.HasEncoder(config.JSON)) 77 | 78 | m := struct { 79 | N string 80 | }{} 81 | err := Decoder([]byte(`{ 82 | // comments 83 | "n":"v"} 84 | `), &m) 85 | is.Nil(err) 86 | is.Eq("v", m.N) 87 | 88 | // disable clear comments 89 | old := config.JSONAllowComments 90 | config.JSONAllowComments = false 91 | err = Decoder([]byte(`{ 92 | // comments 93 | "n":"v"} 94 | `), &m) 95 | is.Err(err) 96 | 97 | config.JSONAllowComments = old 98 | } 99 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= 2 | dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 3 | github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= 4 | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 5 | github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= 6 | github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 7 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 8 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 9 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 10 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 11 | github.com/gookit/goutil v0.7.1 h1:AaFJPN9mrdeYBv8HOybri26EHGCC34WJVT7jUStGJsI= 12 | github.com/gookit/goutil v0.7.1/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU= 13 | github.com/gookit/ini/v2 v2.3.2 h1:W6tzOGE6zOLQelH2xhcH8BIBZPtnEpJgQ+J6SsAKBSw= 14 | github.com/gookit/ini/v2 v2.3.2/go.mod h1:StKSqY5niArRwYBS8Z71+iWUt5ow47qt359sS9YQLYY= 15 | github.com/gookit/properties v0.4.1 h1:Oc7F66mj0yCfcVMOe3saElwMAsfZQ4Kvb9UE7T5n4hM= 16 | github.com/gookit/properties v0.4.1/go.mod h1:719ECwXmpfspYOBFC60HOyTMs2GTVqmNgCPWXMNpO8I= 17 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 18 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 19 | github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0= 20 | github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY= 21 | github.com/titanous/json5 v1.0.0 h1:hJf8Su1d9NuI/ffpxgxQfxh/UiBFZX7bMPid0rIL/7s= 22 | github.com/titanous/json5 v1.0.0/go.mod h1:7JH1M8/LHKc6cyP5o5g3CSaRj+mBrIimTxzpvmckH8c= 23 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 24 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 25 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 26 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 27 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 28 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 29 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 30 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 31 | gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= 32 | gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= 33 | -------------------------------------------------------------------------------- /toml/toml_test.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gookit/config/v2" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | var tomlStr = ` 12 | title = "TOML Example" 13 | name = "app" 14 | 15 | envKey = "${SHELL}" 16 | envKey1 = "${NotExist|defValue}" 17 | 18 | arr1 = [ 19 | "alpha", 20 | "omega" 21 | ] 22 | 23 | [map1] 24 | name = "inhere" 25 | org = "GitHub" 26 | ` 27 | 28 | func Example() { 29 | config.WithOptions(config.ParseEnv) 30 | 31 | // add Decoder and Encoder 32 | config.AddDriver(Driver) 33 | 34 | err := config.LoadFiles("../testdata/toml_base.toml") 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | // fmt.Printf("config data: \n %#v\n", Data()) 40 | 41 | // load more files 42 | err = config.LoadFiles("../testdata/toml_other.toml") 43 | // can also 44 | // config.LoadFiles("testdata/toml_base.toml", "testdata/toml_other.toml") 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | // load from string 50 | _ = config.LoadSources(config.Toml, []byte(tomlStr)) 51 | 52 | // fmt.Printf("config data: \n %#v\n", Data()) 53 | fmt.Print("get config example:\n") 54 | 55 | name := config.String("name") 56 | fmt.Printf("get string\n - val: %v\n", name) 57 | 58 | arr1 := config.Strings("arr1") 59 | fmt.Printf("get array\n - val: %#v\n", arr1) 60 | 61 | val0 := config.String("arr1.0") 62 | fmt.Printf("get sub-value by path 'arr.index'\n - val: %v\n", val0) 63 | 64 | map1 := config.StringMap("map1") 65 | fmt.Printf("get map\n - val: %#v\n", map1) 66 | 67 | val0 = config.String("map1.name") 68 | fmt.Printf("get sub-value by path 'map.key'\n - val: %v\n", val0) 69 | 70 | // can parse env name(ParseEnv: true) 71 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 72 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 73 | 74 | // Out: 75 | // get config example: 76 | // get string 77 | // - val: app2 78 | // get array 79 | // - val: []string{"alpha", "omega"} 80 | // get sub-value by path 'arr.index' 81 | // - val: alpha 82 | // get map 83 | // - val: map[string]string{"name":"inhere", "org":"GitHub"} 84 | // get sub-value by path 'map.key' 85 | // - val: inhere 86 | // get env 'envKey' val: /bin/zsh 87 | // get env 'envKey1' val: defValue 88 | } 89 | 90 | func TestDriver(t *testing.T) { 91 | is := assert.New(t) 92 | 93 | is.Eq("toml", Driver.Name()) 94 | 95 | c := config.NewEmpty("test") 96 | is.False(c.HasDecoder(config.Toml)) 97 | 98 | c.AddDriver(Driver) 99 | is.True(c.HasDecoder(config.Toml)) 100 | is.True(c.HasEncoder(config.Toml)) 101 | 102 | tg := new(map[string]any) 103 | err := Decoder([]byte("invalid"), tg) 104 | is.Err(err) 105 | 106 | out, err := Encoder("invalid") 107 | is.Eq(`"invalid"`, string(out)) 108 | is.Nil(err) 109 | 110 | out, err = Encoder(map[string]any{"k": "v"}) 111 | is.Nil(err) 112 | is.Contains(string(out), `k = "v"`) 113 | } 114 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '40 14 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v6 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v4 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v4 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v4 73 | -------------------------------------------------------------------------------- /json5/json5_test.go: -------------------------------------------------------------------------------- 1 | package json5_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gookit/config/v2" 8 | "github.com/gookit/config/v2/json5" 9 | "github.com/gookit/goutil/testutil/assert" 10 | ) 11 | 12 | func Example() { 13 | config.WithOptions(config.ParseEnv) 14 | 15 | // add Decoder and Encoder 16 | config.AddDriver(json5.Driver) 17 | 18 | err := config.LoadFiles("testdata/json_base.json5") 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | fmt.Printf("config data: \n %#v\n", config.Data()) 24 | 25 | err = config.LoadFiles("testdata/json_other.json") 26 | // config.LoadFiles("testdata/json_base.json", "testdata/json_other.json") 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | fmt.Printf("config data: \n %#v\n", config.Data()) 32 | fmt.Print("get config example:\n") 33 | 34 | name := config.String("name") 35 | fmt.Printf("get string\n - val: %v\n", name) 36 | 37 | arr1 := config.Strings("arr1") 38 | fmt.Printf("get array\n - val: %#v\n", arr1) 39 | 40 | val0 := config.String("arr1.0") 41 | fmt.Printf("get sub-value by path 'arr.index'\n - val: %#v\n", val0) 42 | 43 | map1 := config.StringMap("map1") 44 | fmt.Printf("get map\n - val: %#v\n", map1) 45 | 46 | val0 = config.String("map1.key") 47 | fmt.Printf("get sub-value by path 'map.key'\n - val: %#v\n", val0) 48 | 49 | // can parse env name(ParseEnv: true) 50 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 51 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 52 | 53 | // set value 54 | _ = config.Set("name", "new name") 55 | name = config.String("name") 56 | fmt.Printf("set string\n - val: %v\n", name) 57 | 58 | // if you want export config data 59 | // buf := new(bytes.Buffer) 60 | // _, err = config.DumpTo(buf, json5.NAME) 61 | // if err != nil { 62 | // panic(err) 63 | // } 64 | // fmt.Printf("export config:\n%s", buf.String()) 65 | } 66 | 67 | func TestDriver(t *testing.T) { 68 | is := assert.New(t) 69 | 70 | is.Eq(json5.Name, json5.Driver.Name()) 71 | 72 | c := config.NewEmpty("test") 73 | is.False(c.HasDecoder(json5.Name)) 74 | c.AddDriver(json5.Driver) 75 | 76 | is.True(c.HasDecoder(json5.Name)) 77 | is.True(c.HasEncoder(json5.Name)) 78 | 79 | // test use 80 | m := struct { 81 | N string 82 | }{} 83 | err := json5.Decoder([]byte(`{ 84 | // comments 85 | "n":"v"} 86 | `), &m) 87 | is.Nil(err) 88 | is.Eq("v", m.N) 89 | 90 | // load file 91 | err = c.LoadFiles("../testdata/json_base.json5") 92 | is.NoErr(err) 93 | is.Eq("app", c.Get("name")) 94 | } 95 | 96 | func TestEncode2JSON5(t *testing.T) { 97 | is := assert.New(t) 98 | 99 | mp := map[string]any{ 100 | "name": "app", 101 | "age": 45, 102 | } 103 | bs, err := json5.Encoder(mp) 104 | is.NoErr(err) 105 | is.StrContains(string(bs), `"name":"app"`) 106 | 107 | json5.JSONMarshalIndent = " " 108 | bs, err = json5.Encoder(mp) 109 | is.NoErr(err) 110 | s := string(bs) 111 | is.StrContains(s, ` "name": "app"`) 112 | } 113 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/go-viper/mapstructure/v2" 9 | "github.com/gookit/goutil/envutil" 10 | "github.com/gookit/goutil/reflects" 11 | ) 12 | 13 | // ValDecodeHookFunc returns a mapstructure.DecodeHookFunc 14 | // that parse ENV var, and more custom parse 15 | func ValDecodeHookFunc(parseEnv, parseTime bool) mapstructure.DecodeHookFunc { 16 | return func(f reflect.Type, t reflect.Type, data any) (any, error) { 17 | if f.Kind() != reflect.String { 18 | return data, nil 19 | } 20 | 21 | var err error 22 | str := data.(string) 23 | if parseEnv { 24 | // https://docs.docker.com/compose/environment-variables/env-file/ 25 | str, err = envutil.ParseOrErr(str) 26 | if err != nil { 27 | return nil, err 28 | } 29 | } 30 | if len(str) < 2 { 31 | return str, nil 32 | } 33 | 34 | // feat: support parse time or duration string. eg: 10s 35 | if parseTime && str[0] >= '0' && str[0] <= '9' { 36 | return reflects.ToTimeOrDuration(str, t) 37 | } 38 | return str, nil 39 | } 40 | } 41 | 42 | // resolve format, check is alias 43 | func (c *Config) resolveFormat(f string) string { 44 | if name, ok := c.aliasMap[f]; ok { 45 | return name 46 | } 47 | return f 48 | } 49 | 50 | /************************************************************* 51 | * Deprecated methods 52 | *************************************************************/ 53 | 54 | // SetDecoder add/set a format decoder 55 | // 56 | // Deprecated: please use driver instead 57 | func SetDecoder(format string, decoder Decoder) { 58 | dc.SetDecoder(format, decoder) 59 | } 60 | 61 | // SetDecoder set decoder 62 | // 63 | // Deprecated: please use driver instead 64 | func (c *Config) SetDecoder(format string, decoder Decoder) { 65 | format = c.resolveFormat(format) 66 | c.decoders[format] = decoder 67 | } 68 | 69 | // SetDecoders set decoders 70 | // 71 | // Deprecated: please use driver instead 72 | func (c *Config) SetDecoders(decoders map[string]Decoder) { 73 | for format, decoder := range decoders { 74 | c.SetDecoder(format, decoder) 75 | } 76 | } 77 | 78 | // SetEncoder set a encoder for the format 79 | // 80 | // Deprecated: please use driver instead 81 | func SetEncoder(format string, encoder Encoder) { 82 | dc.SetEncoder(format, encoder) 83 | } 84 | 85 | // SetEncoder set a encoder for the format 86 | // 87 | // Deprecated: please use driver instead 88 | func (c *Config) SetEncoder(format string, encoder Encoder) { 89 | format = c.resolveFormat(format) 90 | c.encoders[format] = encoder 91 | } 92 | 93 | // SetEncoders set encoders 94 | // 95 | // Deprecated: please use driver instead 96 | func (c *Config) SetEncoders(encoders map[string]Encoder) { 97 | for format, encoder := range encoders { 98 | c.SetEncoder(format, encoder) 99 | } 100 | } 101 | 102 | /************************************************************* 103 | * helper methods/functions 104 | *************************************************************/ 105 | 106 | // LoadENVFiles load 107 | // func LoadENVFiles(filePaths ...string) error { 108 | // return dotenv.LoadFiles(filePaths...) 109 | // } 110 | 111 | // GetEnv get os ENV value by name 112 | func GetEnv(name string, defVal ...string) (val string) { 113 | return Getenv(name, defVal...) 114 | } 115 | 116 | // Getenv get os ENV value by name. like os.Getenv, but support default value 117 | // 118 | // Notice: 119 | // - Key is not case-sensitive when getting 120 | func Getenv(name string, defVal ...string) (val string) { 121 | if val = os.Getenv(name); val != "" { 122 | return 123 | } 124 | 125 | if len(defVal) > 0 { 126 | val = defVal[0] 127 | } 128 | return 129 | } 130 | 131 | func parseVarNameAndType(key string) (string, string, string) { 132 | var desc string 133 | typ := "string" 134 | key = strings.Trim(key, "-") 135 | 136 | // can set var type: int, uint, bool 137 | if strings.IndexByte(key, ':') > 0 { 138 | list := strings.SplitN(key, ":", 3) 139 | key, typ = list[0], list[1] 140 | if len(list) == 3 { 141 | desc = list[2] 142 | } 143 | 144 | // if type is not valid and has multi words, as desc message. 145 | if _, ok := validTypes[typ]; !ok { 146 | if desc == "" && strings.ContainsRune(typ, ' ') { 147 | desc = typ 148 | } 149 | typ = "string" 150 | } 151 | } 152 | return key, typ, desc 153 | } 154 | 155 | // format key 156 | func formatKey(key, sep string) string { 157 | return strings.Trim(strings.TrimSpace(key), sep) 158 | } 159 | -------------------------------------------------------------------------------- /yaml/yaml_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/gookit/config/v2" 9 | "github.com/gookit/goutil/testutil" 10 | "github.com/gookit/goutil/testutil/assert" 11 | ) 12 | 13 | var yamlStr = ` 14 | name: app2 15 | debug: false 16 | age: 23 17 | baseKey: value2 18 | 19 | map1: 20 | key: val2 21 | key2: val20 22 | 23 | arr1: 24 | - val1 25 | - val21 26 | ` 27 | 28 | func Example() { 29 | config.WithOptions(config.ParseEnv) 30 | 31 | // add yaml decoder 32 | // only add decoder 33 | // config.SetDecoder(config.Yaml, Decoder) 34 | // Or 35 | config.AddDriver(Driver) 36 | 37 | err := config.LoadFiles("../testdata/yml_other.yml") 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | // load from string 43 | _ = config.LoadSources(config.Yaml, []byte(yamlStr)) 44 | 45 | fmt.Print("get config example:\n") 46 | 47 | name := config.String("name") 48 | fmt.Printf("get string\n - val: %v\n", name) 49 | 50 | arr1 := config.Strings("arr1") 51 | fmt.Printf("get array\n - val: %#v\n", arr1) 52 | 53 | val0 := config.String("arr1.0") 54 | fmt.Printf("get sub-value by path 'arr.index'\n - val: %#v\n", val0) 55 | 56 | map1 := config.StringMap("map1") 57 | fmt.Printf("get map\n - val: %#v\n", map1) 58 | 59 | val0 = config.String("map1.key") 60 | fmt.Printf("get sub-value by path 'map.key'\n - val: %#v\n", val0) 61 | 62 | // can parse env name(ParseEnv: true) 63 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 64 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 65 | 66 | // Out: 67 | // get config example: 68 | // age: 23 69 | // get string 70 | // - val: app2 71 | // get array 72 | // - val: []string{"val1", "val21"} 73 | // get sub-value by path 'arr.index' 74 | // - val: "val1" 75 | // get map 76 | // val: map[string]string{"key":"val2", "key2":"val20"} 77 | // get sub-value by path 'map.key' 78 | // - val: "val2" 79 | // get env 'envKey' val: /bin/zsh 80 | // get env 'envKey1' val: defValue 81 | } 82 | 83 | func TestDumpConfig(t *testing.T) { 84 | is := assert.New(t) 85 | c := config.NewEmpty("test") 86 | // Notice: before dump please set driver encoder 87 | c.AddDriver(Driver) 88 | err := c.LoadStrings(config.Yaml, yamlStr) 89 | is.NoErr(err) 90 | 91 | buf := new(bytes.Buffer) 92 | _, err = c.DumpTo(buf, config.Yaml) 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | fmt.Printf("export config:\n%s", buf.String()) 98 | } 99 | 100 | func TestLoadFile(t *testing.T) { 101 | c := config.NewEmpty("test") 102 | c.AddDriver(Driver) 103 | c.WithOptions(config.ParseEnv) 104 | 105 | err := c.LoadFiles("../testdata/yml_base.yml") 106 | if err != nil { 107 | panic(err) 108 | } 109 | 110 | fmt.Printf("config data: \n %#v\n", c.Data()) 111 | assert.Eq(t, "app", c.String("name")) 112 | 113 | err = c.LoadFiles("../testdata/yml_other.yml") 114 | // config.LoadFiles("testdata/yml_base.yml", "testdata/yml_other.yml") 115 | if err != nil { 116 | panic(err) 117 | } 118 | 119 | fmt.Printf("config data: \n %#v\n", c.Data()) 120 | assert.Eq(t, "app2", c.String("name")) 121 | } 122 | 123 | func TestDriver(t *testing.T) { 124 | is := assert.New(t) 125 | 126 | is.Eq("yaml", Driver.Name()) 127 | // is.IsType(new(Encoder), JSONDriver.GetEncoder()) 128 | 129 | c := config.NewEmpty("test") 130 | is.False(c.HasDecoder(config.Yaml)) 131 | c.AddDriver(Driver) 132 | is.True(c.HasDecoder(config.Yaml)) 133 | is.True(c.HasEncoder(config.Yaml)) 134 | } 135 | 136 | // Support "=", ":", "." characters for default values 137 | // see https://github.com/gookit/config/issues/9 138 | func TestIssue2(t *testing.T) { 139 | is := assert.New(t) 140 | 141 | c := config.NewEmpty("test") 142 | c.AddDriver(Driver) 143 | c.WithOptions(config.ParseEnv) 144 | 145 | err := c.LoadStrings(config.Yaml, ` 146 | command: ${APP_COMMAND|app:run} 147 | `) 148 | is.NoErr(err) 149 | testutil.MockEnvValue("APP_COMMAND", "new val", func(nv string) { 150 | is.Eq("new val", nv) 151 | is.Eq("new val", c.String("command")) 152 | }) 153 | 154 | is.Eq("", config.Getenv("APP_COMMAND")) 155 | is.Eq("app:run", c.String("command")) 156 | 157 | c.ClearAll() 158 | err = c.LoadStrings(config.Yaml, ` 159 | command: ${ APP_COMMAND | app:run } 160 | `) 161 | is.NoErr(err) 162 | testutil.MockEnvValue("APP_COMMAND", "new val", func(nv string) { 163 | is.Eq("new val", nv) 164 | is.Eq("new val", c.String("command")) 165 | }) 166 | is.Eq("", config.Getenv("APP_COMMAND")) 167 | is.Eq("app:run", c.String("command")) 168 | } 169 | -------------------------------------------------------------------------------- /yamlv3/yamlv3_test.go: -------------------------------------------------------------------------------- 1 | package yamlv3 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/gookit/config/v2" 9 | "github.com/gookit/goutil/testutil" 10 | "github.com/gookit/goutil/testutil/assert" 11 | ) 12 | 13 | var yamlStr = ` 14 | name: app2 15 | debug: false 16 | age: 23 17 | baseKey: value2 18 | 19 | map1: 20 | key: val2 21 | key2: val20 22 | 23 | arr1: 24 | - val1 25 | - val21 26 | ` 27 | 28 | func Example() { 29 | config.WithOptions(config.ParseEnv) 30 | 31 | // add yaml decoder 32 | // only add decoder 33 | // config.SetDecoder(config.Yaml, Decoder) 34 | // Or 35 | config.AddDriver(Driver) 36 | 37 | err := config.LoadFiles("../testdata/yml_other.yml") 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | // load from string 43 | _ = config.LoadSources(config.Yaml, []byte(yamlStr)) 44 | 45 | fmt.Print("get config example:\n") 46 | 47 | name := config.String("name") 48 | fmt.Printf("get string\n - val: %v\n", name) 49 | 50 | arr1 := config.Strings("arr1") 51 | fmt.Printf("get array\n - val: %#v\n", arr1) 52 | 53 | val0 := config.String("arr1.0") 54 | fmt.Printf("get sub-value by path 'arr.index'\n - val: %#v\n", val0) 55 | 56 | map1 := config.StringMap("map1") 57 | fmt.Printf("get map\n - val: %#v\n", map1) 58 | 59 | val0 = config.String("map1.key") 60 | fmt.Printf("get sub-value by path 'map.key'\n - val: %#v\n", val0) 61 | 62 | // can parse env name(ParseEnv: true) 63 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 64 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 65 | 66 | // Out: 67 | // get config example: 68 | // age: 23 69 | // get string 70 | // - val: app2 71 | // get array 72 | // - val: []string{"val1", "val21"} 73 | // get sub-value by path 'arr.index' 74 | // - val: "val1" 75 | // get map 76 | // val: map[string]string{"key":"val2", "key2":"val20"} 77 | // get sub-value by path 'map.key' 78 | // - val: "val2" 79 | // get env 'envKey' val: /bin/zsh 80 | // get env 'envKey1' val: defValue 81 | } 82 | 83 | func TestDumpConfig(t *testing.T) { 84 | is := assert.New(t) 85 | c := config.NewEmpty("test") 86 | // Notice: before dump please set driver encoder 87 | c.AddDriver(Driver) 88 | err := c.LoadStrings(config.Yaml, yamlStr) 89 | is.NoErr(err) 90 | 91 | buf := new(bytes.Buffer) 92 | _, err = c.DumpTo(buf, config.Yaml) 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | fmt.Printf("export config:\n%s", buf.String()) 98 | } 99 | 100 | func TestLoadFile(t *testing.T) { 101 | c := config.NewEmpty("test") 102 | c.AddDriver(Driver) 103 | c.WithOptions(config.ParseEnv) 104 | 105 | err := c.LoadFiles("../testdata/yml_base.yml") 106 | if err != nil { 107 | panic(err) 108 | } 109 | 110 | fmt.Printf("config data: \n %#v\n", c.Data()) 111 | assert.Eq(t, "app", c.String("name")) 112 | 113 | err = c.LoadFiles("../testdata/yml_other.yml") 114 | // config.LoadFiles("testdata/yml_base.yml", "testdata/yml_other.yml") 115 | if err != nil { 116 | panic(err) 117 | } 118 | 119 | fmt.Printf("config data: \n %#v\n", c.Data()) 120 | assert.Eq(t, "app2", c.String("name")) 121 | } 122 | 123 | func TestDriver(t *testing.T) { 124 | is := assert.New(t) 125 | 126 | is.Eq("yaml", Driver.Name()) 127 | // is.IsType(new(Encoder), JSONDriver.GetEncoder()) 128 | 129 | c := config.NewEmpty("test") 130 | is.False(c.HasDecoder(config.Yaml)) 131 | c.AddDriver(Driver) 132 | is.True(c.HasDecoder(config.Yaml)) 133 | is.True(c.HasEncoder(config.Yaml)) 134 | } 135 | 136 | // Support "=", ":", "." characters for default values 137 | // see https://github.com/gookit/config/issues/9 138 | func TestIssue2(t *testing.T) { 139 | is := assert.New(t) 140 | 141 | c := config.NewEmpty("test") 142 | c.AddDriver(Driver) 143 | c.WithOptions(config.ParseEnv) 144 | 145 | err := c.LoadStrings(config.Yaml, ` 146 | command: ${APP_COMMAND|app:run} 147 | `) 148 | is.NoErr(err) 149 | testutil.MockEnvValue("APP_COMMAND", "new val", func(nv string) { 150 | is.Eq("new val", nv) 151 | is.Eq("new val", c.String("command")) 152 | }) 153 | 154 | is.Eq("", config.Getenv("APP_COMMAND")) 155 | is.Eq("app:run", c.String("command")) 156 | 157 | c.ClearAll() 158 | err = c.LoadStrings(config.Yaml, ` 159 | command: ${ APP_COMMAND | app:run } 160 | `) 161 | is.NoErr(err) 162 | testutil.MockEnvValue("APP_COMMAND", "new val", func(nv string) { 163 | is.Eq("new val", nv) 164 | is.Eq("new val", c.String("command")) 165 | }) 166 | is.Eq("", config.Getenv("APP_COMMAND")) 167 | is.Eq("app:run", c.String("command")) 168 | } 169 | -------------------------------------------------------------------------------- /driver.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/gookit/goutil/jsonutil" 7 | ) 8 | 9 | // Driver interface. 10 | // TODO refactor: rename GetDecoder() to Decode(), rename GetEncoder() to Encode() 11 | type Driver interface { 12 | Name() string 13 | Aliases() []string // alias format names, use for resolve format name 14 | GetDecoder() Decoder 15 | GetEncoder() Encoder 16 | } 17 | 18 | // DriverV2 interface. 19 | type DriverV2 interface { 20 | Name() string // driver name, also is format name. 21 | Aliases() []string // alias format names, use for resolve format name 22 | Decode(blob []byte, v any) (err error) 23 | Encode(v any) (out []byte, err error) 24 | } 25 | 26 | // Decoder for decode yml,json,toml format content 27 | type Decoder func(blob []byte, v any) (err error) 28 | 29 | // Encoder for decode yml,json,toml format content 30 | type Encoder func(v any) (out []byte, err error) 31 | 32 | // StdDriver struct 33 | type StdDriver struct { 34 | name string 35 | aliases []string 36 | decoder Decoder 37 | encoder Encoder 38 | } 39 | 40 | // NewDriver new std driver instance. 41 | func NewDriver(name string, dec Decoder, enc Encoder) *StdDriver { 42 | return &StdDriver{name: name, decoder: dec, encoder: enc} 43 | } 44 | 45 | // WithAliases set aliases for driver 46 | func (d *StdDriver) WithAliases(aliases ...string) *StdDriver { 47 | d.aliases = aliases 48 | return d 49 | } 50 | 51 | // WithAlias add alias for driver 52 | func (d *StdDriver) WithAlias(alias string) *StdDriver { 53 | d.aliases = append(d.aliases, alias) 54 | return d 55 | } 56 | 57 | // Name of driver 58 | func (d *StdDriver) Name() string { return d.name } 59 | 60 | // Aliases format name of driver 61 | func (d *StdDriver) Aliases() []string { 62 | return d.aliases 63 | } 64 | 65 | // Decode of driver 66 | func (d *StdDriver) Decode(blob []byte, v any) (err error) { 67 | return d.decoder(blob, v) 68 | } 69 | 70 | // Encode of driver 71 | func (d *StdDriver) Encode(v any) ([]byte, error) { 72 | return d.encoder(v) 73 | } 74 | 75 | // GetDecoder of driver 76 | func (d *StdDriver) GetDecoder() Decoder { 77 | return d.decoder 78 | } 79 | 80 | // GetEncoder of driver 81 | func (d *StdDriver) GetEncoder() Encoder { 82 | return d.encoder 83 | } 84 | 85 | /************************************************************* 86 | * JSON driver 87 | *************************************************************/ 88 | 89 | var ( 90 | // JSONAllowComments support write comments on json file. 91 | JSONAllowComments = true 92 | 93 | // JSONMarshalIndent if not empty, will use json.MarshalIndent for encode data. 94 | // 95 | // Deprecated: please use JSONDriver.MarshalIndent 96 | JSONMarshalIndent string 97 | ) 98 | 99 | // JSONDecoder for json decode 100 | var JSONDecoder Decoder = func(data []byte, v any) (err error) { 101 | JSONDriver.ClearComments = JSONAllowComments 102 | return JSONDriver.Decode(data, v) 103 | } 104 | 105 | // JSONEncoder for json encode 106 | var JSONEncoder Encoder = func(v any) (out []byte, err error) { 107 | JSONDriver.MarshalIndent = JSONMarshalIndent 108 | return JSONDriver.Encode(v) 109 | } 110 | 111 | // JSONDriver instance fot json 112 | var JSONDriver = &jsonDriver{ 113 | driverName: JSON, 114 | ClearComments: JSONAllowComments, 115 | MarshalIndent: JSONMarshalIndent, 116 | } 117 | 118 | // jsonDriver for json format content 119 | type jsonDriver struct { 120 | driverName string 121 | // ClearComments before parse JSON string. 122 | ClearComments bool 123 | // MarshalIndent if not empty, will use json.MarshalIndent for encode data. 124 | MarshalIndent string 125 | } 126 | 127 | // Name of the driver 128 | func (d *jsonDriver) Name() string { 129 | return d.driverName 130 | } 131 | 132 | // Aliases of the driver 133 | func (d *jsonDriver) Aliases() []string { 134 | return nil 135 | } 136 | 137 | // Decode for the driver 138 | func (d *jsonDriver) Decode(data []byte, v any) error { 139 | if d.ClearComments { 140 | str := jsonutil.StripComments(string(data)) 141 | return json.Unmarshal([]byte(str), v) 142 | } 143 | return json.Unmarshal(data, v) 144 | } 145 | 146 | // GetDecoder for the driver 147 | func (d *jsonDriver) GetDecoder() Decoder { 148 | return d.Decode 149 | } 150 | 151 | // Encode for the driver 152 | func (d *jsonDriver) Encode(v any) (out []byte, err error) { 153 | if len(d.MarshalIndent) > 0 { 154 | return json.MarshalIndent(v, "", d.MarshalIndent) 155 | } 156 | return json.Marshal(v) 157 | } 158 | 159 | // GetEncoder for the driver 160 | func (d *jsonDriver) GetEncoder() Encoder { 161 | return d.Encode 162 | } 163 | -------------------------------------------------------------------------------- /write_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestSetData(t *testing.T) { 12 | defer func() { 13 | Reset() 14 | }() 15 | 16 | c := Default() 17 | 18 | err := c.LoadStrings(JSON, jsonStr) 19 | assert.NoErr(t, err) 20 | assert.Eq(t, "app", c.String("name")) 21 | assert.True(t, c.Exists("age")) 22 | 23 | SetData(map[string]any{ 24 | "name": "new app", 25 | }) 26 | assert.Eq(t, "new app", c.String("name")) 27 | assert.False(t, c.Exists("age")) 28 | 29 | c.SetData(map[string]any{ 30 | "age": 222, 31 | }) 32 | assert.Eq(t, "", c.String("name")) 33 | assert.False(t, c.Exists("name")) 34 | assert.True(t, c.Exists("age")) 35 | assert.Eq(t, 222, c.Int("age")) 36 | } 37 | 38 | func TestSet(t *testing.T) { 39 | defer func() { 40 | ClearAll() 41 | }() 42 | 43 | is := assert.New(t) 44 | c := Default() 45 | 46 | // clear old 47 | ClearAll() 48 | // err := LoadFiles("testdata/json_base.json") 49 | err := LoadStrings(JSON, jsonStr) 50 | is.Nil(err) 51 | 52 | val := String("name") 53 | is.Eq("app", val) 54 | 55 | // empty key 56 | err = Set("", "val") 57 | is.Err(err) 58 | 59 | // set new value: int 60 | err = Set("newInt", 23) 61 | if is.Nil(err).IsOk() { 62 | iv := Int("newInt") 63 | is.Eq(23, iv) 64 | } 65 | 66 | // set new value: int 67 | err = Set("newBool", false) 68 | if is.Nil(err).IsOk() { 69 | bv := Bool("newBool") 70 | is.False(bv) 71 | } 72 | 73 | // set new value: string 74 | err = Set("newKey", "new val") 75 | if is.Nil(err).IsOk() { 76 | val = String("newKey") 77 | is.Eq("new val", val) 78 | } 79 | 80 | // like yaml.v2 decoded data 81 | err = Set("ymlLike", map[any]any{"k": "v"}) 82 | is.Nil(err) 83 | str := c.String("ymlLike.k") 84 | is.Eq("v", str) 85 | 86 | err = Set("ymlLike.nk", "nv") 87 | is.Nil(err) 88 | str = c.String("ymlLike.nk") 89 | is.Eq("nv", str) 90 | 91 | // disable setByPath 92 | err = Set("some.key", "val", false) 93 | if is.Nil(err).IsOk() { 94 | val = String("some") 95 | is.Eq("", val) 96 | 97 | val = String("some.key") 98 | is.Eq("val", val) 99 | } 100 | // fmt.Printf("%#v\n", c.Data()) 101 | 102 | // set value 103 | err = Set("name", "new name") 104 | if is.Nil(err).IsOk() { 105 | val = String("name") 106 | is.Eq("new name", val) 107 | } 108 | 109 | // set value to arr: by path 110 | err = Set("arr1.1", "new val") 111 | if is.Nil(err).IsOk() { 112 | val = String("arr1.1") 113 | is.Eq("new val", val) 114 | } 115 | 116 | // array only support add 1 level value 117 | err = Set("arr1.1.key", "new val") 118 | is.Err(err) 119 | 120 | // set value to map: by path 121 | err = Set("map1.key", "new val") 122 | if is.Nil(err).IsOk() { 123 | val = String("map1.key") 124 | is.Eq("new val", val) 125 | } 126 | 127 | // more path nodes 128 | err = Set("map1.info.key", "val200") 129 | if is.Nil(err).IsOk() { 130 | // fmt.Printf("%v\n", c.Data()) 131 | smp := StringMap("map1.info") 132 | is.Eq("val200", smp["key"]) 133 | 134 | str = String("map1.info.key") 135 | is.Eq("val200", str) 136 | } 137 | 138 | // new map 139 | err = Set("map2.key", "new val") 140 | if is.Nil(err).IsOk() { 141 | val = String("map2.key") 142 | 143 | is.Eq("new val", val) 144 | } 145 | 146 | // set new value: array(slice) 147 | err = Set("newArr", []string{"a", "b"}) 148 | if is.Nil(err).IsOk() { 149 | arr := Strings("newArr") 150 | 151 | is.Eq(`[]string{"a", "b"}`, fmt.Sprintf("%#v", arr)) 152 | 153 | val = String("newArr.1") 154 | is.Eq("b", val) 155 | 156 | val = String("newArr.100") 157 | is.Eq("", val) 158 | } 159 | 160 | // set new value: map 161 | err = Set("newMap", map[string]string{"k1": "a", "k2": "b"}) 162 | if is.Nil(err).IsOk() { 163 | mp := StringMap("newMap") 164 | is.NotEmpty(mp) 165 | // is.Eq("map[k1:a k2:b]", fmt.Sprintf("%v", mp)) 166 | 167 | val = String("newMap.k1") 168 | is.Eq("a", val) 169 | 170 | val = String("newMap.notExist") 171 | is.Eq("", val) 172 | } 173 | 174 | is.NoErr(Set("name.sub", []int{2}, false)) 175 | ints := Ints("name.sub") 176 | is.Eq([]int{2}, ints) 177 | 178 | // Readonly 179 | Default().Readonly() 180 | is.True(c.Options().Readonly) 181 | is.Err(Set("name", "new name")) 182 | } 183 | 184 | func TestSet_fireEvent(t *testing.T) { 185 | var buf bytes.Buffer 186 | hookFn := func(event string, c *Config) { 187 | buf.WriteString("fire the: ") 188 | buf.WriteString(event) 189 | } 190 | 191 | c := NewWithOptions("test", WithHookFunc(hookFn)) 192 | err := c.LoadData(map[string]any{ 193 | "key": "value", 194 | }) 195 | assert.NoErr(t, err) 196 | assert.Eq(t, "fire the: load.data", buf.String()) 197 | buf.Reset() 198 | 199 | err = c.Set("key", "value2") 200 | assert.NoErr(t, err) 201 | assert.Eq(t, "fire the: set.value", buf.String()) 202 | buf.Reset() 203 | } 204 | -------------------------------------------------------------------------------- /export.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | 10 | "github.com/go-viper/mapstructure/v2" 11 | "github.com/gookit/goutil/structs" 12 | ) 13 | 14 | // Decode all config data to the dst ptr 15 | // 16 | // Usage: 17 | // 18 | // myConf := &MyConf{} 19 | // config.Decode(myConf) 20 | func Decode(dst any) error { return dc.Decode(dst) } 21 | 22 | // Decode all config data to the dst ptr. 23 | // 24 | // It's equals: 25 | // 26 | // c.Structure("", dst) 27 | func (c *Config) Decode(dst any) error { 28 | return c.Structure("", dst) 29 | } 30 | 31 | // MapStruct alias method of the 'Structure' 32 | // 33 | // Usage: 34 | // 35 | // dbInfo := &Db{} 36 | // config.MapStruct("db", dbInfo) 37 | func MapStruct(key string, dst any) error { return dc.MapStruct(key, dst) } 38 | 39 | // MapStruct alias method of the 'Structure' 40 | func (c *Config) MapStruct(key string, dst any) error { return c.Structure(key, dst) } 41 | 42 | // BindStruct alias method of the 'Structure' 43 | func BindStruct(key string, dst any) error { return dc.BindStruct(key, dst) } 44 | 45 | // BindStruct alias method of the 'Structure' 46 | func (c *Config) BindStruct(key string, dst any) error { return c.Structure(key, dst) } 47 | 48 | // MapOnExists mapping data to the dst structure only on key exists. 49 | func MapOnExists(key string, dst any) error { 50 | return dc.MapOnExists(key, dst) 51 | } 52 | 53 | // MapOnExists mapping data to the dst structure only on key exists. 54 | // 55 | // - Support ParseEnv on mapping 56 | // - Support ParseDefault on mapping 57 | func (c *Config) MapOnExists(key string, dst any) error { 58 | err := c.Structure(key, dst) 59 | if err != nil && err == ErrNotFound { 60 | return nil 61 | } 62 | return err 63 | } 64 | 65 | // Structure get config data and binding to the dst structure. 66 | // 67 | // - Support ParseEnv on mapping 68 | // - Support ParseDefault on mapping 69 | // 70 | // Usage: 71 | // 72 | // dbInfo := Db{} 73 | // config.Structure("db", &dbInfo) 74 | func (c *Config) Structure(key string, dst any) (err error) { 75 | var data any 76 | // binding all data on key is empty. 77 | if key == "" { 78 | // fix: if c.data is nil, don't need to apply map structure 79 | if len(c.data) == 0 { 80 | // init default value by tag: default 81 | if c.opts.ParseDefault { 82 | err = structs.InitDefaults(dst, func(opt *structs.InitOptions) { 83 | opt.ParseEnv = c.opts.ParseEnv 84 | opt.ParseTime = c.opts.ParseTime // add ParseTime support on parse default value 85 | }) 86 | } 87 | return 88 | } 89 | 90 | data = c.data 91 | } else { 92 | // binding sub-data of the config 93 | var ok bool 94 | data, ok = c.GetValue(key) 95 | if !ok { 96 | return ErrNotFound 97 | } 98 | } 99 | 100 | // map structure from data 101 | bindConf := c.opts.makeDecoderConfig() 102 | // set result struct ptr 103 | bindConf.Result = dst 104 | decoder, err := mapstructure.NewDecoder(bindConf) 105 | if err == nil { 106 | if err = decoder.Decode(data); err != nil { 107 | return err 108 | } 109 | } 110 | 111 | // init default value by tag: default 112 | if c.opts.ParseDefault { 113 | err = structs.InitDefaults(dst, func(opt *structs.InitOptions) { 114 | opt.ParseEnv = c.opts.ParseEnv 115 | opt.ParseTime = c.opts.ParseTime 116 | }) 117 | } 118 | return err 119 | } 120 | 121 | // ToJSON string, will ignore error 122 | func (c *Config) ToJSON() string { 123 | buf := &bytes.Buffer{} 124 | 125 | _, err := c.DumpTo(buf, JSON) 126 | if err != nil { 127 | return "" 128 | } 129 | return buf.String() 130 | } 131 | 132 | // WriteTo a writer 133 | func WriteTo(out io.Writer) (int64, error) { return dc.WriteTo(out) } 134 | 135 | // WriteTo Write out config data representing the current state to a writer. 136 | func (c *Config) WriteTo(out io.Writer) (n int64, err error) { 137 | return c.DumpTo(out, c.opts.DumpFormat) 138 | } 139 | 140 | // DumpTo a writer and use format 141 | func DumpTo(out io.Writer, format string) (int64, error) { return dc.DumpTo(out, format) } 142 | 143 | // DumpTo use the format(json,yaml,toml) dump config data to a writer 144 | func (c *Config) DumpTo(out io.Writer, format string) (n int64, err error) { 145 | var ok bool 146 | var encoder Encoder 147 | 148 | format = c.resolveFormat(format) 149 | if encoder, ok = c.encoders[format]; !ok { 150 | err = errors.New("not exists/register encoder for the format: " + format) 151 | return 152 | } 153 | 154 | // is empty 155 | if len(c.data) == 0 { 156 | return 157 | } 158 | 159 | // encode data to string 160 | encoded, err := encoder(c.data) 161 | if err != nil { 162 | return 163 | } 164 | 165 | // write content to out 166 | num, _ := fmt.Fprintln(out, string(encoded)) 167 | return int64(num), nil 168 | } 169 | 170 | // DumpToFile use the format(json,yaml,toml) dump config data to a writer 171 | func (c *Config) DumpToFile(fileName string, format string) (err error) { 172 | fsFlags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC 173 | f, err := os.OpenFile(fileName, fsFlags, os.ModePerm) 174 | if err != nil { 175 | return err 176 | } 177 | 178 | _, err = c.DumpTo(f, format) 179 | if err1 := f.Close(); err1 != nil && err == nil { 180 | err = err1 181 | } 182 | return err 183 | } 184 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strings" 5 | 6 | "dario.cat/mergo" 7 | "github.com/go-viper/mapstructure/v2" 8 | "github.com/gookit/goutil" 9 | ) 10 | 11 | // there are some event names for config data changed. 12 | const ( 13 | OnSetValue = "set.value" 14 | OnSetData = "set.data" 15 | OnLoadData = "load.data" 16 | OnReloadData = "reload.data" 17 | OnCleanData = "clean.data" 18 | ) 19 | 20 | // HookFunc on config data changed. 21 | type HookFunc func(event string, c *Config) 22 | 23 | // Options config options 24 | type Options struct { 25 | // ParseEnv parse env in string value and default value. default: false 26 | // 27 | // - like: "${EnvName}" "${EnvName|default}" 28 | ParseEnv bool 29 | // ParseTime parses a duration string to `time.Duration`. default: false 30 | // 31 | // eg: 10s, 2m 32 | ParseTime bool 33 | // ParseDefault tag on binding data to struct. default: false 34 | // 35 | // - tag: default 36 | // 37 | // NOTE: If you want to parse a substruct, you need to set the `default:""` flag on the struct, 38 | // otherwise the fields that will not resolve to it will not be resolved. 39 | ParseDefault bool 40 | // Readonly config is readonly. default: false 41 | Readonly bool 42 | // EnableCache enable config data cache. default: false 43 | EnableCache bool 44 | // ParseKey support key path, allow finding value by key path. default: true 45 | // 46 | // - eg: 'key.sub' will find `map[key]sub` 47 | ParseKey bool 48 | // TagName tag name for binding data to struct 49 | // 50 | // Deprecated: please set tag name by DecoderConfig, or use SetTagName() 51 | TagName string 52 | // Delimiter the delimiter char for split key path, on `ParseKey=true`. 53 | // 54 | // - default is '.' 55 | Delimiter byte 56 | // DumpFormat default write format. default is 'json' 57 | DumpFormat string 58 | // ReadFormat default input format. default is 'json' 59 | ReadFormat string 60 | // DecoderConfig setting for binding data to struct. such as: TagName 61 | DecoderConfig *mapstructure.DecoderConfig 62 | // MergeOptions settings for merge two data 63 | MergeOptions []func(*mergo.Config) 64 | // HookFunc on data changed. you can do something... 65 | HookFunc HookFunc 66 | // WatchChange bool 67 | } 68 | 69 | // OptionFn option func 70 | type OptionFn func(*Options) 71 | 72 | func newDefaultOption() *Options { 73 | return &Options{ 74 | ParseKey: true, 75 | TagName: defaultStructTag, 76 | Delimiter: defaultDelimiter, 77 | // for export 78 | DumpFormat: JSON, 79 | ReadFormat: JSON, 80 | // struct decoder config 81 | DecoderConfig: newDefaultDecoderConfig(""), 82 | MergeOptions: []func(*mergo.Config){ 83 | mergo.WithOverride, 84 | mergo.WithTypeCheck, 85 | }, 86 | } 87 | } 88 | 89 | func newDefaultDecoderConfig(tagName string) *mapstructure.DecoderConfig { 90 | if tagName == "" { 91 | tagName = defaultStructTag 92 | } 93 | 94 | return &mapstructure.DecoderConfig{ 95 | // tag name for binding struct 96 | TagName: tagName, 97 | // will auto convert string to int/uint 98 | WeaklyTypedInput: true, 99 | } 100 | } 101 | 102 | // SetTagName for mapping data to struct 103 | func (o *Options) SetTagName(tagName string) { 104 | o.TagName = tagName 105 | o.DecoderConfig.TagName = tagName 106 | } 107 | 108 | func (o *Options) shouldAddHookFunc() bool { 109 | return o.ParseTime || o.ParseEnv 110 | } 111 | 112 | func (o *Options) makeDecoderConfig() *mapstructure.DecoderConfig { 113 | var bindConf *mapstructure.DecoderConfig 114 | if o.DecoderConfig == nil { 115 | bindConf = newDefaultDecoderConfig(o.TagName) 116 | } else { 117 | // copy new config for each binding. 118 | copyConf := *o.DecoderConfig 119 | bindConf = ©Conf 120 | 121 | // compatible with previous settings opts.TagName 122 | if bindConf.TagName == "" { 123 | bindConf.TagName = o.TagName 124 | } 125 | } 126 | 127 | // add hook on decode value to struct 128 | if bindConf.DecodeHook == nil && o.shouldAddHookFunc() { 129 | bindConf.DecodeHook = ValDecodeHookFunc(o.ParseEnv, o.ParseTime) 130 | } 131 | 132 | return bindConf 133 | } 134 | 135 | /************************************************************* 136 | * config setting 137 | *************************************************************/ 138 | 139 | // WithTagName set tag name for export to struct 140 | func WithTagName(tagName string) func(*Options) { 141 | return func(opts *Options) { 142 | opts.SetTagName(tagName) 143 | } 144 | } 145 | 146 | // ParseEnv set parse env value 147 | func ParseEnv(opts *Options) { opts.ParseEnv = true } 148 | 149 | // ParseTime set parse time string. 150 | func ParseTime(opts *Options) { opts.ParseTime = true } 151 | 152 | // ParseDefault tag value on binding data to struct. 153 | func ParseDefault(opts *Options) { opts.ParseDefault = true } 154 | 155 | // Readonly set readonly 156 | func Readonly(opts *Options) { opts.Readonly = true } 157 | 158 | // Delimiter set delimiter char 159 | func Delimiter(sep byte) func(*Options) { 160 | return func(opts *Options) { 161 | opts.Delimiter = sep 162 | } 163 | } 164 | 165 | // SaveFileOnSet set hook func, will panic on save error 166 | func SaveFileOnSet(fileName string, format string) func(options *Options) { 167 | return func(opts *Options) { 168 | opts.HookFunc = func(event string, c *Config) { 169 | if strings.HasPrefix(event, "set.") { 170 | goutil.PanicErr(c.DumpToFile(fileName, format)) 171 | } 172 | } 173 | } 174 | } 175 | 176 | // WithHookFunc set hook func 177 | func WithHookFunc(fn HookFunc) func(*Options) { 178 | return func(opts *Options) { 179 | opts.HookFunc = fn 180 | } 181 | } 182 | 183 | // EnableCache set readonly 184 | func EnableCache(opts *Options) { opts.EnableCache = true } 185 | 186 | // WithOptions with options 187 | func WithOptions(opts ...OptionFn) { dc.WithOptions(opts...) } 188 | 189 | // WithOptions apply some options 190 | func (c *Config) WithOptions(opts ...OptionFn) *Config { 191 | if !c.IsEmpty() { 192 | panic("config: Cannot set options after data has been loaded") 193 | } 194 | 195 | // apply options 196 | for _, opt := range opts { 197 | opt(c.opts) 198 | } 199 | return c 200 | } 201 | 202 | // GetOptions get options 203 | func GetOptions() *Options { return dc.Options() } 204 | 205 | // Options get 206 | func (c *Config) Options() *Options { 207 | return c.opts 208 | } 209 | 210 | // With apply some options 211 | func (c *Config) With(fn func(c *Config)) *Config { 212 | fn(c) 213 | return c 214 | } 215 | 216 | // Readonly disable set data to config. 217 | // 218 | // Usage: 219 | // 220 | // config.LoadFiles(a, b, c) 221 | // config.Readonly() 222 | func (c *Config) Readonly() { 223 | c.opts.Readonly = true 224 | } 225 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # Copilot Instructions for gookit/config 2 | 3 | ## Repository Overview 4 | 5 | **gookit/config** is a comprehensive Go configuration management library that supports multiple formats (JSON, YAML, TOML, INI, HCL, ENV, Flags). It provides features like environment variable parsing, data merging, struct binding, event hooks, and remote configuration loading. 6 | 7 | - **Language**: Go (requires Go 1.19+) 8 | - **Size**: ~40 Go source files across multiple format driver packages 9 | - **Type**: Library package (not an executable application) 10 | - **Module Path**: `github.com/gookit/config/v2` 11 | - **Coverage**: >95% test coverage with comprehensive unit tests 12 | 13 | ## Build and Test Instructions 14 | 15 | ### Prerequisites 16 | - Go 1.19 or higher (CI tests against 1.19, 1.20, 1.21, 1.22, 1.23, 1.24) 17 | - No additional build tools required (pure Go project) 18 | 19 | ### Dependencies Management 20 | **ALWAYS run this first before any other operations:** 21 | ```bash 22 | go mod tidy 23 | ``` 24 | 25 | ### Building 26 | Build the library to verify compilation: 27 | ```bash 28 | go build ./... 29 | ``` 30 | **Expected**: No output indicates successful build. This is a library, so no binaries are generated. 31 | 32 | ### Testing 33 | Run all tests with coverage (recommended): 34 | ```bash 35 | go test -cover ./... 36 | ``` 37 | **Expected**: All tests pass with >95% coverage. Typical runtime: ~3 seconds. 38 | 39 | Run tests for specific package: 40 | ```bash 41 | go test -cover . # Main package only 42 | go test -cover ./yaml # YAML driver only 43 | go test -cover ./json5 # JSON5 driver only 44 | ``` 45 | 46 | **Note**: Some driver packages may show "no statements" for coverage as they only import/register drivers. 47 | 48 | ### Linting 49 | Install and run linting tools used in CI: 50 | 51 | ```bash 52 | # Install linting tools (one-time setup) 53 | go install honnef.co/go/tools/cmd/staticcheck@latest 54 | go install github.com/mgechev/revive@latest 55 | 56 | # Run static analysis (fails CI if errors found) 57 | staticcheck ./... 58 | 59 | # Run style linting (excludes examples and testdata) 60 | revive -exclude ./_examples/... -exclude ./testdata/... ./... 61 | ``` 62 | 63 | **Expected Issues**: The linting tools may report some issues that are acceptable in this codebase (unused parameters in incomplete HCL implementations, etc.). Only new issues in your changes need to be addressed. 64 | 65 | ### Running Examples 66 | Test functionality with examples: 67 | ```bash 68 | # Run YAML example (demonstrates typical usage) 69 | go run _examples/yaml.go 70 | 71 | # All examples are in _examples/ directory 72 | ls _examples/ 73 | ``` 74 | 75 | ## Project Architecture and Layout 76 | 77 | ### Core Files (Repository Root) 78 | - `config.go` - Main Config struct and core functionality 79 | - `driver.go` - Driver interface and JSON driver implementation 80 | - `options.go` - Configuration options and hooks 81 | - `load.go` - File/data loading functionality 82 | - `read.go` - Data reading and type conversion methods 83 | - `write.go` - Data writing and modification methods 84 | - `export.go` - Data export and struct binding 85 | - `util.go` - Utility functions 86 | 87 | ### Format Driver Packages 88 | Each format has its own package with minimal code (usually just driver registration): 89 | - `json/` - Enhanced JSON driver with additional features 90 | - `json5/` - JSON5 format support 91 | - `yaml/` - YAML v2 support 92 | - `yamlv3/` - YAML v3 support (recommended) 93 | - `toml/` - TOML format support 94 | - `ini/` - INI format support 95 | - `hcl/` - HCL v1 support (basic) 96 | - `hclv2/` - HCL v2 support (incomplete/experimental) 97 | - `properties/` - Java properties format 98 | - `other/` - Empty driver package for examples 99 | 100 | ### Test and Example Structure 101 | - `*_test.go` - Unit tests alongside source files 102 | - `issues_test.go` - Regression tests for reported issues 103 | - `testdata/` - Test configuration files in various formats 104 | - `_examples/` - Usage examples for each format 105 | 106 | ### Configuration Files 107 | - `go.mod`/`go.sum` - Go module dependencies 108 | - `.github/workflows/` - CI/CD workflows 109 | - `go.yml` - Unit tests on multiple Go versions 110 | - `lint.yml` - Code linting with revive and staticcheck 111 | - `codeql.yml` - Security code scanning 112 | - `release.yml` - Automated releases 113 | 114 | ## CI/CD and Validation 115 | 116 | ### GitHub Actions Workflows 117 | The repository runs comprehensive validation on every PR and push: 118 | 119 | 1. **Unit Tests** (`go.yml`): Tests on Go 1.19-1.24, generates coverage reports 120 | 2. **Linting** (`lint.yml`): Runs revive and staticcheck for code quality 121 | 3. **CodeQL** (`codeql.yml`): Security vulnerability scanning 122 | 4. **Release** (`release.yml`): Automated versioning and releases 123 | 124 | ### Pre-commit Validation 125 | Before committing, run the same checks as CI: 126 | ```bash 127 | # Run all tests 128 | go test -cover ./... 129 | 130 | # Run linting 131 | staticcheck ./... 132 | revive -exclude ./_examples/... -exclude ./testdata/... ./... 133 | 134 | # Verify build 135 | go build ./... 136 | ``` 137 | 138 | ### Test Data Requirements 139 | - Always test new features with files in `testdata/` directory 140 | - Each format should have `format_base.ext` and `format_other.ext` test files 141 | - Examples in `_examples/` must work from repository root: `go run _examples/name.go` 142 | 143 | ## Key Architecture Patterns 144 | 145 | ### Driver System 146 | The library uses a driver pattern for format support: 147 | - `Driver` interface defines encode/decode for formats 148 | - `StdDriver` provides standard implementation 149 | - Each format package registers its driver on import 150 | - JSON driver is built-in, others are optional 151 | 152 | ### Configuration Loading 153 | Standard usage pattern: 154 | ```go 155 | config.AddDriver(yamlv3.Driver) // Add format support 156 | config.LoadFiles("config.yml") // Load config files 157 | config.String("key") // Read values 158 | ``` 159 | 160 | ### Options and Hooks 161 | - Options control parsing behavior (env vars, defaults, caching) 162 | - Hook functions fire on data changes (useful for file watching) 163 | - Set options before loading any data 164 | 165 | ### Error Handling 166 | - Methods return errors for validation 167 | - Panic on programmer errors (e.g., setting options after loading data) 168 | - Default values provided for missing configuration 169 | 170 | ## Common Development Patterns 171 | 172 | ### Adding New Format Support 173 | 1. Create new driver package (copy existing as template) 174 | 2. Implement `Driver` interface with encode/decode 175 | 3. Add test files to `testdata/` 176 | 4. Create example in `_examples/` 177 | 5. Add import to main package for registration 178 | 179 | ### Adding Configuration Options 180 | 1. Add field to `Options` struct in `options.go` 181 | 2. Create option function (e.g., `func NewOption(opts *Options) { opts.Field = value }`) 182 | 3. Update `newDefaultOption()` with sensible default 183 | 4. Add tests in `config_test.go` 184 | 185 | ### Modifying Core Functionality 186 | - Main logic in `config.go`, `load.go`, `read.go`, `write.go`, `export.go` 187 | - Always maintain backward compatibility 188 | - Add comprehensive tests for new features 189 | - Update examples if behavior changes 190 | 191 | ## Trust These Instructions 192 | 193 | These instructions have been validated by: 194 | - Running all build and test commands successfully 195 | - Executing examples to verify functionality 196 | - Testing linting tools and CI workflows 197 | - Examining actual CI configurations and workflows 198 | 199 | Only search for additional information if: 200 | - Instructions are incomplete for your specific task 201 | - Commands fail with unexpected errors 202 | - Working with experimental features (like HCL v2) 203 | - Need details about specific internal implementation 204 | 205 | **Always run the validation commands before submitting changes to avoid CI failures.** -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package config is a go config management implement. support YAML,TOML,JSON,INI,HCL format. 3 | 4 | Source code and other details for the project are available at GitHub: 5 | 6 | https://github.com/gookit/config 7 | 8 | JSON format content example: 9 | 10 | { 11 | "name": "app", 12 | "debug": false, 13 | "baseKey": "value", 14 | "age": 123, 15 | "envKey": "${SHELL}", 16 | "envKey1": "${NotExist|defValue}", 17 | "map1": { 18 | "key": "val", 19 | "key1": "val1", 20 | "key2": "val2" 21 | }, 22 | "arr1": [ 23 | "val", 24 | "val1", 25 | "val2" 26 | ], 27 | "lang": { 28 | "dir": "res/lang", 29 | "defLang": "en", 30 | "allowed": { 31 | "en": "val", 32 | "zh-CN": "val2" 33 | } 34 | } 35 | } 36 | 37 | Usage please see example(more example please see examples folder in the lib): 38 | */ 39 | package config 40 | 41 | import ( 42 | "fmt" 43 | "sync" 44 | ) 45 | 46 | // There are supported config format 47 | const ( 48 | Ini = "ini" 49 | Hcl = "hcl" 50 | Yml = "yml" 51 | JSON = "json" 52 | Yaml = "yaml" 53 | Toml = "toml" 54 | Prop = "properties" 55 | ) 56 | 57 | const ( 58 | // default delimiter 59 | defaultDelimiter byte = '.' 60 | // default struct tag name for binding data to struct 61 | defaultStructTag = "mapstructure" 62 | // struct tag name for set default-value on binding data 63 | defaultValueTag = "default" 64 | ) 65 | 66 | // internal vars 67 | // type intArr []int 68 | type strArr []string 69 | 70 | // type intMap map[string]int 71 | type strMap map[string]string 72 | 73 | // This is a default config manager instance 74 | var dc = New("default") 75 | 76 | // Config structure definition 77 | type Config struct { 78 | // save the latest error, will clear after read. 79 | err error 80 | // config instance name 81 | name string 82 | lock sync.RWMutex 83 | 84 | // config options 85 | opts *Options 86 | // all config data 87 | data map[string]any 88 | 89 | // loaded config files records 90 | loadedUrls []string 91 | loadedFiles []string 92 | driverNames []string 93 | // driver alias to name map. 94 | aliasMap map[string]string 95 | reloading bool 96 | 97 | // TODO Deprecated decoder and encoder, use driver instead 98 | // drivers map[string]Driver 99 | 100 | // decoders["toml"] = func(blob []byte, v any) (err error){} 101 | // decoders["yaml"] = func(blob []byte, v any) (err error){} 102 | decoders map[string]Decoder 103 | encoders map[string]Encoder 104 | 105 | // cache on got config data 106 | intCache map[string]int 107 | strCache map[string]string 108 | // iArrCache map[string]intArr TODO cache it 109 | // iMapCache map[string]intMap 110 | sArrCache map[string]strArr 111 | sMapCache map[string]strMap 112 | } 113 | 114 | // New config instance with custom options, default with JSON driver 115 | func New(name string, opts ...OptionFn) *Config { 116 | return NewEmpty(name, opts...).WithDriver(JSONDriver) 117 | } 118 | 119 | // NewGeneric create generic config instance with custom options. 120 | // 121 | // - default add options: ParseEnv, ParseDefault, ParseTime 122 | func NewGeneric(name string, opts ...OptionFn) *Config { 123 | return NewEmpty(name, ParseEnv, ParseDefault, ParseTime).WithOptions(opts...).WithDriver(JSONDriver) 124 | } 125 | 126 | // NewEmpty create config instance with custom options 127 | func NewEmpty(name string, opts ...OptionFn) *Config { 128 | c := &Config{ 129 | name: name, 130 | opts: newDefaultOption(), 131 | data: make(map[string]any), 132 | // don't add any drivers 133 | encoders: map[string]Encoder{}, 134 | decoders: map[string]Decoder{}, 135 | aliasMap: make(map[string]string), 136 | } 137 | 138 | return c.WithOptions(opts...) 139 | } 140 | 141 | // NewWith create config instance, and you can call some init func 142 | func NewWith(name string, fn func(c *Config)) *Config { 143 | return New(name).With(fn) 144 | } 145 | 146 | // NewWithOptions config instance. alias of New() 147 | func NewWithOptions(name string, opts ...OptionFn) *Config { 148 | return New(name).WithOptions(opts...) 149 | } 150 | 151 | // Default get the default instance 152 | func Default() *Config { return dc } 153 | 154 | /************************************************************* 155 | * config drivers 156 | *************************************************************/ 157 | 158 | // WithDriver set multi drivers at once. 159 | func WithDriver(drivers ...Driver) { dc.WithDriver(drivers...) } 160 | 161 | // WithDriver set multi drivers at once. 162 | func (c *Config) WithDriver(drivers ...Driver) *Config { 163 | for _, driver := range drivers { 164 | c.AddDriver(driver) 165 | } 166 | return c 167 | } 168 | 169 | // AddDriver set a decoder and encoder driver for a format. 170 | func AddDriver(driver Driver) { dc.AddDriver(driver) } 171 | 172 | // AddDriver set a decoder and encoder driver for a format. 173 | func (c *Config) AddDriver(driver Driver) { 174 | format := driver.Name() 175 | if len(driver.Aliases()) > 0 { 176 | for _, alias := range driver.Aliases() { 177 | c.aliasMap[alias] = format 178 | } 179 | } 180 | 181 | c.driverNames = append(c.driverNames, format) 182 | c.decoders[format] = driver.GetDecoder() 183 | c.encoders[format] = driver.GetEncoder() 184 | } 185 | 186 | // HasDecoder has decoder 187 | func (c *Config) HasDecoder(format string) bool { 188 | format = c.resolveFormat(format) 189 | _, ok := c.decoders[format] 190 | return ok 191 | } 192 | 193 | // HasEncoder has encoder 194 | func (c *Config) HasEncoder(format string) bool { 195 | format = c.resolveFormat(format) 196 | _, ok := c.encoders[format] 197 | return ok 198 | } 199 | 200 | // DelDriver delete driver of the format 201 | func (c *Config) DelDriver(format string) { 202 | format = c.resolveFormat(format) 203 | delete(c.decoders, format) 204 | delete(c.encoders, format) 205 | } 206 | 207 | /************************************************************* 208 | * helper methods 209 | *************************************************************/ 210 | 211 | // Name get config name 212 | func (c *Config) Name() string { return c.name } 213 | 214 | // AddAlias add alias for a format(driver name) 215 | func AddAlias(format, alias string) { dc.AddAlias(format, alias) } 216 | 217 | // AddAlias add alias for a format(driver name) 218 | // 219 | // Example: 220 | // 221 | // config.AddAlias("ini", "conf") 222 | func (c *Config) AddAlias(format, alias string) { 223 | c.aliasMap[alias] = format 224 | } 225 | 226 | // AliasMap get alias map 227 | func (c *Config) AliasMap() map[string]string { return c.aliasMap } 228 | 229 | // Error get last error, will clear after read. 230 | func (c *Config) Error() error { 231 | err := c.err 232 | c.err = nil 233 | return err 234 | } 235 | 236 | // IsEmpty of the config 237 | func (c *Config) IsEmpty() bool { 238 | return len(c.data) == 0 239 | } 240 | 241 | // LoadedUrls get loaded urls list 242 | func (c *Config) LoadedUrls() []string { return c.loadedUrls } 243 | 244 | // LoadedFiles get loaded files name 245 | func (c *Config) LoadedFiles() []string { return c.loadedFiles } 246 | 247 | // DriverNames get loaded driver names 248 | func (c *Config) DriverNames() []string { return c.driverNames } 249 | 250 | // Reset data and caches 251 | func Reset() { dc.ClearAll() } 252 | 253 | // ClearAll data and caches 254 | func ClearAll() { dc.ClearAll() } 255 | 256 | // ClearAll data and caches 257 | func (c *Config) ClearAll() { 258 | c.ClearData() 259 | c.ClearCaches() 260 | 261 | c.aliasMap = make(map[string]string) 262 | // options 263 | c.opts.Readonly = false 264 | } 265 | 266 | // ClearData clear data 267 | func (c *Config) ClearData() { 268 | c.fireHook(OnCleanData) 269 | 270 | c.data = make(map[string]any) 271 | c.loadedUrls = []string{} 272 | c.loadedFiles = []string{} 273 | } 274 | 275 | // ClearCaches clear caches 276 | func (c *Config) ClearCaches() { 277 | if c.opts.EnableCache { 278 | c.intCache = nil 279 | c.strCache = nil 280 | c.sMapCache = nil 281 | c.sArrCache = nil 282 | } 283 | } 284 | 285 | /************************************************************* 286 | * helper methods 287 | *************************************************************/ 288 | 289 | // fire hook 290 | func (c *Config) fireHook(name string) { 291 | if c.opts.HookFunc != nil { 292 | c.opts.HookFunc(name, c) 293 | } 294 | } 295 | 296 | // record error 297 | func (c *Config) addError(err error) { 298 | c.err = err 299 | } 300 | 301 | // format and record error 302 | func (c *Config) addErrorf(format string, a ...any) { 303 | c.err = fmt.Errorf(format, a...) 304 | } 305 | -------------------------------------------------------------------------------- /export_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/gookit/goutil/dump" 9 | "github.com/gookit/goutil/jsonutil" 10 | "github.com/gookit/goutil/testutil/assert" 11 | ) 12 | 13 | func TestExport(t *testing.T) { 14 | is := assert.New(t) 15 | c := New("test") 16 | 17 | str := c.ToJSON() 18 | is.Eq("", str) 19 | 20 | err := c.LoadStrings(JSON, jsonStr) 21 | is.Nil(err) 22 | 23 | str = c.ToJSON() 24 | is.Contains(str, `"name":"app"`) 25 | 26 | buf := &bytes.Buffer{} 27 | _, err = c.WriteTo(buf) 28 | is.Nil(err) 29 | 30 | // test dump 31 | buf = &bytes.Buffer{} 32 | _, err = c.DumpTo(buf, "invalid") 33 | is.Err(err) 34 | _, err = c.DumpTo(buf, Yml) 35 | is.Err(err) 36 | 37 | _, err = c.DumpTo(buf, JSON) 38 | is.Nil(err) 39 | } 40 | 41 | func TestDumpTo_encode_error(t *testing.T) { 42 | is := assert.New(t) 43 | c := NewEmpty("test") 44 | is.NoErr(c.Set("age", 34)) 45 | 46 | drv := NewDriver(JSON, JSONDecoder, func(v any) (out []byte, err error) { 47 | return nil, errors.New("encode data error") 48 | }) 49 | c.WithDriver(drv) 50 | 51 | // encode error 52 | buf := &bytes.Buffer{} 53 | _, err := c.DumpTo(buf, JSON) 54 | is.ErrMsg(err, "encode data error") 55 | 56 | is.Empty(c.ToJSON()) 57 | } 58 | 59 | func TestConfig_Structure(t *testing.T) { 60 | is := assert.New(t) 61 | 62 | cfg := Default() 63 | cfg.ClearAll() 64 | 65 | err := cfg.LoadStrings(JSON, `{ 66 | "age": 28, 67 | "name": "inhere", 68 | "sports": ["pingPong", "跑步"] 69 | }`) 70 | 71 | is.Nil(err) 72 | type User struct { 73 | Age int // always float64 from JSON 74 | Name string 75 | Sports []string 76 | } 77 | 78 | user := &User{} 79 | // map all data 80 | err = MapStruct("", user) 81 | is.Nil(err) 82 | 83 | is.Eq(28, user.Age) 84 | is.Eq("inhere", user.Name) 85 | is.Eq("pingPong", user.Sports[0]) 86 | 87 | // map all data 88 | u1 := &User{} 89 | err = Decode(u1) 90 | is.Nil(err) 91 | 92 | is.Eq(28, u1.Age) 93 | is.Eq("inhere", u1.Name) 94 | is.Eq("pingPong", u1.Sports[0]) 95 | 96 | // - auto convert string to int 97 | // age use string in JSON 98 | cfg1 := New("test") 99 | err = cfg1.LoadStrings(JSON, `{ 100 | "age": "26", 101 | "name": "inhere", 102 | "sports": ["pingPong", "跑步"] 103 | }`) 104 | 105 | is.Nil(err) 106 | 107 | user1 := &User{} 108 | err = cfg1.MapStruct("", user1) 109 | is.Nil(err) 110 | 111 | dump.P(*user1) 112 | 113 | // map some data 114 | err = cfg.LoadStrings(JSON, `{ 115 | "sec": { 116 | "key": "val", 117 | "age": 120, 118 | "tags": [12, 34] 119 | } 120 | }`) 121 | is.Nil(err) 122 | 123 | some := struct { 124 | Age int 125 | Key string 126 | Tags []int 127 | }{} 128 | err = BindStruct("sec", &some) 129 | is.Nil(err) 130 | is.Eq(120, some.Age) 131 | is.Eq(12, some.Tags[0]) 132 | cfg.ClearAll() 133 | 134 | // custom data 135 | cfg = New("test") 136 | err = cfg.LoadData(map[string]any{ 137 | "key": "val", 138 | "age": 120, 139 | "tags": []int{12, 34}, 140 | }) 141 | is.NoErr(err) 142 | 143 | s1 := struct { 144 | Age int 145 | Kye string 146 | Tags []int 147 | }{} 148 | err = cfg.BindStruct("", &s1) 149 | is.Nil(err) 150 | is.Eq(120, s1.Age) 151 | is.Eq(12, s1.Tags[0]) 152 | 153 | // key not exist 154 | err = cfg.BindStruct("not-exist", &s1) 155 | is.Err(err) 156 | is.Eq("this key does not exist in the config", err.Error()) 157 | 158 | // invalid dst 159 | err = cfg.BindStruct("sec", "invalid") 160 | is.Err(err) 161 | 162 | cfg.ClearAll() 163 | } 164 | 165 | func TestMapStruct_embedded_struct_squash_false(t *testing.T) { 166 | loader := NewWithOptions("test", func(options *Options) { 167 | options.DecoderConfig.TagName = "json" 168 | options.DecoderConfig.Squash = false 169 | }) 170 | assert.False(t, loader.Options().DecoderConfig.Squash) 171 | 172 | err := loader.LoadStrings(JSON, `{ 173 | "c": "12", 174 | "test1": { 175 | "b": "34" 176 | } 177 | }`) 178 | assert.NoErr(t, err) 179 | dump.Println(loader.Data()) 180 | assert.Eq(t, 12, loader.Int("c")) 181 | assert.Eq(t, 34, loader.Int("test1.b")) 182 | 183 | type Test1 struct { 184 | B int `json:"b"` 185 | } 186 | type Test2 struct { 187 | Test1 188 | C int `json:"c"` 189 | } 190 | cfg := &Test2{} 191 | 192 | err = loader.MapStruct("", cfg) 193 | assert.NoErr(t, err) 194 | dump.Println(cfg) 195 | assert.Eq(t, 34, cfg.Test1.B) 196 | 197 | type Test3 struct { 198 | *Test1 199 | C int `json:"c"` 200 | } 201 | cfg1 := &Test3{} 202 | err = loader.MapStruct("", cfg1) 203 | assert.NoErr(t, err) 204 | dump.Println(cfg1) 205 | assert.Eq(t, 34, cfg1.Test1.B) 206 | 207 | loader.SetData(map[string]any{ 208 | "c": 120, 209 | "b": 340, 210 | }) 211 | dump.Println(loader.Data()) 212 | 213 | cfg2 := &Test3{} 214 | err = loader.BindStruct("", cfg2) 215 | 216 | cfg3 := &Test3{} 217 | _ = jsonutil.DecodeString(`{"c": 12, "b": 34}`, cfg3) 218 | 219 | dump.Println(cfg2, cfg3) 220 | } 221 | 222 | func TestMapStruct_embedded_struct_squash_true(t *testing.T) { 223 | loader := NewWithOptions("test", func(options *Options) { 224 | options.DecoderConfig.TagName = "json" 225 | options.DecoderConfig.Squash = true 226 | }) 227 | assert.True(t, loader.Options().DecoderConfig.Squash) 228 | 229 | err := loader.LoadStrings(JSON, `{ 230 | "c": "12", 231 | "test1": { 232 | "b": "34" 233 | } 234 | }`) 235 | assert.NoErr(t, err) 236 | dump.Println(loader.Data()) 237 | assert.Eq(t, 12, loader.Int("c")) 238 | assert.Eq(t, 34, loader.Int("test1.b")) 239 | 240 | type Test1 struct { 241 | B int `json:"b"` 242 | } 243 | 244 | // use value - will not set ok 245 | type Test2 struct { 246 | Test1 247 | // Test1 `json:",squash"` 248 | C int `json:"c"` 249 | } 250 | cfg := &Test2{} 251 | 252 | err = loader.MapStruct("", cfg) 253 | assert.NoErr(t, err) 254 | dump.Println(cfg) 255 | assert.Eq(t, 0, cfg.Test1.B) 256 | 257 | // use pointer 258 | type Test3 struct { 259 | *Test1 260 | C int `json:"c"` 261 | } 262 | cfg1 := &Test3{} 263 | err = loader.MapStruct("", cfg1) 264 | assert.NoErr(t, err) 265 | dump.Println(cfg1) 266 | assert.Eq(t, 34, cfg1.B) 267 | assert.Eq(t, 34, cfg1.Test1.B) 268 | 269 | loader.SetData(map[string]any{ 270 | "c": 120, 271 | "b": 340, 272 | }) 273 | dump.Println(loader.Data()) 274 | 275 | cfg2 := &Test3{} 276 | err = loader.BindStruct("", cfg2) 277 | 278 | cfg3 := &Test3{} 279 | _ = jsonutil.DecodeString(`{"c": 12, "b": 34}`, cfg3) 280 | 281 | dump.Println(cfg2, cfg3) 282 | } 283 | 284 | func TestMapOnExists(t *testing.T) { 285 | cfg := Default() 286 | cfg.ClearAll() 287 | 288 | err := cfg.LoadStrings(JSON, `{ 289 | "age": 28, 290 | "name": "inhere", 291 | "sports": ["pingPong", "跑步"] 292 | }`) 293 | assert.NoErr(t, err) 294 | assert.NoErr(t, MapOnExists("not-exists", nil)) 295 | 296 | user := &struct { 297 | Age int 298 | Name string 299 | Sports []string 300 | }{} 301 | assert.NoErr(t, MapOnExists("", user)) 302 | 303 | assert.Eq(t, 28, user.Age) 304 | assert.Eq(t, "inhere", user.Name) 305 | } 306 | 307 | func TestConfig_BindStruct_set_DecoderConfig(t *testing.T) { 308 | cfg := NewWith("test", func(c *Config) { 309 | c.opts.DecoderConfig = nil 310 | }) 311 | err := cfg.LoadStrings(JSON, `{ 312 | "age": 28, 313 | "name": "inhere", 314 | "sports": ["pingPong", "跑步"] 315 | }`) 316 | assert.NoErr(t, err) 317 | 318 | user := &struct { 319 | Age int 320 | Name string 321 | Sports []string 322 | }{} 323 | assert.NoErr(t, cfg.BindStruct("", user)) 324 | 325 | assert.Eq(t, 28, user.Age) 326 | assert.Eq(t, "inhere", user.Name) 327 | 328 | // not use ptr 329 | assert.Err(t, cfg.BindStruct("", *user)) 330 | } 331 | 332 | func TestConfig_BindStruct_error(t *testing.T) { 333 | // cfg := NewEmpty() 334 | } 335 | 336 | func TestConfig_BindStruct_default(t *testing.T) { 337 | type MyConf struct { 338 | Env string `default:"${APP_ENV | dev}"` 339 | Debug bool `default:"${APP_DEBUG | false}"` 340 | } 341 | 342 | cfg := NewWithOptions("test", ParseEnv, ParseDefault) 343 | // cfg.SetData(map[string]any{ 344 | // "env": "prod", 345 | // "debug": "true", 346 | // }) 347 | 348 | mc := &MyConf{} 349 | err := cfg.Decode(mc) 350 | dump.P(mc) 351 | assert.NoErr(t, err) 352 | assert.Eq(t, "dev", mc.Env) 353 | assert.False(t, mc.Debug) 354 | } 355 | 356 | // test DumpToFile 357 | func TestConfig_DumpToFile(t *testing.T) { 358 | cfg := New("test") 359 | err := cfg.LoadStrings(JSON, `{ 360 | "age": 28, 361 | "name": "inhere", 362 | "sports": ["pingPong", "跑步"] 363 | }`) 364 | assert.NoErr(t, err) 365 | 366 | // open file error 367 | err = cfg.DumpToFile("not-exists/some.json", JSON) 368 | assert.Err(t, err) 369 | assert.ErrSubMsg(t, err, "open not-exists/some.json") // TIP: 不同go版本错误不一样 370 | 371 | // encoder error 372 | err = cfg.DumpToFile("./testdata/test_dump_file.yaml", Yaml) 373 | assert.Err(t, err) 374 | assert.ErrMsg(t, err, "not exists/register encoder for the format: yaml") 375 | } 376 | -------------------------------------------------------------------------------- /load_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "reflect" 7 | "runtime" 8 | "testing" 9 | 10 | "github.com/gookit/goutil/dump" 11 | "github.com/gookit/goutil/testutil" 12 | "github.com/gookit/goutil/testutil/assert" 13 | ) 14 | 15 | func TestDefaultLoad(t *testing.T) { 16 | is := assert.New(t) 17 | 18 | ClearAll() 19 | err := LoadFiles("testdata/json_base.json", "testdata/json_other.json") 20 | is.Nil(err) 21 | 22 | ClearAll() 23 | err = LoadFilesByFormat(JSON, "testdata/json_base.json", "testdata/json_other.json") 24 | is.Nil(err) 25 | 26 | ClearAll() 27 | err = LoadExists("testdata/json_base.json", "not-exist.json") 28 | is.Nil(err) 29 | 30 | ClearAll() 31 | err = LoadExistsByFormat(JSON, "testdata/json_base.json", "not-exist.json") 32 | is.Nil(err) 33 | 34 | ClearAll() 35 | // load map 36 | err = LoadData(map[string]any{ 37 | "name": "inhere", 38 | "age": 28, 39 | "working": true, 40 | "tags": []string{"a", "b"}, 41 | "info": map[string]string{"k1": "a", "k2": "b"}, 42 | }) 43 | is.NotEmpty(Data()) 44 | is.NotEmpty(Keys()) 45 | is.Empty(Sub("not-exist")) 46 | is.Nil(err) 47 | } 48 | 49 | func TestLoad(t *testing.T) { 50 | is := assert.New(t) 51 | 52 | var name string 53 | c := New("test"). 54 | WithOptions(WithHookFunc(func(event string, c *Config) { 55 | name = event 56 | })) 57 | err := c.LoadExists("testdata/json_base.json", "not-exist.json") 58 | is.Nil(err) 59 | 60 | c.ClearAll() 61 | is.Eq(OnCleanData, name) 62 | 63 | // load map data 64 | err = c.LoadData(map[string]any{ 65 | "name": "inhere", 66 | "age": float64(28), 67 | "working": true, 68 | "tags": []string{"a", "b"}, 69 | "info": map[string]string{"k1": "a", "k2": "b"}, 70 | }, map[string]string{"str-map": "value"}) 71 | 72 | is.Eq(OnLoadData, name) 73 | is.NotEmpty(c.Data()) 74 | is.Nil(err) 75 | is.Eq("value", c.String("str-map")) 76 | 77 | // LoadData 78 | err = c.LoadData("invalid") 79 | is.Err(err) 80 | 81 | is.Panics(func() { 82 | c.WithOptions(ParseEnv) 83 | }) 84 | 85 | err = c.LoadStrings(JSON, `{"name": "inhere"}`, jsonStr) 86 | is.Nil(err) 87 | 88 | // LoadSources 89 | err = c.LoadSources(JSON, []byte(`{"name": "inhere"}`), []byte(jsonStr)) 90 | is.Nil(err) 91 | 92 | err = c.LoadSources(JSON, []byte(`invalid`)) 93 | is.Err(err) 94 | 95 | err = c.LoadSources(JSON, []byte(`{"name": "inhere"}`), []byte(`invalid`)) 96 | is.Err(err) 97 | 98 | c = New("test") 99 | 100 | // LoadFiles 101 | err = c.LoadFiles("not-exist.json") 102 | is.Err(err) 103 | 104 | err = c.LoadFiles("testdata/json_error.json") 105 | is.Err(err) 106 | 107 | err = c.LoadExists("testdata/json_error.json") 108 | is.Err(err) 109 | 110 | // LoadStrings 111 | err = c.LoadStrings("invalid", jsonStr) 112 | is.Err(err) 113 | 114 | err = c.LoadStrings(JSON, "invalid") 115 | is.Err(err) 116 | 117 | err = c.LoadStrings(JSON, `{"name": "inhere"}`, "invalid") 118 | is.Err(err) 119 | } 120 | 121 | func TestLoad_error(t *testing.T) { 122 | is := assert.New(t) 123 | 124 | err := LoadFilesByFormat(Yaml, "testdata/json_base.json") 125 | is.Err(err) 126 | 127 | err = LoadExistsByFormat(Yaml, "testdata/json_base.json") 128 | is.Err(err) 129 | } 130 | 131 | func TestLoadRemote(t *testing.T) { 132 | is := assert.New(t) 133 | 134 | // invalid remote url 135 | url3 := "invalid-url" 136 | err := LoadRemote(JSON, url3) 137 | is.Err(err) 138 | 139 | if runtime.GOOS == "windows" { 140 | t.Skip("skip test load remote on Windows") 141 | return 142 | } 143 | 144 | // load remote config 145 | c := New("remote") 146 | url := "https://raw.githubusercontent.com/gookit/config/master/testdata/json_base.json" 147 | err = c.LoadRemote(JSON, url) 148 | is.Nil(err) 149 | is.Eq("123", c.String("age", "")) 150 | 151 | is.Len(c.LoadedUrls(), 1) 152 | is.Eq(url, c.LoadedUrls()[0]) 153 | 154 | // load invalid remote data 155 | url1 := "https://raw.githubusercontent.com/gookit/config/master/testdata/json_error.json" 156 | err = c.LoadRemote(JSON, url1) 157 | is.Err(err) 158 | 159 | // load not exist 160 | url2 := "https://raw.githubusercontent.com/gookit/config/master/testdata/not-exist.txt" 161 | err = c.LoadRemote(JSON, url2) 162 | is.Err(err) 163 | } 164 | 165 | func TestLoadFlags(t *testing.T) { 166 | is := assert.New(t) 167 | 168 | ClearAll() 169 | c := Default() 170 | bakArgs := os.Args 171 | defer func() { 172 | os.Args = bakArgs 173 | }() 174 | 175 | defines := []string{"name", 176 | "env:Set the run `env`", 177 | "debug:bool", "age:int", "var0:uint", 178 | "unknownTyp:notExist", 179 | "desc:string:This is a custom description: abc", 180 | } 181 | 182 | // custom global flag instance 183 | flag.CommandLine = flag.NewFlagSet("binFile", flag.ContinueOnError) 184 | 185 | t.Run("show help", func(t *testing.T) { 186 | // show help 187 | os.Args = []string{"./binFile", "--help"} 188 | is.Nil(LoadFlags(defines)) 189 | // reset flag instance 190 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 191 | }) 192 | 193 | t.Run("load flags", func(t *testing.T) { 194 | // --name inhere --env dev --age 99 --debug 195 | os.Args = []string{ 196 | "./binFile", 197 | "--env", "dev", 198 | "--age", "99", 199 | "--var0", "12", 200 | "--name", "inhere", 201 | "--unknownTyp", "val", 202 | "--debug", 203 | "--desc=abc", 204 | } 205 | 206 | // load flag info 207 | err := LoadFlags(defines) 208 | is.Nil(err) 209 | is.Eq("inhere", c.String("name", "")) 210 | is.Eq("dev", c.String("env", "")) 211 | is.Eq(99, c.Int("age")) 212 | is.Eq(uint(12), c.Uint("var0")) 213 | is.Eq(uint(20), c.Uint("not-exist", uint(20))) 214 | is.Eq("val", c.Get("unknownTyp")) 215 | is.True(c.Bool("debug", false)) 216 | is.Eq("abc", c.String("desc")) 217 | descFlag := flag.Lookup("desc") 218 | is.NotNil(descFlag) 219 | is.Eq("This is a custom description: abc", descFlag.Usage) 220 | }) 221 | 222 | t.Run("set sub key", func(t *testing.T) { 223 | // set sub key 224 | c = New("flag") 225 | _ = c.LoadStrings(JSON, jsonStr) 226 | os.Args = []string{ 227 | "./binFile", 228 | "--map1.key", "new val", 229 | } 230 | is.Eq("val", c.String("map1.key")) 231 | err := c.LoadFlags([]string{"--map1.key"}) 232 | is.NoErr(err) 233 | is.Eq("new val", c.String("map1.key")) 234 | }) 235 | // fmt.Println(err) 236 | // fmt.Printf("%#v\n", c.Data()) 237 | } 238 | 239 | func TestLoadOSEnv(t *testing.T) { 240 | ClearAll() 241 | 242 | testutil.MockEnvValues(map[string]string{ 243 | "APP_NAME": "config", 244 | "app_debug": "true", 245 | "test_env0": "val0", 246 | "TEST_ENV1": "val1", 247 | }, func() { 248 | assert.Eq(t, "", String("test_env0")) 249 | 250 | LoadOSEnv([]string{"APP_NAME", "app_debug", "test_env0"}, true) 251 | 252 | assert.True(t, Bool("app_debug")) 253 | assert.Eq(t, "config", String("app_name")) 254 | assert.Eq(t, "val0", String("test_env0")) 255 | assert.Eq(t, "", String("test_env1")) 256 | }) 257 | 258 | ClearAll() 259 | } 260 | 261 | func TestLoadOSEnvs(t *testing.T) { 262 | ClearAll() 263 | 264 | testutil.MockEnvValues(map[string]string{ 265 | "APP_NAME": "config", 266 | "APP_DEBUG": "true", 267 | "TEST_ENV0": "val0", 268 | "TEST_ENV1": "val1", 269 | }, func() { 270 | assert.Eq(t, "", String("test_env0")) 271 | assert.Eq(t, "val0", Getenv("TEST_ENV0")) 272 | 273 | LoadOSEnvs(map[string]string{ 274 | "APP_NAME": "", 275 | "APP_DEBUG": "app_debug", 276 | "TEST_ENV0": "test0", 277 | }) 278 | 279 | assert.True(t, Bool("app_debug")) 280 | assert.Eq(t, "config", String("app_name")) 281 | assert.Eq(t, "val0", String("test0")) 282 | assert.Eq(t, "", String("test_env1")) 283 | }) 284 | 285 | ClearAll() 286 | } 287 | 288 | func TestLoadFromDir(t *testing.T) { 289 | ClearAll() 290 | assert.NoErr(t, LoadStrings(JSON, `{ 291 | "topKey": "a value" 292 | }`)) 293 | 294 | assert.NoErr(t, LoadFromDir("testdata/subdir", JSON)) 295 | dump.P(Data()) 296 | assert.Eq(t, "value in sub data", Get("subdata.key01")) 297 | assert.Eq(t, "value in task.json", Get("task.key01")) 298 | 299 | ClearAll() 300 | assert.NoErr(t, LoadFromDir("testdata/emptydir", JSON)) 301 | 302 | // with DataKey option. see https://github.com/gookit/config/issues/173 303 | assert.NoErr(t, LoadFromDir("testdata/subdir", JSON, func(lo *LoadOptions) { 304 | lo.DataKey = "dataList" 305 | })) 306 | dump.P(Data()) 307 | dl := Get("dataList") 308 | assert.NotNil(t, dl) 309 | assert.IsKind(t, reflect.Slice, dl) 310 | ClearAll() 311 | } 312 | 313 | func TestReloadFiles(t *testing.T) { 314 | ClearAll() 315 | c := Default() 316 | // no loaded files 317 | assert.NoErr(t, ReloadFiles()) 318 | 319 | var eventName string 320 | c.WithOptions(WithHookFunc(func(event string, c *Config) { 321 | eventName = event 322 | })) 323 | 324 | // load files 325 | err := LoadFiles("testdata/json_base.json", "testdata/json_other.json") 326 | assert.NoErr(t, err) 327 | assert.Eq(t, OnLoadData, eventName) 328 | assert.NotEmpty(t, c.LoadedFiles()) 329 | assert.Eq(t, "app2", c.String("name")) 330 | 331 | // set value 332 | assert.NoErr(t, c.Set("name", "new value")) 333 | assert.Eq(t, OnSetValue, eventName) 334 | assert.Eq(t, "new value", c.String("name")) 335 | 336 | // reload files 337 | assert.NoErr(t, ReloadFiles()) 338 | assert.Eq(t, OnReloadData, eventName) 339 | 340 | // value is reverted 341 | assert.Eq(t, "app2", c.String("name")) 342 | ClearAll() 343 | } 344 | -------------------------------------------------------------------------------- /read_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/gookit/goutil/testutil" 9 | "github.com/gookit/goutil/testutil/assert" 10 | ) 11 | 12 | func TestConfig_GetValue(t *testing.T) { 13 | is := assert.New(t) 14 | 15 | ClearAll() 16 | err := LoadStrings(JSON, jsonStr) 17 | is.Nil(err) 18 | 19 | c := Default() 20 | 21 | // error on get 22 | _, ok := GetValue("") 23 | is.False(ok) 24 | 25 | _, ok = c.GetValue("notExist") 26 | is.False(ok) 27 | _, ok = c.GetValue("name.sub") 28 | is.False(ok) 29 | is.Err(c.Error()) 30 | 31 | _, ok = c.GetValue("map1.key", false) 32 | is.False(ok) 33 | is.False(Exists("map1.key", false)) 34 | 35 | val, ok := GetValue("map1.notExist") 36 | is.Nil(val) 37 | is.False(ok) 38 | is.False(Exists("map1.notExist")) 39 | 40 | val, ok = GetValue("notExist.sub") 41 | is.False(ok) 42 | is.Nil(val) 43 | is.False(Exists("notExist.sub")) 44 | 45 | val, ok = c.GetValue("arr1.100") 46 | is.Nil(val) 47 | is.False(ok) 48 | is.False(Exists("arr1.100")) 49 | 50 | val, ok = c.GetValue("arr1.notExist") 51 | is.Nil(val) 52 | is.False(ok) 53 | is.False(Exists("arr1.notExist")) 54 | 55 | // load data for tests 56 | err = c.LoadData(map[string]any{ 57 | "setStrMap": map[string]string{ 58 | "k": "v", 59 | }, 60 | "setIntMap": map[string]int{ 61 | "k2": 23, 62 | }, 63 | }) 64 | is.Nil(err) 65 | // -- assert map[string]string 66 | is.True(Exists("setStrMap.k")) 67 | is.Eq("v", Get("setStrMap.k")) 68 | is.False(Exists("setStrMap.k1")) 69 | 70 | // -- assert map[string]int 71 | is.True(Exists("setIntMap.k2")) 72 | is.Eq(23, Get("setIntMap.k2")) 73 | is.False(Exists("setIntMap.k1")) 74 | 75 | ClearAll() 76 | } 77 | 78 | func TestGet(t *testing.T) { 79 | is := assert.New(t) 80 | 81 | ClearAll() 82 | err := LoadStrings(JSON, jsonStr) 83 | is.Nil(err) 84 | 85 | // fmt.Printf("%#v\n", Data()) 86 | c := Default() 87 | 88 | is.False(c.IsEmpty()) 89 | is.True(Exists("age")) 90 | is.True(Exists("map1.key")) 91 | is.True(Exists("arr1.1")) 92 | is.False(Exists("arr1.1", false)) 93 | is.False(Exists("not-exist.sub")) 94 | is.False(Exists("")) 95 | is.False(Exists("not-exist")) 96 | 97 | // get value 98 | val := Get("age") 99 | is.Eq(float64(123), val) 100 | is.Eq("float64", fmt.Sprintf("%T", val)) 101 | 102 | val = Get("not-exist") 103 | is.Nil(val) 104 | 105 | val = Get("name") 106 | is.Eq("app", val) 107 | 108 | is.Eq([]string{"php", "go"}, StringsBySplit("tagsStr", ",")) 109 | 110 | // get string array 111 | arr := Strings("notExist") 112 | is.Empty(arr) 113 | 114 | arr = Strings("map1") 115 | is.Empty(arr) 116 | 117 | arr = Strings("arr1") 118 | is.Eq(`[]string{"val", "val1", "val2"}`, fmt.Sprintf("%#v", arr)) 119 | 120 | val = String("arr1.1") 121 | is.Eq("val1", val) 122 | 123 | err = LoadStrings(JSON, `{ 124 | "iArr": [12, 34, 36], 125 | "iMap": {"k1": 12, "k2": 34, "k3": 36} 126 | }`) 127 | is.Nil(err) 128 | 129 | // Ints: get int arr 130 | iarr := Ints("name") 131 | is.False(Exists("name.1")) 132 | is.Empty(iarr) 133 | 134 | iarr = Ints("notExist") 135 | is.Empty(iarr) 136 | 137 | iarr = Ints("iArr") 138 | is.Eq(`[]int{12, 34, 36}`, fmt.Sprintf("%#v", iarr)) 139 | 140 | iv := Int("iArr.1") 141 | is.Eq(34, iv) 142 | 143 | iv = Int("iArr.100") 144 | is.Eq(0, iv) 145 | 146 | // IntMap: get int map 147 | imp := IntMap("name") 148 | is.Empty(imp) 149 | imp = IntMap("notExist") 150 | is.Empty(imp) 151 | 152 | imp = IntMap("iMap") 153 | is.NotEmpty(imp) 154 | 155 | iv = Int("iMap.k2") 156 | is.Eq(34, iv) 157 | is.True(Exists("iMap.k2")) 158 | 159 | iv = Int("iMap.notExist") 160 | is.Eq(0, iv) 161 | is.False(Exists("iMap.notExist")) 162 | 163 | // set a intMap 164 | err = Set("intMap0", map[string]int{"a": 1, "b": 2}) 165 | is.Nil(err) 166 | 167 | imp = IntMap("intMap0") 168 | is.NotEmpty(imp) 169 | is.Eq(1, imp["a"]) 170 | is.Eq(2, Get("intMap0.b")) 171 | is.True(Exists("intMap0.a")) 172 | is.False(Exists("intMap0.c")) 173 | 174 | // StringMap: get string map 175 | smp := StringMap("map1") 176 | is.Eq("val1", smp["key1"]) 177 | 178 | // like load from yaml content 179 | // c = New("test") 180 | err = c.LoadData(map[string]any{ 181 | "newIArr": []int{2, 3}, 182 | "newSArr": []string{"a", "b"}, 183 | "newIArr1": []any{12, 23}, 184 | "newIArr2": []any{12, "abc"}, 185 | "invalidMap": map[string]int{"k": 1}, 186 | "yMap": map[any]any{ 187 | "k0": "v0", 188 | "k1": 23, 189 | }, 190 | "yMap1": map[any]any{ 191 | "k": "v", 192 | "k1": 23, 193 | "k2": []any{23, 45}, 194 | }, 195 | "yMap10": map[string]any{ 196 | "k": "v", 197 | "k1": 23, 198 | "k2": []any{23, 45}, 199 | }, 200 | "yMap2": map[any]any{ 201 | "k": 2, 202 | "k1": 23, 203 | }, 204 | "yArr": []any{23, 45, "val", map[string]any{"k4": "v4"}}, 205 | }) 206 | is.Nil(err) 207 | 208 | iarr = Ints("newIArr") 209 | is.Eq("[2 3]", fmt.Sprintf("%v", iarr)) 210 | 211 | iarr = Ints("newIArr1") 212 | is.Eq("[12 23]", fmt.Sprintf("%v", iarr)) 213 | iarr = Ints("newIArr2") 214 | is.Empty(iarr) 215 | 216 | iv = Int("newIArr.1") 217 | is.True(Exists("newIArr.1")) 218 | is.Eq(3, iv) 219 | 220 | iv = Int("newIArr.200") 221 | is.False(Exists("newIArr.200")) 222 | is.Eq(0, iv) 223 | 224 | // invalid intMap 225 | imp = IntMap("yMap1") 226 | is.Empty(imp) 227 | 228 | imp = IntMap("yMap10") 229 | is.Empty(imp) 230 | 231 | imp = IntMap("yMap2") 232 | is.Eq(2, imp["k"]) 233 | 234 | val = String("newSArr.1") 235 | is.True(Exists("newSArr.1")) 236 | is.Eq("b", val) 237 | 238 | val = String("newSArr.100") 239 | is.False(Exists("newSArr.100")) 240 | is.Eq("", val) 241 | 242 | smp = StringMap("invalidMap") 243 | is.Nil(smp) 244 | 245 | smp = StringMap("yMap.notExist") 246 | is.Nil(smp) 247 | 248 | smp = StringMap("yMap") 249 | is.True(Exists("yMap.k0")) 250 | is.False(Exists("yMap.k100")) 251 | is.Eq("v0", smp["k0"]) 252 | 253 | iarr = Ints("yMap1.k2") 254 | is.Eq("[23 45]", fmt.Sprintf("%v", iarr)) 255 | } 256 | 257 | func TestInt(t *testing.T) { 258 | is := assert.New(t) 259 | ClearAll() 260 | _ = LoadStrings(JSON, jsonStr) 261 | 262 | is.True(Exists("age")) 263 | 264 | iv := Int("age") 265 | is.Eq(123, iv) 266 | 267 | iv = Int("name") 268 | is.Eq(0, iv) 269 | 270 | iv = Int("notExist", 34) 271 | is.Eq(34, iv) 272 | 273 | c := Default() 274 | iv = c.Int("age") 275 | is.Eq(123, iv) 276 | iv = c.Int("notExist") 277 | is.Eq(0, iv) 278 | 279 | uiv := Uint("age") 280 | is.Eq(uint(123), uiv) 281 | 282 | dur := Duration("age") 283 | is.Eq(time.Duration(123), dur) 284 | is.Eq(time.Duration(340), Duration("not-exist", 340)) 285 | 286 | ClearAll() 287 | } 288 | 289 | func TestInt64(t *testing.T) { 290 | is := assert.New(t) 291 | ClearAll() 292 | _ = LoadStrings(JSON, jsonStr) 293 | 294 | // get int64 295 | iv64 := Int64("age") 296 | is.Eq(int64(123), iv64) 297 | 298 | iv64 = Int64("name") 299 | is.Eq(iv64, int64(0)) 300 | 301 | iv64 = Int64("age", 34) 302 | is.Eq(int64(123), iv64) 303 | iv64 = Int64("notExist", 34) 304 | is.Eq(int64(34), iv64) 305 | 306 | c := Default() 307 | iv64 = c.Int64("age") 308 | is.Eq(int64(123), iv64) 309 | iv64 = c.Int64("notExist") 310 | is.Eq(int64(0), iv64) 311 | 312 | ClearAll() 313 | } 314 | 315 | func TestFloat(t *testing.T) { 316 | is := assert.New(t) 317 | ClearAll() 318 | _ = LoadStrings(JSON, jsonStr) 319 | c := Default() 320 | 321 | // get float 322 | err := c.Set("flVal", 23.45) 323 | is.Nil(err) 324 | flt := c.Float("flVal") 325 | is.Eq(23.45, flt) 326 | 327 | flt = Float("name") 328 | is.Eq(float64(0), flt) 329 | 330 | flt = c.Float("notExists") 331 | is.Eq(float64(0), flt) 332 | 333 | flt = c.Float("notExists", 10) 334 | is.Eq(float64(10), flt) 335 | 336 | flt = Float("flVal", 0) 337 | is.Eq(23.45, flt) 338 | 339 | ClearAll() 340 | } 341 | 342 | func TestString(t *testing.T) { 343 | is := assert.New(t) 344 | ClearAll() 345 | _ = LoadStrings(JSON, jsonStr) 346 | 347 | // get string 348 | val := String("arr1") 349 | is.Eq("[val val1 val2]", val) 350 | 351 | str := String("notExists") 352 | is.Eq("", str) 353 | is.Panics(func() { 354 | MustString("notExists") 355 | }) 356 | 357 | str = String("notExists", "defVal") 358 | is.Eq("defVal", str) 359 | 360 | c := Default() 361 | str = c.String("name") 362 | is.Eq("app", str) 363 | str = c.String("notExist") 364 | is.Eq("", str) 365 | 366 | ClearAll() 367 | } 368 | 369 | func TestBool(t *testing.T) { 370 | is := assert.New(t) 371 | ClearAll() 372 | _ = LoadSources(JSON, []byte(jsonStr)) 373 | 374 | // get bool 375 | val := Get("debug") 376 | is.Eq(true, val) 377 | 378 | bv := Bool("debug") 379 | is.Eq(true, bv) 380 | 381 | bv = Bool("age") 382 | is.Eq(false, bv) 383 | 384 | bv = Bool("debug", false) 385 | is.Eq(true, bv) 386 | 387 | bv = Bool("notExist", false) 388 | is.Eq(false, bv) 389 | 390 | c := Default() 391 | bv = c.Bool("debug") 392 | is.True(bv) 393 | bv = c.Bool("notExist") 394 | is.False(bv) 395 | 396 | ClearAll() 397 | } 398 | 399 | func TestSubDataMap(t *testing.T) { 400 | is := assert.New(t) 401 | ClearAll() 402 | _ = LoadSources(JSON, []byte(jsonStr)) 403 | 404 | mp := SubDataMap("map1") 405 | is.NotEmpty(mp) 406 | is.Eq("230", mp.Get("key4")) 407 | is.Eq(230, mp.Int("key4")) 408 | 409 | mp = SubDataMap("notExist") 410 | is.Empty(mp) 411 | 412 | ClearAll() 413 | } 414 | 415 | func TestParseEnv(t *testing.T) { 416 | is := assert.New(t) 417 | 418 | cfg := NewWithOptions("test", ParseEnv) 419 | err := cfg.LoadStrings(JSON, `{ 420 | "ekey": "${EnvKey}", 421 | "ekey0": "${ EnvKey0 }", 422 | "ekey1": "${EnvKey1|defValue}", 423 | "ekey2": "${ EnvKey2 | defValue1 }", 424 | "ekey3": "${ EnvKey3 | app:run }", 425 | "ekey4": "${FirstEnv}/${ SecondEnv }", 426 | "ekey5": "${TEST_SHELL|/bin/bash}", 427 | "ekey6": "${ EnvKey6 | app=run }", 428 | "ekey7": "${ EnvKey7 | app.run }", 429 | "ekey8": "${ EnvKey8 | app/run }" 430 | }`) 431 | 432 | is.NoErr(err) 433 | 434 | tests := []struct{ EKey, EVal, CKey, CVal string }{ 435 | {"EnvKey", "EnvKey val", "ekey", "EnvKey val"}, 436 | {"EnvKey", "", "ekey", ""}, 437 | {"EnvKey0", "EnvKey0 val", "ekey0", "EnvKey0 val"}, 438 | {"EnvKey3", "EnvKey3 val", "ekey3", "EnvKey3 val"}, 439 | {"EnvKey3", "", "ekey3", "app:run"}, 440 | {"EnvKey6", "", "ekey6", "app=run"}, 441 | {"EnvKey7", "", "ekey7", "app.run"}, 442 | {"EnvKey8", "", "ekey8", "app/run"}, 443 | {"TEST_SHELL", "/bin/zsh", "ekey5", "/bin/zsh"}, 444 | {"TEST_SHELL", "", "ekey5", "/bin/bash"}, 445 | } 446 | 447 | for _, smp := range tests { 448 | is.Eq("", Getenv(smp.EKey)) 449 | 450 | testutil.MockEnvValue(smp.EKey, smp.EVal, func(eVal string) { 451 | is.Eq(smp.EVal, eVal) 452 | is.Eq(smp.CVal, cfg.String(smp.CKey)) 453 | }) 454 | } 455 | 456 | // test multi ENV key 457 | is.Eq("", Getenv("FirstEnv")) 458 | 459 | testutil.MockEnvValues(map[string]string{ 460 | "FirstEnv": "abc", 461 | "SecondEnv": "def", 462 | }, func() { 463 | is.Eq("abc", Getenv("FirstEnv")) 464 | is.Eq("def", Getenv("SecondEnv")) 465 | is.Eq("abc/def", cfg.String("ekey4")) 466 | }) 467 | 468 | testutil.MockEnvValues(map[string]string{ 469 | "FirstEnv": "abc", 470 | }, func() { 471 | is.Eq("abc", Getenv("FirstEnv")) 472 | is.Eq("", Getenv("SecondEnv")) 473 | is.Eq("abc/", cfg.String("ekey4")) 474 | }) 475 | } 476 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "github.com/gookit/goutil/maputil" 10 | "github.com/gookit/goutil/testutil" 11 | "github.com/gookit/goutil/testutil/assert" 12 | ) 13 | 14 | var jsonStr = `{ 15 | "name": "app", 16 | "debug": true, 17 | "baseKey": "value", 18 | "tagsStr": "php,go", 19 | "age": 123, 20 | "envKey": "${SHELL}", 21 | "envKey1": "${NotExist|defValue}", 22 | "invalidEnvKey": "${noClose", 23 | "map1": { 24 | "key": "val", 25 | "key1": "val1", 26 | "key2": "val2", 27 | "key4": "230", 28 | "key3": "${SHELL}" 29 | }, 30 | "arr1": [ 31 | "val", 32 | "val1", 33 | "val2" 34 | ] 35 | }` 36 | 37 | func Example() { 38 | // WithOptions(ParseEnv) 39 | 40 | // use yaml github.com/gookit/config/yamlv3 41 | // AddDriver(Yaml, yamlv3.Driver) 42 | // use toml github.com/gookit/config/toml 43 | // AddDriver(Toml, toml.Driver) 44 | // use toml github.com/gookit/config/hcl 45 | // AddDriver(Hcl, hcl.Driver) 46 | 47 | err := LoadFiles("testdata/json_base.json") 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | // fmt.Printf("config data: \n %#v\n", Data()) 53 | 54 | err = LoadFiles("testdata/json_other.json") 55 | // LoadFiles("testdata/json_base.json", "testdata/json_other.json") 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | // load from string 61 | err = LoadSources(JSON, []byte(jsonStr)) 62 | if err != nil { 63 | panic(err) 64 | } 65 | 66 | // fmt.Printf("config data: \n %#v\n", Data()) 67 | fmt.Print("get config example:\n") 68 | 69 | name := String("name") 70 | fmt.Printf("- get string\n val: %v\n", name) 71 | 72 | arr1 := Strings("arr1") 73 | fmt.Printf("- get array\n val: %#v\n", arr1) 74 | 75 | val0 := String("arr1.0") 76 | fmt.Printf("- get sub-value by path 'arr.index'\n val: %#v\n", val0) 77 | 78 | map1 := StringMap("map1") 79 | fmt.Printf("- get map\n val: %#v\n", map1) 80 | 81 | val0 = String("map1.key") 82 | fmt.Printf("- get sub-value by path 'map.key'\n val: %#v\n", val0) 83 | 84 | // can parse env name(ParseEnv: true) 85 | fmt.Printf("get env 'envKey' val: %s\n", String("envKey", "")) 86 | fmt.Printf("get env 'envKey1' val: %s\n", String("envKey1", "")) 87 | 88 | // set value 89 | _ = Set("name", "new name") 90 | name = String("name") 91 | fmt.Printf("- set string\n val: %v\n", name) 92 | 93 | // if you want export config data 94 | // buf := new(bytes.Buffer) 95 | // _, err = config.DumpTo(buf, config.JSON) 96 | // if err != nil { 97 | // panic(err) 98 | // } 99 | // fmt.Printf("export config:\n%s", buf.String()) 100 | 101 | // Out: 102 | // get config example: 103 | // - get string 104 | // val: app 105 | // - get array 106 | // val: []string{"val", "val1", "val2"} 107 | // - get sub-value by path 'arr.index' 108 | // val: "val" 109 | // - get map 110 | // val: map[string]string{"key":"val", "key1":"val1", "key2":"val2"} 111 | // - get sub-value by path 'map.key' 112 | // val: "val" 113 | // get env 'envKey' val: /bin/zsh 114 | // get env 'envKey1' val: defValue 115 | // - set string 116 | // val: new name 117 | } 118 | 119 | func Example_exportConfig() { 120 | // Notice: before dump please set driver encoder 121 | // SetEncoder(Yaml, yaml.Encoder) 122 | 123 | ClearAll() 124 | // load from string 125 | err := LoadStrings(JSON, `{ 126 | "name": "app", 127 | "age": 34 128 | }`) 129 | if err != nil { 130 | panic(err) 131 | } 132 | 133 | buf := new(bytes.Buffer) 134 | _, err = DumpTo(buf, JSON) 135 | if err != nil { 136 | panic(err) 137 | } 138 | 139 | fmt.Printf("%s", buf.String()) 140 | 141 | // Output: 142 | // {"age":34,"name":"app"} 143 | } 144 | 145 | func BenchmarkGet(b *testing.B) { 146 | err := LoadStrings(JSON, jsonStr) 147 | if err != nil { 148 | panic(err) 149 | } 150 | 151 | b.ResetTimer() 152 | for i := 0; i < b.N; i++ { 153 | Get("name") 154 | } 155 | } 156 | 157 | func TestBasic(t *testing.T) { 158 | is := assert.New(t) 159 | 160 | ClearAll() 161 | c := Default() 162 | is.True(c.HasDecoder(JSON)) 163 | is.True(c.HasEncoder(JSON)) 164 | is.Eq("default", c.Name()) 165 | is.NoErr(c.Error()) 166 | 167 | c = NewWithOptions("test", Readonly, WithTagName("mytag")) 168 | opts := c.Options() 169 | is.True(opts.Readonly) 170 | is.Eq(JSON, opts.DumpFormat) 171 | is.Eq(JSON, opts.ReadFormat) 172 | is.Eq("mytag", opts.TagName) 173 | } 174 | 175 | func TestGetEnv(t *testing.T) { 176 | testutil.MockEnvValues(map[string]string{ 177 | "APP_NAME": "config", 178 | "APP_DEBUG": "true", 179 | }, func() { 180 | assert.Eq(t, "config", Getenv("APP_NAME")) 181 | assert.Eq(t, "true", Getenv("APP_DEBUG")) 182 | assert.Eq(t, "defVal", GetEnv("not-exsit", "defVal")) 183 | }) 184 | } 185 | 186 | func TestSetDecoderEncoder(t *testing.T) { 187 | is := assert.New(t) 188 | 189 | c := Default() 190 | c.ClearAll() 191 | 192 | is.True(c.HasDecoder(JSON)) 193 | is.True(c.HasEncoder(JSON)) 194 | 195 | c.DelDriver(JSON) 196 | 197 | is.False(c.HasDecoder(JSON)) 198 | is.False(c.HasEncoder(JSON)) 199 | 200 | SetDecoder(JSON, JSONDecoder) 201 | SetEncoder(JSON, JSONEncoder) 202 | 203 | is.True(c.HasDecoder(JSON)) 204 | is.True(c.HasEncoder(JSON)) 205 | } 206 | 207 | func TestDefault(t *testing.T) { 208 | is := assert.New(t) 209 | 210 | ClearAll() 211 | WithOptions(ParseEnv) 212 | is.True(GetOptions().ParseEnv) 213 | 214 | _ = LoadStrings(JSON, `{"name": "inhere"}`) 215 | 216 | buf := &bytes.Buffer{} 217 | _, err := WriteTo(buf) 218 | is.Nil(err) 219 | 220 | // add alias 221 | AddAlias("ini", "conf") 222 | is.NotEmpty(Default().AliasMap()) 223 | } 224 | 225 | func TestJSONDriver(t *testing.T) { 226 | is := assert.New(t) 227 | is.Eq("json", JSONDriver.Name()) 228 | 229 | // empty 230 | c := NewEmpty("test") 231 | is.False(c.HasDecoder(JSON)) 232 | 233 | c.AddDriver(JSONDriver) 234 | is.True(c.HasDecoder(JSON)) 235 | is.True(c.HasEncoder(JSON)) 236 | is.Len(c.DriverNames(), 1) 237 | 238 | is.Eq(byte('.'), c.Options().Delimiter) 239 | is.Eq(".", string(c.Options().Delimiter)) 240 | c.WithOptions(func(opt *Options) { 241 | opt.Delimiter = 0 242 | }) 243 | is.Eq(byte(0), c.Options().Delimiter) 244 | 245 | err := c.LoadStrings(JSON, `{"key": 1}`) 246 | is.NoErr(err) 247 | is.Eq(1, c.Int("key")) 248 | 249 | c = NewWith("test", func(c *Config) { 250 | err = c.LoadData(map[string]any{"key1": 2}) 251 | is.NoErr(err) 252 | }) 253 | is.Eq(2, c.Int("key1")) 254 | 255 | // test call JSONDriver.Encode 256 | s := c.ToJSON() 257 | is.StrContains(s, `{"key1":2}`) 258 | 259 | // set MarshalIndent 260 | JSONDriver.MarshalIndent = " " 261 | s = c.ToJSON() 262 | is.StrContains(s, ` "key1": 2`) 263 | JSONDriver.MarshalIndent = "" // reset 264 | } 265 | 266 | func TestDriver(t *testing.T) { 267 | is := assert.New(t) 268 | 269 | c := Default() 270 | is.True(c.HasDecoder(JSON)) 271 | is.True(c.HasEncoder(JSON)) 272 | 273 | c.DelDriver(JSON) 274 | is.False(c.HasDecoder(JSON)) 275 | is.False(c.HasEncoder(JSON)) 276 | 277 | AddDriver(JSONDriver) 278 | is.True(c.HasDecoder(JSON)) 279 | is.True(c.HasEncoder(JSON)) 280 | 281 | c.DelDriver(JSON) 282 | is.False(c.HasDecoder(JSON)) 283 | is.False(c.HasEncoder(JSON)) 284 | 285 | WithDriver(JSONDriver) 286 | is.True(c.HasDecoder(JSON)) 287 | is.True(c.HasEncoder(JSON)) 288 | 289 | c.DelDriver(JSON) 290 | 291 | c.SetDecoders(map[string]Decoder{JSON: JSONDecoder}) 292 | c.SetEncoders(map[string]Encoder{JSON: JSONEncoder}) 293 | is.True(c.HasDecoder(JSON)) 294 | is.True(c.HasEncoder(JSON)) 295 | } 296 | 297 | func TestStdDriver_methods(t *testing.T) { 298 | d1 := NewDriver("my001", JSONDecoder, JSONEncoder) 299 | d1.WithAlias("json") 300 | assert.Eq(t, "my001", d1.Name()) 301 | assert.Contains(t, d1.Aliases(), "json") 302 | 303 | s := `{"age": 245}` 304 | m := make(maputil.Map) 305 | err := d1.Decode([]byte(s), &m) 306 | assert.NoErr(t, err) 307 | 308 | bs, err := d1.Encode(m) 309 | assert.NoErr(t, err) 310 | assert.StrContains(t, string(bs), `{"age":245}`) 311 | } 312 | 313 | func TestOptions(t *testing.T) { 314 | is := assert.New(t) 315 | 316 | // options: ParseEnv 317 | c := New("test") 318 | c.WithOptions(ParseEnv) 319 | 320 | is.True(c.Options().ParseEnv) 321 | 322 | err := c.LoadStrings(JSON, jsonStr) 323 | is.Nil(err) 324 | 325 | str := c.String("name") 326 | is.Eq("app", str) 327 | 328 | // test: parse env name 329 | shell := os.Getenv("SHELL") 330 | // ensure env var is exist 331 | if shell == "" { 332 | _ = os.Setenv("SHELL", "/usr/bin/bash") 333 | } 334 | 335 | str = c.String("envKey") 336 | is.NotContains(str, "${") 337 | 338 | // revert 339 | if shell != "" { 340 | _ = os.Setenv("SHELL", shell) 341 | } 342 | 343 | str = c.String("invalidEnvKey") 344 | is.Contains(str, "${") 345 | 346 | str = c.String("envKey1") 347 | is.NotContains(str, "${") 348 | is.Eq("defValue", str) 349 | 350 | // options: Readonly 351 | c = New("test") 352 | c.WithOptions(Readonly) 353 | 354 | is.True(c.Options().Readonly) 355 | 356 | err = c.LoadStrings(JSON, jsonStr) 357 | is.Nil(err) 358 | 359 | str = c.String("name") 360 | is.Eq("app", str) 361 | 362 | err = c.Set("name", "new app") 363 | is.Err(err) 364 | } 365 | 366 | func TestDelimiter(t *testing.T) { 367 | // options: Delimiter 368 | is := assert.New(t) 369 | c := New("test") 370 | c.WithOptions(Delimiter(':')) 371 | is.Eq(byte(':'), c.Options().Delimiter) 372 | 373 | err := c.LoadData(map[string]any{ 374 | "top0": 1, 375 | "top1": map[string]int{"sub0": 2}, 376 | }) 377 | is.NoErr(err) 378 | // is.Eq(1, c.Int("top0")) 379 | is.Eq(2, c.Int("top1:sub0")) 380 | 381 | // load will use defaultDelimiter 382 | c = NewWithOptions("test", Delimiter(0)) 383 | is.Eq(byte(0), c.Options().Delimiter) 384 | 385 | err = c.LoadData(map[string]any{ 386 | "top0": 1, 387 | "top1": map[string]int{"sub0": 2}, 388 | }) 389 | is.NoErr(err) 390 | is.Eq(2, c.Int("top1.sub0")) 391 | } 392 | 393 | func TestEnableCache(t *testing.T) { 394 | is := assert.New(t) 395 | 396 | c := NewWithOptions("test", EnableCache) 397 | err := c.LoadStrings(JSON, jsonStr) 398 | is.Nil(err) 399 | 400 | str := c.String("name") 401 | is.Eq("app", str) 402 | 403 | // re-get, from caches 404 | str = c.String("name") 405 | is.Eq("app", str) 406 | 407 | sArr := c.Strings("arr1") 408 | is.Eq("val1", sArr[1]) 409 | 410 | // re-get, from caches 411 | sArr = c.Strings("arr1") 412 | is.Eq("val1", sArr[1]) 413 | 414 | sMap := c.StringMap("map1") 415 | is.Eq("val1", sMap["key1"]) 416 | sMap = c.StringMap("map1") 417 | is.Eq("val1", sMap["key1"]) 418 | 419 | c.ClearAll() 420 | } 421 | 422 | func TestJSONAllowComments(t *testing.T) { 423 | is := assert.New(t) 424 | 425 | m := struct { 426 | N string 427 | }{} 428 | 429 | // disable clear comments 430 | old := JSONAllowComments 431 | JSONAllowComments = false 432 | err := JSONDecoder([]byte(`{ 433 | // comments 434 | "n":"v"} 435 | `), &m) 436 | is.Err(err) 437 | 438 | JSONAllowComments = true 439 | err = JSONDecoder([]byte(`{ 440 | // comments 441 | "n":"v"} 442 | `), &m) 443 | is.NoErr(err) 444 | JSONAllowComments = old 445 | } 446 | 447 | func TestSaveFileOnSet(t *testing.T) { 448 | old := JSONMarshalIndent 449 | JSONMarshalIndent = " " 450 | defer func() { 451 | JSONMarshalIndent = old 452 | }() 453 | 454 | is := assert.New(t) 455 | c := New("test") 456 | c.WithOptions(SaveFileOnSet("testdata/config.bak.json", JSON)) 457 | 458 | err := c.LoadStrings(JSON, jsonStr) 459 | is.Nil(err) 460 | 461 | is.NoErr(c.Set("new-key", "new-value")) 462 | is.Eq("new-value", c.Get("new-key")) 463 | } 464 | 465 | func TestMapStringStringParseEnv(t *testing.T) { 466 | is := assert.New(t) 467 | c := New("test") 468 | c.WithOptions(ParseEnv) 469 | err := c.LoadStrings(JSON, jsonStr) 470 | is.Nil(err) 471 | 472 | shellVal := "/usr/bin/bash" 473 | testutil.MockEnvValue("SHELL", shellVal, func(_ string) { 474 | sMap := c.StringMap("map1") 475 | is.Eq(shellVal, sMap["key3"]) 476 | }) 477 | } 478 | -------------------------------------------------------------------------------- /load.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "io/fs" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "time" 14 | 15 | "dario.cat/mergo" 16 | "github.com/gookit/goutil/errorx" 17 | "github.com/gookit/goutil/fsutil" 18 | ) 19 | 20 | // LoadFiles load one or multi files, will fire OnLoadData event 21 | // 22 | // Usage: 23 | // 24 | // config.LoadFiles(file1, file2, ...) 25 | func LoadFiles(sourceFiles ...string) error { return dc.LoadFiles(sourceFiles...) } 26 | 27 | // LoadFiles load and parse config files, will fire OnLoadData event 28 | func (c *Config) LoadFiles(sourceFiles ...string) (err error) { 29 | for _, file := range sourceFiles { 30 | if err = c.loadFile(file, false, ""); err != nil { 31 | return 32 | } 33 | } 34 | return 35 | } 36 | 37 | // LoadExists load one or multi files, will ignore not exist 38 | // 39 | // Usage: 40 | // 41 | // config.LoadExists(file1, file2, ...) 42 | func LoadExists(sourceFiles ...string) error { return dc.LoadExists(sourceFiles...) } 43 | 44 | // LoadExists load and parse config files, but will ignore not exists file. 45 | func (c *Config) LoadExists(sourceFiles ...string) (err error) { 46 | for _, file := range sourceFiles { 47 | if file == "" { 48 | continue 49 | } 50 | 51 | if err = c.loadFile(file, true, ""); err != nil { 52 | return 53 | } 54 | } 55 | return 56 | } 57 | 58 | // LoadRemote load config data from remote URL. 59 | func LoadRemote(format, url string) error { return dc.LoadRemote(format, url) } 60 | 61 | // LoadRemote load config data from remote URL. 62 | // 63 | // Usage: 64 | // 65 | // c.LoadRemote(config.JSON, "http://abc.com/api-config.json") 66 | func (c *Config) LoadRemote(format, url string) (err error) { 67 | // create http client 68 | client := http.Client{Timeout: 300 * time.Second} 69 | resp, err := client.Get(url) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | //noinspection GoUnhandledErrorResult 75 | defer resp.Body.Close() 76 | if resp.StatusCode != 200 { 77 | return fmt.Errorf("fetch remote config error, reply status code is %d", resp.StatusCode) 78 | } 79 | 80 | // read response content 81 | bts, err := io.ReadAll(resp.Body) 82 | if err == nil { 83 | if err = c.parseSourceCode(format, bts); err != nil { 84 | return 85 | } 86 | c.loadedUrls = append(c.loadedUrls, url) 87 | } 88 | return 89 | } 90 | 91 | // LoadOSEnv load data from OS ENV 92 | // 93 | // Deprecated: please use LoadOSEnvs() 94 | func LoadOSEnv(keys []string, keyToLower bool) { dc.LoadOSEnv(keys, keyToLower) } 95 | 96 | // LoadOSEnv load data from os ENV 97 | // 98 | // Deprecated: please use Config.LoadOSEnvs() 99 | func (c *Config) LoadOSEnv(keys []string, keyToLower bool) { 100 | for _, key := range keys { 101 | // NOTICE: if is Windows os, os.Getenv() Key is not case-sensitive 102 | val := os.Getenv(key) 103 | if keyToLower { 104 | key = strings.ToLower(key) 105 | } 106 | _ = c.Set(key, val) 107 | } 108 | c.fireHook(OnLoadData) 109 | } 110 | 111 | // LoadOSEnvs load data from OS ENVs. see Config.LoadOSEnvs 112 | func LoadOSEnvs(nameToKeyMap map[string]string) { dc.LoadOSEnvs(nameToKeyMap) } 113 | 114 | // LoadOSEnvs load data from os ENVs. format: `{ENV_NAME: config_key}` 115 | // 116 | // - `config_key` allow use key path. eg: `{"DB_USERNAME": "db.username"}` 117 | func (c *Config) LoadOSEnvs(nameToKeyMap map[string]string) { 118 | for name, cfgKey := range nameToKeyMap { 119 | if val := os.Getenv(name); val != "" { 120 | if cfgKey == "" { 121 | cfgKey = strings.ToLower(name) 122 | } 123 | _ = c.Set(cfgKey, val) 124 | } 125 | } 126 | 127 | c.fireHook(OnLoadData) 128 | } 129 | 130 | // support bound types for CLI flags vars 131 | var validTypes = map[string]int{ 132 | "int": 1, 133 | "uint": 1, 134 | "bool": 1, 135 | // string is default 136 | "string": 1, 137 | } 138 | 139 | // LoadFlags load data from cli flags. see Config.LoadFlags 140 | func LoadFlags(defines []string) error { return dc.LoadFlags(defines) } 141 | 142 | // LoadFlags parse command line arguments, based on provide keys. 143 | // 144 | // Usage: 145 | // 146 | // // 'debug' flag is bool type 147 | // c.LoadFlags([]string{"env", "debug:bool"}) 148 | // // can with flag desc message 149 | // c.LoadFlags([]string{"env:set the run env"}) 150 | // c.LoadFlags([]string{"debug:bool:set debug mode"}) 151 | // // can set value to map key. eg: myapp --map1.sub-key=val 152 | // c.LoadFlags([]string{"--map1.sub-key"}) 153 | func (c *Config) LoadFlags(defines []string) (err error) { 154 | hash := map[string]int8{} 155 | 156 | // bind vars 157 | for _, str := range defines { 158 | key, typ, desc := parseVarNameAndType(str) 159 | if desc == "" { 160 | desc = "config flag " + key 161 | } 162 | 163 | switch typ { 164 | case "int": 165 | ptr := new(int) 166 | flag.IntVar(ptr, key, c.Int(key), desc) 167 | hash[key] = 0 168 | case "uint": 169 | ptr := new(uint) 170 | flag.UintVar(ptr, key, c.Uint(key), desc) 171 | hash[key] = 0 172 | case "bool": 173 | ptr := new(bool) 174 | flag.BoolVar(ptr, key, c.Bool(key), desc) 175 | hash[key] = 0 176 | default: // as string 177 | ptr := new(string) 178 | flag.StringVar(ptr, key, c.String(key), desc) 179 | hash[key] = 0 180 | } 181 | } 182 | 183 | // parse and collect 184 | flag.Parse() 185 | flag.Visit(func(f *flag.Flag) { 186 | name := f.Name 187 | // only get name in the keys. 188 | if _, ok := hash[name]; !ok { 189 | return 190 | } 191 | 192 | // if f.Value implement the flag.Getter, read typed value 193 | if gtr, ok := f.Value.(flag.Getter); ok { 194 | _ = c.Set(name, gtr.Get()) 195 | // } else { // TIP: basic type flag always implements Getter interface 196 | // _ = c.Set(name, f.Value.String()) // ignore error 197 | } 198 | }) 199 | 200 | c.fireHook(OnLoadData) 201 | return 202 | } 203 | 204 | // LoadData load one or multi data 205 | func LoadData(dataSource ...any) error { return dc.LoadData(dataSource...) } 206 | 207 | // LoadData load data from map OR struct 208 | // 209 | // The dataSources type allow: 210 | // - map[string]any 211 | // - map[string]string 212 | func (c *Config) LoadData(dataSources ...any) (err error) { 213 | if c.opts.Delimiter == 0 { 214 | c.opts.Delimiter = defaultDelimiter 215 | } 216 | 217 | var loaded bool 218 | for _, ds := range dataSources { 219 | if smp, ok := ds.(map[string]string); ok { 220 | loaded = true 221 | c.LoadSMap(smp) 222 | continue 223 | } 224 | 225 | err = mergo.Merge(&c.data, ds, c.opts.MergeOptions...) 226 | if err != nil { 227 | return errorx.WithStack(err) 228 | } 229 | loaded = true 230 | } 231 | 232 | if loaded { 233 | c.fireHook(OnLoadData) 234 | } 235 | return 236 | } 237 | 238 | // LoadSMap to config 239 | func (c *Config) LoadSMap(smp map[string]string) { 240 | for k, v := range smp { 241 | c.data[k] = v 242 | } 243 | c.fireHook(OnLoadData) 244 | } 245 | 246 | // LoadSources load one or multi byte data 247 | func LoadSources(format string, src []byte, more ...[]byte) error { 248 | return dc.LoadSources(format, src, more...) 249 | } 250 | 251 | // LoadSources load data from byte content. 252 | // 253 | // Usage: 254 | // 255 | // config.LoadSources(config.Yaml, []byte(` 256 | // name: blog 257 | // arr: 258 | // key: val 259 | // 260 | // `)) 261 | func (c *Config) LoadSources(format string, src []byte, more ...[]byte) (err error) { 262 | err = c.parseSourceCode(format, src) 263 | if err != nil { 264 | return 265 | } 266 | 267 | for _, sc := range more { 268 | err = c.parseSourceCode(format, sc) 269 | if err != nil { 270 | return 271 | } 272 | } 273 | return 274 | } 275 | 276 | // LoadStrings load one or multi string 277 | func LoadStrings(format string, str string, more ...string) error { 278 | return dc.LoadStrings(format, str, more...) 279 | } 280 | 281 | // LoadStrings load data from source string content. 282 | func (c *Config) LoadStrings(format string, str string, more ...string) (err error) { 283 | err = c.parseSourceCode(format, []byte(str)) 284 | if err != nil { 285 | return 286 | } 287 | 288 | for _, s := range more { 289 | err = c.parseSourceCode(format, []byte(s)) 290 | if err != nil { 291 | return 292 | } 293 | } 294 | return 295 | } 296 | 297 | // LoadFilesByFormat load one or multi config files by give format, will fire OnLoadData event 298 | func LoadFilesByFormat(format string, configFiles ...string) error { 299 | return dc.LoadFilesByFormat(format, configFiles...) 300 | } 301 | 302 | // LoadFilesByFormat load one or multi files by give format, will fire OnLoadData event 303 | func (c *Config) LoadFilesByFormat(format string, configFiles ...string) (err error) { 304 | for _, file := range configFiles { 305 | if err = c.loadFile(file, false, format); err != nil { 306 | return 307 | } 308 | } 309 | return 310 | } 311 | 312 | // LoadExistsByFormat load one or multi files by give format, will fire OnLoadData event 313 | func LoadExistsByFormat(format string, configFiles ...string) error { 314 | return dc.LoadExistsByFormat(format, configFiles...) 315 | } 316 | 317 | // LoadExistsByFormat load one or multi files by give format, will fire OnLoadData event 318 | func (c *Config) LoadExistsByFormat(format string, configFiles ...string) (err error) { 319 | for _, file := range configFiles { 320 | if err = c.loadFile(file, true, format); err != nil { 321 | return 322 | } 323 | } 324 | return 325 | } 326 | 327 | // LoadOptions for load config from dir. 328 | type LoadOptions struct { 329 | // DataKey use for load config from dir. 330 | // see https://github.com/gookit/config/issues/173 331 | DataKey string 332 | } 333 | 334 | // LoadOptFn type func 335 | type LoadOptFn func(lo *LoadOptions) 336 | 337 | func newLoadOptions(loFns []LoadOptFn) *LoadOptions { 338 | lo := &LoadOptions{} 339 | for _, fn := range loFns { 340 | fn(lo) 341 | } 342 | return lo 343 | } 344 | 345 | // LoadFromDir Load custom format files from the given directory, the file name will be used as the key. 346 | // 347 | // Example: 348 | // 349 | // // file: /somedir/task.json 350 | // LoadFromDir("/somedir", "json") 351 | // 352 | // // after load 353 | // Config.data = map[string]any{"task": file data} 354 | func LoadFromDir(dirPath, format string, loFns ...LoadOptFn) error { 355 | return dc.LoadFromDir(dirPath, format, loFns...) 356 | } 357 | 358 | // LoadFromDir Load custom format files from the given directory, the file name will be used as the key. 359 | // 360 | // NOTE: will not be reloaded on call ReloadFiles(), if data loaded by the method. 361 | // 362 | // Example: 363 | // 364 | // // file: /somedir/task.json , will use filename 'task' as key 365 | // Config.LoadFromDir("/somedir", "json") 366 | // 367 | // // after load, the data will be: 368 | // Config.data = map[string]any{"task": {file data}} 369 | func (c *Config) LoadFromDir(dirPath, format string, loFns ...LoadOptFn) (err error) { 370 | extName := "." + format 371 | extLen := len(extName) 372 | 373 | lo := newLoadOptions(loFns) 374 | dirData := make(map[string]any) 375 | dataList := make([]map[string]any, 0, 8) 376 | 377 | err = fsutil.FindInDir(dirPath, func(fPath string, ent fs.DirEntry) error { 378 | baseName := ent.Name() 379 | if strings.HasSuffix(baseName, extName) { 380 | data, err := c.parseSourceToMap(format, fsutil.MustReadFile(fPath)) 381 | if err != nil { 382 | return err 383 | } 384 | 385 | // filename without ext. 386 | onlyName := baseName[:len(baseName)-extLen] 387 | if lo.DataKey != "" { 388 | dataList = append(dataList, data) 389 | } else { 390 | dirData[onlyName] = data 391 | } 392 | 393 | // TODO use file name as key, it cannot be reloaded. So, cannot append to loadedFiles 394 | // c.loadedFiles = append(c.loadedFiles, fPath) 395 | } 396 | return nil 397 | }) 398 | 399 | if err != nil { 400 | return err 401 | } 402 | if lo.DataKey != "" { 403 | dirData[lo.DataKey] = dataList 404 | } 405 | 406 | if len(dirData) == 0 { 407 | return nil 408 | } 409 | return c.loadDataMap(dirData) 410 | } 411 | 412 | // ReloadFiles reload config data use loaded files 413 | func ReloadFiles() error { return dc.ReloadFiles() } 414 | 415 | // ReloadFiles reload config data use loaded files. use on watching loaded files change 416 | func (c *Config) ReloadFiles() (err error) { 417 | files := c.loadedFiles 418 | if len(files) == 0 { 419 | return 420 | } 421 | 422 | data := c.Data() 423 | c.reloading = true 424 | c.ClearCaches() 425 | 426 | defer func() { 427 | // revert to back up data on error 428 | if err != nil { 429 | c.data = data 430 | } 431 | 432 | c.lock.Unlock() 433 | c.reloading = false 434 | 435 | if err == nil { 436 | c.fireHook(OnReloadData) 437 | } 438 | }() 439 | 440 | // with lock 441 | c.lock.Lock() 442 | 443 | // reload config files 444 | return c.LoadFiles(files...) 445 | } 446 | 447 | // load config file, will fire OnLoadData event 448 | func (c *Config) loadFile(file string, loadExist bool, format string) (err error) { 449 | fd, err := os.Open(file) 450 | if err != nil { 451 | // skip not exist file 452 | if os.IsNotExist(err) && loadExist { 453 | return nil 454 | } 455 | return err 456 | } 457 | //noinspection GoUnhandledErrorResult 458 | defer fd.Close() 459 | 460 | // read file content 461 | bts, err := io.ReadAll(fd) 462 | if err == nil { 463 | // get format for file ext 464 | if format == "" { 465 | format = strings.Trim(filepath.Ext(file), ".") 466 | } 467 | 468 | // parse file content 469 | if err = c.parseSourceCode(format, bts); err != nil { 470 | return 471 | } 472 | 473 | if !c.reloading { 474 | c.loadedFiles = append(c.loadedFiles, file) 475 | } 476 | } 477 | return 478 | } 479 | 480 | // parse config source code to Config. 481 | func (c *Config) parseSourceCode(format string, blob []byte) (err error) { 482 | data, err := c.parseSourceToMap(format, blob) 483 | if err != nil { 484 | return err 485 | } 486 | 487 | return c.loadDataMap(data) 488 | } 489 | 490 | func (c *Config) loadDataMap(data map[string]any) (err error) { 491 | // first: init config data 492 | if len(c.data) == 0 { 493 | c.data = data 494 | } else { 495 | // again ... will merge data 496 | err = mergo.Merge(&c.data, data, c.opts.MergeOptions...) 497 | } 498 | 499 | if !c.reloading && err == nil { 500 | c.fireHook(OnLoadData) 501 | } 502 | return err 503 | } 504 | 505 | // parse config source code to Config. 506 | func (c *Config) parseSourceToMap(format string, blob []byte) (map[string]any, error) { 507 | format = c.resolveFormat(format) 508 | decode := c.decoders[format] 509 | if decode == nil { 510 | return nil, errors.New("not register decoder for the format: " + format) 511 | } 512 | 513 | if c.opts.Delimiter == 0 { 514 | c.opts.Delimiter = defaultDelimiter 515 | } 516 | 517 | // decode content to data 518 | data := make(map[string]any) 519 | 520 | if err := decode(blob, &data); err != nil { 521 | return nil, err 522 | } 523 | return data, nil 524 | } 525 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # Config 2 | 3 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/gookit/config?style=flat-square) 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/1e0f0ca096d94ffdab375234ec4167ee)](https://app.codacy.com/gh/gookit/config?utm_source=github.com&utm_medium=referral&utm_content=gookit/config&utm_campaign=Badge_Grade_Settings) 5 | [![Build Status](https://travis-ci.org/gookit/config.svg?branch=master)](https://travis-ci.org/gookit/config) 6 | [![Actions Status](https://github.com/gookit/config/workflows/Unit-Tests/badge.svg)](https://github.com/gookit/config/actions) 7 | [![Coverage Status](https://coveralls.io/repos/github/gookit/config/badge.svg?branch=master)](https://coveralls.io/github/gookit/config?branch=master) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/gookit/config)](https://goreportcard.com/report/github.com/gookit/config) 9 | [![Go Reference](https://pkg.go.dev/badge/github.com/gookit/config/v2.svg)](https://pkg.go.dev/github.com/gookit/config/v2) 10 | 11 | `config` - 简洁、功能完善的 Go 应用程序配置管理工具库 12 | 13 | > **[EN README](README.md)** 14 | 15 | ## 功能简介 16 | 17 | - 支持多种格式: `JSON`(默认), `JSON5`, `INI`, `Properties`, `YAML`, `TOML`, `HCL`, `ENV`, `Flags` 18 | - `JSON` 内容支持注释,可以设置解析时清除注释 19 | - `HCL` 需要手动引入 `github.com/hashicorp/hcl` 添加自定义驱动 20 | - 其他驱动都是按需使用,不使用的不会加载编译到应用中 21 | - 支持多个文件、多数据加载 22 | - 支持从 OS ENV 变量数据加载配置 23 | - 支持从远程 URL 加载配置数据 24 | - 支持从命令行参数(`flags`)设置配置数据 25 | - 数据自动覆盖合并,加载多份数据时将按`key`自动合并 26 | - 支持丰富的自定义选项设置 27 | - `Readonly` 支持设置配置数据只读 28 | - `EnableCache` 支持设置配置数据缓存 29 | - `ParseEnv` 支持获取时自动解析string值里的ENV变量(`shell: ${SHELL}` -> `shell: /bin/zsh`) 30 | - `ParseDefault` 支持在绑定数据到结构体时解析默认值 (tag: `default:"def_value"`, 配合`ParseEnv`也支持ENV变量) 31 | - `ParseTime` 支持绑定数据到struct时自动转换 `10s`,`2m` 为 `time.Duration` 32 | - 完整选项设置请查看 `config.Options` 33 | - 支持将全部或部分配置数据绑定到结构体 `config.BindStruct("key", &s)` 34 | - 支持通过结构体标签 `default` 解析并设置默认值. eg: `default:"def_value"` 35 | - 支持从 ENV 初始化设置字段值 `default:"${APP_ENV | dev}"` 36 | - 支持通过 `.` 分隔符来按路径获取子级值,也支持自定义分隔符。 e.g `map.key` `arr.2` 37 | - 支持在配置数据更改时触发事件 38 | - 可用事件: `set.value`, `set.data`, `load.data`, `clean.data`, `reload.data` 39 | - 简洁的使用API `Get` `Int` `Uint` `Int64` `String` `Bool` `Ints` `IntMap` `Strings` `StringMap` ... 40 | - 完善的单元测试(code coverage > 95%) 41 | 42 | ## 只使用INI 43 | 44 | 如果你仅仅想用INI来做简单配置管理,推荐使用 [gookit/ini](https://github.com/gookit/ini) 45 | 46 | ### 加载 .env 文件 47 | 48 | `gookit/ini`: 提供一个子包 `dotenv`,支持从文件(eg `.env`)中导入数据到ENV 49 | 50 | ```shell 51 | go get github.com/gookit/ini/v2/dotenv 52 | ``` 53 | 54 | ## GoDoc 55 | 56 | - [godoc for github](https://godoc.org/github.com/gookit/config) 57 | 58 | ## 安装包 59 | 60 | ```bash 61 | go get github.com/gookit/config/v2 62 | ``` 63 | 64 | ## 快速使用 65 | 66 | 这里使用yaml格式内容作为示例(`testdata/yml_other.yml`): 67 | 68 | ```yaml 69 | name: app2 70 | debug: false 71 | baseKey: value2 72 | shell: ${SHELL} 73 | envKey1: ${NotExist|defValue} 74 | 75 | map1: 76 | key: val2 77 | key2: val20 78 | 79 | arr1: 80 | - val1 81 | - val21 82 | ``` 83 | 84 | ### 载入数据 85 | 86 | > 示例代码请看 [_examples/yaml.go](_examples/yaml.go): 87 | 88 | ```go 89 | package main 90 | 91 | import ( 92 | "github.com/gookit/config/v2" 93 | "github.com/gookit/config/v2/yaml" 94 | ) 95 | 96 | // go run ./examples/yaml.go 97 | func main() { 98 | // 设置选项支持ENV变量解析:当获取的值为string类型时,会尝试解析其中的ENV变量 99 | config.WithOptions(config.ParseEnv) 100 | 101 | // 添加驱动程序以支持yaml内容解析(除了JSON是默认支持,其他的则是按需使用) 102 | config.AddDriver(yaml.Driver) 103 | 104 | // 加载配置,可以同时传入多个文件 105 | err := config.LoadFiles("testdata/yml_base.yml") 106 | if err != nil { 107 | panic(err) 108 | } 109 | 110 | // fmt.Printf("config data: \n %#v\n", config.Data()) 111 | 112 | // 加载更多文件 113 | err = config.LoadFiles("testdata/yml_other.yml") 114 | // 也可以一次性加载多个文件 115 | // err := config.LoadFiles("testdata/yml_base.yml", "testdata/yml_other.yml") 116 | if err != nil { 117 | panic(err) 118 | } 119 | } 120 | ``` 121 | 122 | **使用提示**: 123 | 124 | - 可以使用 `WithOptions()` 添加更多额外选项功能. 例如: `ParseEnv`, `ParseDefault` 125 | - 可以使用 `AddDriver()` 添加需要使用的格式驱动器(`json` 是默认加载的的,无需添加) 126 | - 然后就可以使用 `LoadFiles()` `LoadStrings()` 等方法加载配置数据 127 | - 可以传入多个文件,也可以调用多次 128 | - 多次加载的数据会自动按key进行合并处理 129 | 130 | ### 绑定数据到结构体 131 | 132 | > 注意:结构体默认的绑定映射tag是 `mapstructure`,可以通过设置 `Options.TagName` 来更改它 133 | 134 | ```go 135 | type User struct { 136 | Age int `mapstructure:"age"` 137 | Key string `mapstructure:"key"` 138 | UserName string `mapstructure:"user_name"` 139 | Tags []int `mapstructure:"tags"` 140 | } 141 | 142 | user := User{} 143 | err = config.BindStruct("user", &user) 144 | 145 | fmt.Println(user.UserName) // inhere 146 | ``` 147 | 148 | **更改结构标签名称** 149 | 150 | ```go 151 | config.WithOptions(func(opt *Options) { 152 | options.DecoderConfig.TagName = "config" 153 | }) 154 | 155 | // use custom tag name. 156 | type User struct { 157 | Age int `config:"age"` 158 | Key string `config:"key"` 159 | UserName string `config:"user_name"` 160 | Tags []int `config:"tags"` 161 | } 162 | 163 | user := User{} 164 | err = config.Decode(&user) 165 | ``` 166 | 167 | 将所有配置数据绑定到结构: 168 | 169 | ```go 170 | config.Decode(&myConf) 171 | // 也可以 172 | config.BindStruct("", &myConf) 173 | ``` 174 | 175 | > `config.MapOnExists` 与 `BindStruct` 一样,但仅当 key 存在时才进行映射绑定 176 | 177 | ### 快速获取数据 178 | 179 | ```go 180 | // 获取整型 181 | age := config.Int("age") 182 | fmt.Print(age) // 100 183 | 184 | // 获取布尔值 185 | val := config.Bool("debug") 186 | fmt.Print(val) // true 187 | 188 | // 获取字符串 189 | name := config.String("name") 190 | fmt.Print(name) // inhere 191 | 192 | // 获取字符串数组 193 | arr1 := config.Strings("arr1") 194 | fmt.Printf("%#v", arr1) // []string{"val1", "val21"} 195 | 196 | // 获取字符串KV映射 197 | val := config.StringMap("map1") 198 | fmt.Printf("%#v",val) // map[string]string{"key":"val2", "key2":"val20"} 199 | 200 | // 值包含ENV变量 201 | value := config.String("shell") 202 | fmt.Print(value) // /bin/zsh 203 | 204 | // 通过key路径获取值 205 | // from array 206 | value := config.String("arr1.0") 207 | fmt.Print(value) // "val1" 208 | 209 | // from map 210 | value := config.String("map1.key") 211 | fmt.Print(value) // "val2" 212 | ``` 213 | 214 | ### 设置新的值 215 | 216 | ```go 217 | // set value 218 | config.Set("name", "new name") 219 | // get 220 | name = config.String("name") 221 | fmt.Print(name) // new name 222 | ``` 223 | 224 | ## 加载配置文件 225 | 226 | - `LoadExists(sourceFiles ...string) (err error)` 从存在的配置文件里加载数据,会忽略不存在的文件 227 | - `LoadFiles(sourceFiles ...string) (err error)` 从给定的配置文件里加载数据,有文件不存在则会panic 228 | 229 | > **TIP**: 更多加载方式请查看 `config.Load*` 相关方法 230 | 231 | ## 从ENV载入数据 232 | 233 | `LoadOSEnvs` 支持从环境变量中读取数据,并解析为配置数据。格式为 `ENV_NAME: config_key` 234 | 235 | - `config_key` 可以是 key path 格式。 eg: `{"DB_USERNAME": "db.username"}` 值将会映射到 `db` 配置的 `username` 236 | 237 | ```go 238 | // os env: APP_NAME=config APP_DEBUG=true 239 | // load ENV info 240 | config.LoadOSEnvs(map[string]string{"APP_NAME": "app_name", "APP_DEBUG": "app_debug"}) 241 | 242 | // read 243 | config.Bool("app_debug") // true 244 | config.String("app_name") // "config" 245 | ``` 246 | 247 | ## 从命令行参数载入数据 248 | 249 | 支持简单的从命令行 `flag` 参数解析,加载数据。 250 | 251 | - 配置参数格式为 `name:type:desc` OR `name:type` OR `name:desc` (type, desc 是可选的) 252 | - `type` 可以设置 `flag` 的类型,支持 `bool`, `int`, `string`(默认) 253 | - `desc` 可以设置 `flag` 的描述信息 254 | - `name` 可以是 key path 格式。 eg: `db.username`, input: `--db.username=someone` 值将会映射到 `db` 配置的 `username` 255 | 256 | ```go 257 | // 'debug' flag is bool type 258 | config.LoadFlags([]string{"env", "debug:bool"}) 259 | // can with flag desc message 260 | config.LoadFlags([]string{"env:set the run env"}) 261 | config.LoadFlags([]string{"debug:bool:set debug mode"}) 262 | // can set value to map key. eg: myapp --map1.sub-key=val 263 | config.LoadFlags([]string{"map1.sub-key"}) 264 | ``` 265 | 266 | Examples: 267 | 268 | ```go 269 | // flags like: --name inhere --env dev --age 99 --debug --map1.sub-key=val 270 | 271 | // load flag info 272 | keys := []string{ 273 | "name", 274 | "env:set the run env", 275 | "age:int", 276 | "debug:bool:set debug mode", 277 | "map1.sub-key", 278 | } 279 | err := config.LoadFlags(keys) 280 | 281 | // read 282 | config.String("name") // "inhere" 283 | config.String("env") // "dev" 284 | config.Int("age") // 99 285 | config.Bool("debug") // true 286 | config.Get("map1") // map[string]any{"sub-key":"val"} 287 | ``` 288 | 289 | ## 创建自定义实例 290 | 291 | 您可以创建自定义配置实例: 292 | 293 | ```go 294 | // create new instance, will auto register JSON driver 295 | myConf := config.New("my-conf") 296 | 297 | // create empty instance 298 | myConf := config.NewEmpty("my-conf") 299 | 300 | // create and with some options 301 | myConf := config.NewWithOptions("my-conf", config.ParseEnv, config.ReadOnly) 302 | ``` 303 | 304 | ## 监听配置更改 305 | 306 | 现在,您可以添加一个钩子函数来监听配置数据更改。然后,您可以执行一些自定义操作, 例如:将数据写入文件 307 | 308 | **在创建配置时添加钩子函数**: 309 | 310 | ```go 311 | hookFn := func(event string, c *Config) { 312 | fmt.Println("fire the:", event) 313 | } 314 | 315 | c := NewWithOptions("test", WithHookFunc(hookFn)) 316 | // for global config 317 | config.WithOptions(WithHookFunc(hookFn)) 318 | ``` 319 | 320 | **之后**, 当调用 `LoadXXX, Set, SetData, ClearData` 等方法时, 就会输出: 321 | 322 | ```text 323 | fire the: load.data 324 | fire the: set.value 325 | fire the: set.data 326 | fire the: clean.data 327 | ``` 328 | 329 | ### 监听载入的配置文件变动 330 | 331 | 想要监听载入的配置文件变动,并在变动时重新加载配置,你需要使用 https://github.com/fsnotify/fsnotify 库。 332 | 使用方法可以参考示例 [./_example/watch_file.go](_examples/watch_file.go) 333 | 334 | 同时,你需要监听 `reload.data` 事件: 335 | 336 | ```go 337 | config.WithOptions(config.WithHookFunc(func(event string, c *config.Config) { 338 | if event == config.OnReloadData { 339 | fmt.Println("config reloaded, you can do something ....") 340 | } 341 | })) 342 | ``` 343 | 344 | 当配置发生变化并重新加载后,你可以做相关的事情,例如:重新绑定配置到你的结构体。 345 | 346 | ## 导出配置到文件 347 | 348 | > 可以使用 `config.DumpTo(out io.Writer, format string)` 将整个配置数据导出到指定的writer, 比如 buffer,file。 349 | 350 | **示例:导出为JSON文件** 351 | 352 | ```go 353 | buf := new(bytes.Buffer) 354 | 355 | _, err := config.DumpTo(buf, config.JSON) 356 | ioutil.WriteFile("my-config.json", buf.Bytes(), 0755) 357 | ``` 358 | 359 | **示例:导出格式化的JSON** 360 | 361 | 可以设置默认变量 `JSONMarshalIndent` 的值 或 自定义新的 JSON 驱动程序。 362 | 363 | ```go 364 | config.JSONMarshalIndent = " " 365 | ``` 366 | 367 | **示例:导出为YAML文件** 368 | 369 | ```go 370 | _, err := config.DumpTo(buf, config.YAML) 371 | ioutil.WriteFile("my-config.yaml", buf.Bytes(), 0755) 372 | ``` 373 | 374 | ## 可用选项 375 | 376 | ```go 377 | // Options config options 378 | type Options struct { 379 | // parse env in string value. like: "${EnvName}" "${EnvName|default}" 380 | ParseEnv bool 381 | // ParseTime parses a duration string to time.Duration 382 | // eg: 10s, 2m 383 | ParseTime bool 384 | // config is readonly. default is False 385 | Readonly bool 386 | // enable config data cache. default is False 387 | EnableCache bool 388 | // parse key, allow find value by key path. default is True eg: 'key.sub' will find `map[key]sub` 389 | ParseKey bool 390 | // the delimiter char for split key, when `FindByPath=true`. default is '.' 391 | Delimiter byte 392 | // default write format. default is JSON 393 | DumpFormat string 394 | // default input format. default is JSON 395 | ReadFormat string 396 | // DecoderConfig setting for binding data to struct 397 | DecoderConfig *mapstructure.DecoderConfig 398 | // HookFunc on data changed. 399 | HookFunc HookFunc 400 | // ParseDefault tag on binding data to struct. tag: default 401 | ParseDefault bool 402 | } 403 | ``` 404 | 405 | > **提示**: 访问 https://pkg.go.dev/github.com/gookit/config/v2#Options 查看最新的选项信息 406 | 407 | Examples for set options: 408 | 409 | ```go 410 | config.WithOptions(config.WithTagName("mytag")) 411 | config.WithOptions(func(opt *Options) { 412 | opt.SetTagNames("config") 413 | }) 414 | ``` 415 | 416 | ### 选项: 解析默认值 417 | 418 | NEW: 支持通过结构标签 `default` 解析并设置默认值,支持嵌套解析处理。 419 | 420 | > 注意 ⚠️ 如果想要解析子结构体字段,需要对父结构体设置 `default:""` 标记,否则不会解析子结构体的字段。 421 | 422 | ```go 423 | // add option: config.ParseDefault 424 | c := config.New("test").WithOptions(config.ParseDefault) 425 | 426 | // only set name 427 | c.SetData(map[string]any{ 428 | "name": "inhere", 429 | }) 430 | 431 | // age load from default tag 432 | type User struct { 433 | Age int `default:"30"` 434 | Name string 435 | Tags []int 436 | } 437 | 438 | user := &User{} 439 | goutil.MustOk(c.Decode(user)) 440 | dump.Println(user) 441 | ``` 442 | 443 | **Output**: 444 | 445 | ```shell 446 | &config_test.User { 447 | Age: int(30), 448 | Name: string("inhere"), #len=6 449 | Tags: []int [ #len=0 450 | ], 451 | }, 452 | ``` 453 | 454 | ## API方法参考 455 | 456 | ### 载入配置 457 | 458 | - `LoadData(dataSource ...any) (err error)` 从struct或map加载数据 459 | - `LoadFlags(keys []string) (err error)` 从命令行参数载入数据 460 | - `LoadOSEnvs(nameToKeyMap map[string]string)` 从ENV载入配置数据 461 | - `LoadExists(sourceFiles ...string) (err error)` 从存在的配置文件里加载数据,会忽略不存在的文件 462 | - `LoadFiles(sourceFiles ...string) (err error)` 从给定的配置文件里加载数据,有文件不存在则会panic 463 | - `LoadFromDir(dirPath, format string) (err error)` 从给定目录里加载自定格式的文件,文件名会作为 key 464 | - `LoadRemote(format, url string) (err error)` 从远程 URL 加载配置数据 465 | - `LoadSources(format string, src []byte, more ...[]byte) (err error)` 从给定格式的字节数据加载配置 466 | - `LoadStrings(format string, str string, more ...string) (err error)` 从给定格式的字符串配置里加载配置数据 467 | - `LoadFilesByFormat(format string, sourceFiles ...string) (err error)` 从给定格式的文件加载配置 468 | - `LoadExistsByFormat(format string, sourceFiles ...string) error` 从给定格式的文件加载配置,会忽略不存在的文件 469 | 470 | ### 获取值 471 | 472 | - `Bool(key string, defVal ...bool) bool` 473 | - `Int(key string, defVal ...int) int` 474 | - `Uint(key string, defVal ...uint) uint` 475 | - `Int64(key string, defVal ...int64) int64` 476 | - `Ints(key string) (arr []int)` 477 | - `IntMap(key string) (mp map[string]int)` 478 | - `Float(key string, defVal ...float64) float64` 479 | - `String(key string, defVal ...string) string` 480 | - `Strings(key string) (arr []string)` 481 | - `StringMap(key string) (mp map[string]string)` 482 | - `SubDataMap(key string) maputi.Data` 483 | - `Get(key string, findByPath ...bool) (value any)` 484 | 485 | **将数据映射到结构体:** 486 | 487 | - `BindStruct(key string, dst any) error` 488 | - `MapOnExists(key string, dst any) error` 489 | 490 | ### 设置值 491 | 492 | - `Set(key string, val any, setByPath ...bool) (err error)` 493 | 494 | ### 有用的方法 495 | 496 | - `Getenv(name string, defVal ...string) (val string)` 497 | - `AddDriver(driver Driver)` 498 | - `Data() map[string]any` 499 | - `Exists(key string, findByPath ...bool) bool` 500 | - `DumpTo(out io.Writer, format string) (n int64, err error)` 501 | - `SetData(data map[string]any)` 设置数据以覆盖 `Config.Data` 502 | 503 | ## 单元测试 504 | 505 | ```bash 506 | go test -cover 507 | // contains all sub-folder 508 | go test -cover ./... 509 | ``` 510 | 511 | ## 使用Config的项目 512 | 513 | 看看这些使用了 https://github.com/gookit/config 的项目: 514 | 515 | - https://github.com/JanDeDobbeleer/oh-my-posh A prompt theme engine for any shell. 516 | - [+ See More](https://pkg.go.dev/github.com/gookit/config?tab=importedby) 517 | 518 | ## Gookit 工具包 519 | 520 | - [gookit/ini](https://github.com/gookit/ini) INI配置读取管理,支持多文件加载,数据覆盖合并, 解析ENV变量, 解析变量引用 521 | - [gookit/rux](https://github.com/gookit/rux) Simple and fast request router for golang HTTP 522 | - [gookit/gcli](https://github.com/gookit/gcli) Go的命令行应用,工具库,运行CLI命令,支持命令行色彩,用户交互,进度显示,数据格式化显示 523 | - [gookit/event](https://github.com/gookit/event) Go实现的轻量级的事件管理、调度程序库, 支持设置监听器的优先级, 支持对一组事件进行监听 524 | - [gookit/cache](https://github.com/gookit/cache) 通用的缓存使用包装库,通过包装各种常用的驱动,来提供统一的使用API 525 | - [gookit/config](https://github.com/gookit/config) Go应用配置管理,支持多种格式(JSON, YAML, TOML, INI, HCL, ENV, Flags),多文件加载,远程文件加载,数据合并 526 | - [gookit/color](https://github.com/gookit/color) CLI 控制台颜色渲染工具库, 拥有简洁的使用API,支持16色,256色,RGB色彩渲染输出 527 | - [gookit/filter](https://github.com/gookit/filter) 提供对Golang数据的过滤,净化,转换 528 | - [gookit/validate](https://github.com/gookit/validate) Go通用的数据验证与过滤库,使用简单,内置大部分常用验证、过滤器 529 | - [gookit/goutil](https://github.com/gookit/goutil) Go 的一些工具函数,格式化,特殊处理,常用信息获取等 530 | - 更多请查看 https://github.com/gookit 531 | 532 | ## 相关包 533 | 534 | - Ini 解析 [gookit/ini/parser](https://github.com/gookit/ini/tree/master/parser) 535 | - Properties 解析 [gookit/properties](https://github.com/gookit/properties) 536 | - Yaml 解析 [go-yaml](https://github.com/go-yaml/yaml) 537 | - Toml 解析 [go toml](https://github.com/BurntSushi/toml) 538 | - 数据合并 [mergo](https://github.com/imdario/mergo) 539 | - 映射数据到结构体 [mapstructure](https://github.com/mitchellh/mapstructure) 540 | - JSON5 解析 541 | - [yosuke-furukawa/json5](https://github.com/yosuke-furukawa/json5) 542 | - [titanous/json5](https://github.com/titanous/json5) 543 | 544 | ## License 545 | 546 | **MIT** 547 | -------------------------------------------------------------------------------- /read.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | 8 | "github.com/gookit/goutil/envutil" 9 | "github.com/gookit/goutil/maputil" 10 | "github.com/gookit/goutil/mathutil" 11 | "github.com/gookit/goutil/strutil" 12 | ) 13 | 14 | // Exists key exists check 15 | func Exists(key string, findByPath ...bool) bool { return dc.Exists(key, findByPath...) } 16 | 17 | // Exists key exists check 18 | func (c *Config) Exists(key string, findByPath ...bool) (ok bool) { 19 | sep := c.opts.Delimiter 20 | if key = formatKey(key, string(sep)); key == "" { 21 | return 22 | } 23 | 24 | if _, ok = c.data[key]; ok { 25 | return 26 | } 27 | 28 | // disable find by path. 29 | if len(findByPath) > 0 && !findByPath[0] { 30 | return 31 | } 32 | 33 | // has sub key? eg. "lang.dir" 34 | if strings.IndexByte(key, sep) == -1 { 35 | return 36 | } 37 | 38 | keys := strings.Split(key, string(sep)) 39 | topK := keys[0] 40 | 41 | // find top item data based on top key 42 | var item any 43 | if item, ok = c.data[topK]; !ok { 44 | return 45 | } 46 | for _, k := range keys[1:] { 47 | switch typeData := item.(type) { 48 | case map[string]int: // is map(from Set) 49 | if item, ok = typeData[k]; !ok { 50 | return 51 | } 52 | case map[string]string: // is map(from Set) 53 | if item, ok = typeData[k]; !ok { 54 | return 55 | } 56 | case map[string]any: // is map(decode from toml/json/yaml.v3) 57 | if item, ok = typeData[k]; !ok { 58 | return 59 | } 60 | case map[any]any: // is map(decode from yaml.v2) 61 | if item, ok = typeData[k]; !ok { 62 | return 63 | } 64 | case []int: // is array(is from Set) 65 | i, err := strconv.Atoi(k) 66 | 67 | // check slice index 68 | if err != nil || len(typeData) < i { 69 | return false 70 | } 71 | case []string: // is array(is from Set) 72 | i, err := strconv.Atoi(k) 73 | if err != nil || len(typeData) < i { 74 | return false 75 | } 76 | case []any: // is array(load from file) 77 | i, err := strconv.Atoi(k) 78 | if err != nil || len(typeData) < i { 79 | return false 80 | } 81 | default: // error 82 | return false 83 | } 84 | } 85 | return true 86 | } 87 | 88 | /************************************************************* 89 | * read config data 90 | *************************************************************/ 91 | 92 | // Data return all config data 93 | func Data() map[string]any { return dc.Data() } 94 | 95 | // Data get all config data. 96 | // 97 | // Note: will don't apply any options, like ParseEnv 98 | func (c *Config) Data() map[string]any { 99 | return c.data 100 | } 101 | 102 | // Sub return a map config data by key 103 | func Sub(key string) map[string]any { return dc.Sub(key) } 104 | 105 | // Sub get a map config data by key 106 | // 107 | // Note: will don't apply any options, like ParseEnv 108 | func (c *Config) Sub(key string) map[string]any { 109 | if mp, ok := c.GetValue(key); ok { 110 | if mmp, ok := mp.(map[string]any); ok { 111 | return mmp 112 | } 113 | } 114 | return nil 115 | } 116 | 117 | // Keys return all config data 118 | func Keys() []string { return dc.Keys() } 119 | 120 | // Keys get all config data 121 | func (c *Config) Keys() []string { 122 | keys := make([]string, 0, len(c.data)) 123 | for key := range c.data { 124 | keys = append(keys, key) 125 | } 126 | return keys 127 | } 128 | 129 | // Get config value by key string, support get sub-value by key path(eg. 'map.key'), 130 | func Get(key string, findByPath ...bool) any { return dc.Get(key, findByPath...) } 131 | 132 | // Get config value by key, findByPath default is true. 133 | func (c *Config) Get(key string, findByPath ...bool) any { 134 | val, _ := c.GetValue(key, findByPath...) 135 | return val 136 | } 137 | 138 | // GetValue get value by given key string. findByPath default is true. 139 | func GetValue(key string, findByPath ...bool) (any, bool) { 140 | return dc.GetValue(key, findByPath...) 141 | } 142 | 143 | // GetValue get value by given key string. findByPath default is true. 144 | // 145 | // Return: 146 | // - ok is true, find value from config 147 | // - ok is false, not found or error 148 | func (c *Config) GetValue(key string, findByPath ...bool) (value any, ok bool) { 149 | sep := c.opts.Delimiter 150 | if key = formatKey(key, string(sep)); key == "" { 151 | c.addError(ErrKeyIsEmpty) 152 | return 153 | } 154 | 155 | // if not is readonly 156 | if !c.opts.Readonly { 157 | c.lock.RLock() 158 | defer c.lock.RUnlock() 159 | } 160 | 161 | // is top key 162 | if value, ok = c.data[key]; ok { 163 | return 164 | } 165 | 166 | // disable find by path. 167 | if len(findByPath) > 0 && !findByPath[0] { 168 | // c.addError(ErrNotFound) 169 | return 170 | } 171 | 172 | // has sub key? eg. "lang.dir" 173 | if strings.IndexByte(key, sep) == -1 { 174 | // c.addError(ErrNotFound) 175 | return 176 | } 177 | 178 | keys := strings.Split(key, string(sep)) 179 | topK := keys[0] 180 | 181 | // find top item data based on top key 182 | var item any 183 | if item, ok = c.data[topK]; !ok { 184 | // c.addError(ErrNotFound) 185 | return 186 | } 187 | 188 | // find child 189 | // NOTICE: don't merge case, will result in an error. 190 | // e.g. case []int, []string 191 | // OR 192 | // case []int: 193 | // case []string: 194 | for _, k := range keys[1:] { 195 | switch typeData := item.(type) { 196 | case map[string]int: // is map(from Set) 197 | if item, ok = typeData[k]; !ok { 198 | return 199 | } 200 | case map[string]string: // is map(from Set) 201 | if item, ok = typeData[k]; !ok { 202 | return 203 | } 204 | case map[string]any: // is map(decode from toml/json) 205 | if item, ok = typeData[k]; !ok { 206 | return 207 | } 208 | case map[any]any: // is map(decode from yaml) 209 | if item, ok = typeData[k]; !ok { 210 | return 211 | } 212 | case []int: // is array(is from Set) 213 | i, err := strconv.Atoi(k) 214 | 215 | // check slice index 216 | if err != nil || len(typeData) < i { 217 | ok = false 218 | c.addError(err) 219 | return 220 | } 221 | 222 | item = typeData[i] 223 | case []string: // is array(is from Set) 224 | i, err := strconv.Atoi(k) 225 | if err != nil || len(typeData) < i { 226 | ok = false 227 | c.addError(err) 228 | return 229 | } 230 | 231 | item = typeData[i] 232 | case []any: // is array(load from file) 233 | i, err := strconv.Atoi(k) 234 | if err != nil || len(typeData) < i { 235 | ok = false 236 | c.addError(err) 237 | return 238 | } 239 | 240 | item = typeData[i] 241 | default: // error 242 | ok = false 243 | c.addErrorf("cannot get value of the key '%s'", key) 244 | return 245 | } 246 | } 247 | 248 | return item, true 249 | } 250 | 251 | /************************************************************* 252 | * read config (basic data type) 253 | *************************************************************/ 254 | 255 | // String get a string by key 256 | func String(key string, defVal ...string) string { return dc.String(key, defVal...) } 257 | 258 | // String get a string by key, if not found return default value 259 | func (c *Config) String(key string, defVal ...string) string { 260 | value, ok := c.getString(key) 261 | 262 | if !ok && len(defVal) > 0 { // give default value 263 | value = defVal[0] 264 | } 265 | return value 266 | } 267 | 268 | // MustString get a string by key, will panic on empty or not exists 269 | func MustString(key string) string { return dc.MustString(key) } 270 | 271 | // MustString get a string by key, will panic on empty or not exists 272 | func (c *Config) MustString(key string) string { 273 | value, ok := c.getString(key) 274 | if !ok { 275 | panic("config: string value not found, key: " + key) 276 | } 277 | return value 278 | } 279 | 280 | func (c *Config) getString(key string) (value string, ok bool) { 281 | // find from cache 282 | if c.opts.EnableCache && len(c.strCache) > 0 { 283 | value, ok = c.strCache[key] 284 | if ok { 285 | return 286 | } 287 | } 288 | 289 | val, ok := c.GetValue(key) 290 | if !ok { 291 | return 292 | } 293 | 294 | switch typVal := val.(type) { 295 | // from json `int` always is float64 296 | case string: 297 | value = typVal 298 | if c.opts.ParseEnv { 299 | value = envutil.ParseEnvValue(value) 300 | } 301 | default: 302 | var err error 303 | value, err = strutil.AnyToString(val, false) 304 | if err != nil { 305 | return "", false 306 | } 307 | } 308 | 309 | // add cache 310 | if ok && c.opts.EnableCache { 311 | if c.strCache == nil { 312 | c.strCache = make(map[string]string) 313 | } 314 | c.strCache[key] = value 315 | } 316 | return 317 | } 318 | 319 | // Int get an int by key 320 | func Int(key string, defVal ...int) int { return dc.Int(key, defVal...) } 321 | 322 | // Int get a int value, if not found return default value 323 | func (c *Config) Int(key string, defVal ...int) (value int) { 324 | i64, exist := c.tryInt64(key) 325 | 326 | if exist { 327 | value = int(i64) 328 | } else if len(defVal) > 0 { 329 | value = defVal[0] 330 | } 331 | return 332 | } 333 | 334 | // Uint get a uint value, if not found return default value 335 | func Uint(key string, defVal ...uint) uint { return dc.Uint(key, defVal...) } 336 | 337 | // Uint get a int value, if not found return default value 338 | func (c *Config) Uint(key string, defVal ...uint) (value uint) { 339 | i64, exist := c.tryInt64(key) 340 | 341 | if exist { 342 | value = uint(i64) 343 | } else if len(defVal) > 0 { 344 | value = defVal[0] 345 | } 346 | return 347 | } 348 | 349 | // Int64 get a int value, if not found return default value 350 | func Int64(key string, defVal ...int64) int64 { return dc.Int64(key, defVal...) } 351 | 352 | // Int64 get a int value, if not found return default value 353 | func (c *Config) Int64(key string, defVal ...int64) (value int64) { 354 | value, exist := c.tryInt64(key) 355 | 356 | if !exist && len(defVal) > 0 { 357 | value = defVal[0] 358 | } 359 | return 360 | } 361 | 362 | // try to get an int64 value by given key 363 | func (c *Config) tryInt64(key string) (value int64, ok bool) { 364 | strVal, ok := c.getString(key) 365 | if !ok { 366 | return 367 | } 368 | 369 | value, err := strconv.ParseInt(strVal, 10, 0) 370 | if err != nil { 371 | c.addError(err) 372 | } 373 | return 374 | } 375 | 376 | // Duration get a time.Duration type value. if not found return default value 377 | func Duration(key string, defVal ...time.Duration) time.Duration { return dc.Duration(key, defVal...) } 378 | 379 | // Duration get a time.Duration type value. if not found return default value 380 | func (c *Config) Duration(key string, defVal ...time.Duration) time.Duration { 381 | value, exist := c.tryInt64(key) 382 | 383 | if !exist && len(defVal) > 0 { 384 | return defVal[0] 385 | } 386 | return time.Duration(value) 387 | } 388 | 389 | // Float get a float64 value, if not found return default value 390 | func Float(key string, defVal ...float64) float64 { return dc.Float(key, defVal...) } 391 | 392 | // Float get a float64 by key 393 | func (c *Config) Float(key string, defVal ...float64) (value float64) { 394 | str, ok := c.getString(key) 395 | if !ok { 396 | if len(defVal) > 0 { 397 | value = defVal[0] 398 | } 399 | return 400 | } 401 | 402 | value, err := strconv.ParseFloat(str, 64) 403 | if err != nil { 404 | c.addError(err) 405 | } 406 | return 407 | } 408 | 409 | // Bool get a bool value, if not found return default value 410 | func Bool(key string, defVal ...bool) bool { return dc.Bool(key, defVal...) } 411 | 412 | // Bool looks up a value for a key in this section and attempts to parse that value as a boolean, 413 | // along with a boolean result similar to a map lookup. 414 | // 415 | // of following(case insensitive): 416 | // - true 417 | // - yes 418 | // - false 419 | // - no 420 | // - 1 421 | // - 0 422 | // 423 | // The `ok` boolean will be false in the event that the value could not be parsed as a bool 424 | func (c *Config) Bool(key string, defVal ...bool) (value bool) { 425 | rawVal, ok := c.getString(key) 426 | if !ok { 427 | if len(defVal) > 0 { 428 | return defVal[0] 429 | } 430 | return 431 | } 432 | 433 | lowerCase := strings.ToLower(rawVal) 434 | switch lowerCase { 435 | case "", "0", "false", "no": 436 | value = false 437 | case "1", "true", "yes": 438 | value = true 439 | default: 440 | c.addErrorf("the value '%s' cannot be convert to bool", lowerCase) 441 | } 442 | return 443 | } 444 | 445 | /************************************************************* 446 | * read config (complex data type) 447 | *************************************************************/ 448 | 449 | // Ints get config data as an int slice/array 450 | func Ints(key string) []int { return dc.Ints(key) } 451 | 452 | // Ints get config data as an int slice/array 453 | func (c *Config) Ints(key string) (arr []int) { 454 | rawVal, ok := c.GetValue(key) 455 | if !ok { 456 | return 457 | } 458 | 459 | switch typeData := rawVal.(type) { 460 | case []int: 461 | arr = typeData 462 | case []any: 463 | for _, v := range typeData { 464 | iv, err := mathutil.ToInt(v) 465 | // iv, err := strconv.Atoi(fmt.Sprintf("%v", v)) 466 | if err != nil { 467 | c.addError(err) 468 | arr = arr[0:0] // reset 469 | return 470 | } 471 | 472 | arr = append(arr, iv) 473 | } 474 | default: 475 | c.addErrorf("value cannot be convert to []int, key is '%s'", key) 476 | } 477 | return 478 | } 479 | 480 | // IntMap get config data as a map[string]int 481 | func IntMap(key string) map[string]int { return dc.IntMap(key) } 482 | 483 | // IntMap get config data as a map[string]int 484 | func (c *Config) IntMap(key string) (mp map[string]int) { 485 | rawVal, ok := c.GetValue(key) 486 | if !ok { 487 | return 488 | } 489 | 490 | switch typeData := rawVal.(type) { 491 | case map[string]int: // from Set 492 | mp = typeData 493 | case map[string]any: // decode from json,toml 494 | mp = make(map[string]int) 495 | for k, v := range typeData { 496 | // iv, err := strconv.Atoi(fmt.Sprintf("%v", v)) 497 | iv, err := mathutil.ToInt(v) 498 | if err != nil { 499 | c.addError(err) 500 | mp = map[string]int{} // reset 501 | return 502 | } 503 | mp[k] = iv 504 | } 505 | case map[any]any: // if decode from yaml 506 | mp = make(map[string]int) 507 | for k, v := range typeData { 508 | // iv, err := strconv.Atoi(fmt.Sprintf( "%v", v)) 509 | iv, err := mathutil.ToInt(v) 510 | if err != nil { 511 | c.addError(err) 512 | mp = map[string]int{} // reset 513 | return 514 | } 515 | 516 | // sk := fmt.Sprintf("%v", k) 517 | sk, _ := strutil.AnyToString(k, false) 518 | mp[sk] = iv 519 | } 520 | default: 521 | c.addErrorf("value cannot be convert to map[string]int, key is '%s'", key) 522 | } 523 | return 524 | } 525 | 526 | // Strings get strings by key 527 | func Strings(key string) []string { return dc.Strings(key) } 528 | 529 | // Strings get config data as a string slice/array 530 | func (c *Config) Strings(key string) (arr []string) { 531 | var ok bool 532 | // find from cache 533 | if c.opts.EnableCache && len(c.sArrCache) > 0 { 534 | arr, ok = c.sArrCache[key] 535 | if ok { 536 | return 537 | } 538 | } 539 | 540 | rawVal, ok := c.GetValue(key) 541 | if !ok { 542 | return 543 | } 544 | 545 | switch typeData := rawVal.(type) { 546 | case []string: 547 | arr = typeData 548 | case []any: 549 | for _, v := range typeData { 550 | // arr = append(arr, fmt.Sprintf("%v", v)) 551 | arr = append(arr, strutil.MustString(v)) 552 | } 553 | default: 554 | c.addErrorf("value cannot be convert to []string, key is '%s'", key) 555 | return 556 | } 557 | 558 | // add cache 559 | if c.opts.EnableCache { 560 | if c.sArrCache == nil { 561 | c.sArrCache = make(map[string]strArr) 562 | } 563 | c.sArrCache[key] = arr 564 | } 565 | return 566 | } 567 | 568 | // StringsBySplit get []string by split a string value. 569 | func StringsBySplit(key, sep string) []string { return dc.StringsBySplit(key, sep) } 570 | 571 | // StringsBySplit get []string by split a string value. 572 | func (c *Config) StringsBySplit(key, sep string) (ss []string) { 573 | if str, ok := c.getString(key); ok { 574 | ss = strutil.Split(str, sep) 575 | } 576 | return 577 | } 578 | 579 | // StringMap get config data as a map[string]string 580 | func StringMap(key string) map[string]string { return dc.StringMap(key) } 581 | 582 | // StringMap get config data as a map[string]string 583 | func (c *Config) StringMap(key string) (mp map[string]string) { 584 | var ok bool 585 | 586 | // find from cache 587 | if c.opts.EnableCache && len(c.sMapCache) > 0 { 588 | mp, ok = c.sMapCache[key] 589 | if ok { 590 | return 591 | } 592 | } 593 | 594 | rawVal, ok := c.GetValue(key) 595 | if !ok { 596 | return 597 | } 598 | 599 | switch typeData := rawVal.(type) { 600 | case map[string]string: // from Set 601 | mp = typeData 602 | case map[string]any: // decode from json,toml,yaml.v3 603 | mp = make(map[string]string, len(typeData)) 604 | 605 | for k, v := range typeData { 606 | switch tv := v.(type) { 607 | case string: 608 | if c.opts.ParseEnv { 609 | mp[k] = envutil.ParseEnvValue(tv) 610 | } else { 611 | mp[k] = tv 612 | } 613 | default: 614 | mp[k] = strutil.QuietString(v) 615 | } 616 | } 617 | case map[any]any: // decode from yaml v2 618 | mp = make(map[string]string, len(typeData)) 619 | 620 | for k, v := range typeData { 621 | sk := strutil.QuietString(k) 622 | 623 | switch typVal := v.(type) { 624 | case string: 625 | if c.opts.ParseEnv { 626 | mp[sk] = envutil.ParseEnvValue(typVal) 627 | } else { 628 | mp[sk] = typVal 629 | } 630 | default: 631 | mp[sk] = strutil.QuietString(v) 632 | } 633 | } 634 | default: 635 | c.addErrorf("value cannot be convert to map[string]string, key is %q", key) 636 | return 637 | } 638 | 639 | // add cache 640 | if c.opts.EnableCache { 641 | if c.sMapCache == nil { 642 | c.sMapCache = make(map[string]strMap) 643 | } 644 | c.sMapCache[key] = mp 645 | } 646 | return 647 | } 648 | 649 | // SubDataMap get sub config data as maputil.Map 650 | func SubDataMap(key string) maputil.Map { return dc.SubDataMap(key) } 651 | 652 | // SubDataMap get sub config data as maputil.Map 653 | // 654 | // TIP: will not enable parse Env and more 655 | func (c *Config) SubDataMap(key string) maputil.Map { 656 | if mp, ok := c.GetValue(key); ok { 657 | if mmp, ok := mp.(map[string]any); ok { 658 | return mmp 659 | } 660 | } 661 | 662 | // keep is not nil 663 | return maputil.Map{} 664 | } 665 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Config 2 | 3 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/gookit/config?style=flat-square) 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/1e0f0ca096d94ffdab375234ec4167ee)](https://app.codacy.com/gh/gookit/config?utm_source=github.com&utm_medium=referral&utm_content=gookit/config&utm_campaign=Badge_Grade_Settings) 5 | [![Build Status](https://travis-ci.org/gookit/config.svg?branch=master)](https://travis-ci.org/gookit/config) 6 | [![Actions Status](https://github.com/gookit/config/workflows/Unit-Tests/badge.svg)](https://github.com/gookit/config/actions) 7 | [![Coverage Status](https://coveralls.io/repos/github/gookit/config/badge.svg?branch=master)](https://coveralls.io/github/gookit/config?branch=master) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/gookit/config)](https://goreportcard.com/report/github.com/gookit/config) 9 | [![Go Reference](https://pkg.go.dev/badge/github.com/gookit/config/v2.svg)](https://pkg.go.dev/github.com/gookit/config/v2) 10 | 11 | `config` - Simple, full-featured Go application configuration management tool library. 12 | 13 | > **[中文说明](README.zh-CN.md)** 14 | 15 | ## Features 16 | 17 | - Support multi format: `JSON`(default), `JSON5`, `INI`, `Properties`, `YAML`, `TOML`, `ENV`, `Flags` 18 | - `JSON` content support comments. will auto clear comments 19 | - `HCL` need to import `github.com/hashicorp/hcl` for custom driver 20 | - Other drivers are used on demand, not used will not be loaded into the application. 21 | - Possibility to add custom driver for your specific format 22 | - Support multi-file and multi-data loading 23 | - Support for loading configuration from system ENV 24 | - Support for loading configuration data from remote URLs 25 | - Support for setting configuration data from command line(`flags`) 26 | - Support listen and fire events on config data changed. 27 | - allow events: `set.value`, `set.data`, `load.data`, `clean.data`, `reload.data` 28 | - Support data overlay and merge, automatically load by key when loading multiple copies of data 29 | - Support for binding all or part of the configuration data to the structure 30 | - Support init default value by struct tag `default:"def_value"` 31 | - Support init default value from ENV `default:"${APP_ENV | dev}"` 32 | - Support get sub value by key-path, like `map.key` `arr.2` 33 | - Support parse ENV name and allow with default value. like `envKey: ${SHELL|/bin/bash}` -> `envKey: /bin/zsh` 34 | - Generic API: `Get` `Int` `Uint` `Int64` `Float` `String` `Bool` `Ints` `IntMap` `Strings` `StringMap` ... 35 | - Complete unit test(code coverage > 95%) 36 | 37 | ## Only use INI 38 | 39 | If you just want to use INI for simple config management, recommended use [gookit/ini](https://github.com/gookit/ini) 40 | 41 | ### Load dotenv file 42 | 43 | On `gookit/ini`: Provide a sub-package `dotenv` that supports importing data from files (eg `.env`) to ENV 44 | 45 | ```shell 46 | go get github.com/gookit/ini/v2/dotenv 47 | ``` 48 | 49 | ## GoDoc 50 | 51 | - [godoc for github](https://pkg.go.dev/github.com/gookit/config) 52 | 53 | ## Install 54 | 55 | ```bash 56 | go get github.com/gookit/config/v2 57 | ``` 58 | 59 | ## Usage 60 | 61 | Here using the yaml format as an example(`testdata/yml_other.yml`): 62 | 63 | ```yaml 64 | name: app2 65 | debug: false 66 | baseKey: value2 67 | shell: ${SHELL} 68 | envKey1: ${NotExist|defValue} 69 | 70 | map1: 71 | key: val2 72 | key2: val20 73 | 74 | arr1: 75 | - val1 76 | - val21 77 | ``` 78 | 79 | ### Load data 80 | 81 | > examples code please see [_examples/yaml.go](_examples/yaml.go): 82 | 83 | ```go 84 | package main 85 | 86 | import ( 87 | "github.com/gookit/config/v2" 88 | "github.com/gookit/config/v2/yaml" 89 | ) 90 | 91 | // go run ./examples/yaml.go 92 | func main() { 93 | // config.ParseEnv: will parse env var in string value. eg: shell: ${SHELL} 94 | config.WithOptions(config.ParseEnv) 95 | 96 | // add driver for support yaml content 97 | config.AddDriver(yaml.Driver) 98 | 99 | err := config.LoadFiles("testdata/yml_base.yml") 100 | if err != nil { 101 | panic(err) 102 | } 103 | 104 | // load more files 105 | err = config.LoadFiles("testdata/yml_other.yml") 106 | // can also load multi at once 107 | // err := config.LoadFiles("testdata/yml_base.yml", "testdata/yml_other.yml") 108 | if err != nil { 109 | panic(err) 110 | } 111 | 112 | // fmt.Printf("config data: \n %#v\n", config.Data()) 113 | } 114 | ``` 115 | **Usage tips**: 116 | 117 | - More extra options can be added using `WithOptions()`. For example: `ParseEnv`, `ParseDefault` 118 | - You can use `AddDriver()` to add the required format driver (`json` is loaded by default, no need to add) 119 | - The configuration data can then be loaded using `LoadFiles()` `LoadStrings()` etc. 120 | - You can pass in multiple files or call multiple times 121 | - Data loaded multiple times will be automatically merged by key 122 | 123 | ## Bind Structure 124 | 125 | > Note: The default binding mapping tag of a structure is `mapstructure`, which can be changed by setting the decoder's option `options.DecoderConfig.TagName` 126 | 127 | ```go 128 | type User struct { 129 | Age int `mapstructure:"age"` 130 | Key string `mapstructure:"key"` 131 | UserName string `mapstructure:"user_name"` 132 | Tags []int `mapstructure:"tags"` 133 | } 134 | 135 | user := User{} 136 | err = config.BindStruct("user", &user) 137 | 138 | fmt.Println(user.UserName) // inhere 139 | ``` 140 | 141 | **Change struct tag name** 142 | 143 | ```go 144 | config.WithOptions(func(opt *Options) { 145 | options.DecoderConfig.TagName = "config" 146 | }) 147 | 148 | // use custom tag name. 149 | type User struct { 150 | Age int `config:"age"` 151 | Key string `config:"key"` 152 | UserName string `config:"user_name"` 153 | Tags []int `config:"tags"` 154 | } 155 | 156 | user := User{} 157 | err = config.Decode(&user) 158 | ``` 159 | 160 | **Can bind all config data to a struct**: 161 | 162 | ```go 163 | config.Decode(&myConf) 164 | // can also 165 | config.BindStruct("", &myConf) 166 | ``` 167 | 168 | > `config.MapOnExists` like `BindStruct`,but map binding only if key exists 169 | 170 | ### Direct read data 171 | 172 | - Get integer 173 | 174 | ```go 175 | age := config.Int("age") 176 | fmt.Print(age) // 100 177 | ``` 178 | 179 | - Get bool 180 | 181 | ```go 182 | val := config.Bool("debug") 183 | fmt.Print(val) // true 184 | ``` 185 | 186 | - Get string 187 | 188 | ```go 189 | name := config.String("name") 190 | fmt.Print(name) // inhere 191 | ``` 192 | 193 | - Get strings(slice) 194 | 195 | ```go 196 | arr1 := config.Strings("arr1") 197 | fmt.Printf("%#v", arr1) // []string{"val1", "val21"} 198 | ``` 199 | 200 | - Get string map 201 | 202 | ```go 203 | val := config.StringMap("map1") 204 | fmt.Printf("%#v",val) // map[string]string{"key":"val2", "key2":"val20"} 205 | ``` 206 | 207 | - Value contains ENV var 208 | 209 | ```go 210 | value := config.String("shell") 211 | fmt.Print(value) // "/bin/zsh" 212 | ``` 213 | 214 | - Get value by key path 215 | 216 | ```go 217 | // from array 218 | value := config.String("arr1.0") 219 | fmt.Print(value) // "val1" 220 | 221 | // from map 222 | value := config.String("map1.key") 223 | fmt.Print(value) // "val2" 224 | ``` 225 | 226 | - Setting new value 227 | 228 | ```go 229 | // set value 230 | config.Set("name", "new name") 231 | name = config.String("name") 232 | fmt.Print(name) // "new name" 233 | ``` 234 | 235 | ## Load from ENV 236 | 237 | Support load ENV vars to config data. 238 | 239 | - Support set value to sub key in map. 240 | - eg: `{"DB_USERNAME": "db.username"}` value will set to `username` in `db` 241 | 242 | ```go 243 | // os env: APP_NAME=config APP_DEBUG=true DB_USERNAME=someone 244 | 245 | // load ENV info 246 | config.LoadOSEnvs(map[string]string{"APP_NAME": "app_name", "APP_DEBUG": "app_debug", "DB_USERNAME": "db.username"}) 247 | 248 | // read 249 | config.Bool("app_debug") // true 250 | config.String("app_name") // "config" 251 | ``` 252 | 253 | ## Load from flags 254 | 255 | Support simple CLI flags parameter parsing, load to config data. 256 | 257 | - define format: `name:type:desc` OR `name:type` OR `name:desc` (type, desc is optional) 258 | - `type` can set `flag` type. allow: `bool`, `int`, `string`(default) 259 | - `desc` can set `flag` description 260 | - `name` can be in key path format. 261 | - eg: `db.username`, input: `--db.username=someone` values will be mapped to `username` of the `db` configuration 262 | 263 | ```go 264 | // 'debug' flag is bool type 265 | config.LoadFlags([]string{"env", "debug:bool"}) 266 | // can with flag desc message 267 | config.LoadFlags([]string{"env:set the run env"}) 268 | config.LoadFlags([]string{"debug:bool:set debug mode"}) 269 | // can set value to map key. eg: myapp --map1.sub-key=val 270 | config.LoadFlags([]string{"map1.sub-key"}) 271 | ``` 272 | 273 | Examples: 274 | 275 | ```go 276 | // flags like: --name inhere --env dev --age 99 --debug --map1.sub-key=val 277 | 278 | // load flag info 279 | keys := []string{ 280 | "name", 281 | "env:set the run env", 282 | "age:int", 283 | "debug:bool:set debug mode", 284 | "map1.sub-key", 285 | } 286 | err := config.LoadFlags(keys) 287 | 288 | // read 289 | config.String("name") // "inhere" 290 | config.String("env") // "dev" 291 | config.Int("age") // 99 292 | config.Bool("debug") // true 293 | config.Get("map1") // map[string]any{"sub-key":"val"} 294 | ``` 295 | 296 | ## New config instance 297 | 298 | You can create custom config instance 299 | 300 | ```go 301 | // create new instance, will auto register JSON driver 302 | myConf := config.New("my-conf") 303 | 304 | // create empty instance 305 | myConf := config.NewEmpty("my-conf") 306 | 307 | // create and with some options 308 | myConf := config.NewWithOptions("my-conf", config.ParseEnv, config.ReadOnly) 309 | ``` 310 | 311 | ## Listen config change 312 | 313 | Now, you can add a hook func for listen config data change. then, you can do something like: write data to file 314 | 315 | **Add hook func on create config**: 316 | 317 | ```go 318 | hookFn := func(event string, c *Config) { 319 | fmt.Println("fire the:", event) 320 | } 321 | 322 | c := NewWithOptions("test", config.WithHookFunc(hookFn)) 323 | // for global config 324 | config.WithOptions(config.WithHookFunc(hookFn)) 325 | ``` 326 | 327 | After that, when calling `LoadXXX, Set, SetData, ClearData` methods, it will output: 328 | 329 | ```text 330 | fire the: load.data 331 | fire the: set.value 332 | fire the: set.data 333 | fire the: clean.data 334 | ``` 335 | 336 | ### Watch loaded config files 337 | 338 | To listen for changes to loaded config files, and reload the config when it changes, you need to use the https://github.com/fsnotify/fsnotify library. 339 | For usage, please refer to the example [./_example/watch_file.go](_examples/watch_file.go) 340 | 341 | Also, you need to listen to the `reload.data` event: 342 | 343 | ```go 344 | config.WithOptions(config.WithHookFunc(func(event string, c *config.Config) { 345 | if event == config.OnReloadData { 346 | fmt.Println("config reloaded, you can do something ....") 347 | } 348 | })) 349 | ``` 350 | 351 | When the configuration changes, you can do related things, for example: rebind the configuration to your struct. 352 | 353 | ## Dump config data 354 | 355 | > Can use `config.DumpTo()` export the configuration data to the specified `writer`, such as: buffer,file 356 | 357 | **Dump to JSON file** 358 | 359 | ```go 360 | buf := new(bytes.Buffer) 361 | 362 | _, err := config.DumpTo(buf, config.JSON) 363 | ioutil.WriteFile("my-config.json", buf.Bytes(), 0755) 364 | ``` 365 | 366 | **Dump pretty JSON** 367 | 368 | You can set the default var `JSONMarshalIndent` or custom a new JSON driver. 369 | 370 | ```go 371 | config.JSONMarshalIndent = " " 372 | ``` 373 | 374 | **Dump to YAML file** 375 | 376 | ```go 377 | _, err := config.DumpTo(buf, config.YAML) 378 | ioutil.WriteFile("my-config.yaml", buf.Bytes(), 0755) 379 | ``` 380 | 381 | ## Available options 382 | 383 | ```go 384 | // Options config options 385 | type Options struct { 386 | // parse env in string value. like: "${EnvName}" "${EnvName|default}" 387 | ParseEnv bool 388 | // ParseTime parses a duration string to time.Duration 389 | // eg: 10s, 2m 390 | ParseTime bool 391 | // config is readonly. default is False 392 | Readonly bool 393 | // enable config data cache. default is False 394 | EnableCache bool 395 | // parse key, allow find value by key path. default is True eg: 'key.sub' will find `map[key]sub` 396 | ParseKey bool 397 | // the delimiter char for split key path, if `FindByPath=true`. default is '.' 398 | Delimiter byte 399 | // default write format 400 | DumpFormat string 401 | // default input format 402 | ReadFormat string 403 | // DecoderConfig setting for binding data to struct 404 | DecoderConfig *mapstructure.DecoderConfig 405 | // HookFunc on data changed. 406 | HookFunc HookFunc 407 | // ParseDefault tag on binding data to struct. tag: default 408 | ParseDefault bool 409 | } 410 | ``` 411 | 412 | > **TIP**: please visit https://pkg.go.dev/github.com/gookit/config/v2#Options to see the latest options information 413 | 414 | Examples for set options: 415 | 416 | ```go 417 | config.WithOptions(config.WithTagName("mytag")) 418 | config.WithOptions(func(opt *Options) { 419 | opt.SetTagNames("config") 420 | }) 421 | ``` 422 | 423 | ### Options: Parse default 424 | 425 | Support parse default value by struct tag `default`, and support parse fields in sub struct. 426 | 427 | > **NOTE**⚠️ If you want to parse a sub-struct, you need to set the `default:""` flag on the parent struct, 428 | > otherwise the fields of the sub-struct will not be resolved. 429 | 430 | ```go 431 | // add option: config.ParseDefault 432 | c := config.New("test").WithOptions(config.ParseDefault) 433 | 434 | // only set name 435 | c.SetData(map[string]any{ 436 | "name": "inhere", 437 | }) 438 | 439 | // age load from default tag 440 | type User struct { 441 | Age int `default:"30"` 442 | Name string 443 | Tags []int 444 | } 445 | 446 | user := &User{} 447 | goutil.MustOk(c.Decode(user)) 448 | dump.Println(user) 449 | ``` 450 | 451 | **Output**: 452 | 453 | ```shell 454 | &config_test.User { 455 | Age: int(30), 456 | Name: string("inhere"), #len=6 457 | Tags: []int [ #len=0 458 | ], 459 | }, 460 | ``` 461 | 462 | ## API Methods Refer 463 | 464 | ### Load Config 465 | 466 | - `LoadOSEnvs(nameToKeyMap map[string]string)` Load data from os ENV 467 | - `LoadData(dataSource ...any) (err error)` Load from struts or maps 468 | - `LoadFlags(keys []string) (err error)` Load from CLI flags 469 | - `LoadExists(sourceFiles ...string) (err error)` 470 | - `LoadFiles(sourceFiles ...string) (err error)` 471 | - `LoadFromDir(dirPath, format string) (err error)` Load custom format files from the given directory, the file name will be used as the key 472 | - `LoadRemote(format, url string) (err error)` 473 | - `LoadSources(format string, src []byte, more ...[]byte) (err error)` 474 | - `LoadStrings(format string, str string, more ...string) (err error)` 475 | - `LoadFilesByFormat(format string, sourceFiles ...string) (err error)` 476 | - `LoadExistsByFormat(format string, sourceFiles ...string) error` 477 | 478 | ### Getting Values 479 | 480 | - `Bool(key string, defVal ...bool) bool` 481 | - `Int(key string, defVal ...int) int` 482 | - `Uint(key string, defVal ...uint) uint` 483 | - `Int64(key string, defVal ...int64) int64` 484 | - `Ints(key string) (arr []int)` 485 | - `IntMap(key string) (mp map[string]int)` 486 | - `Float(key string, defVal ...float64) float64` 487 | - `String(key string, defVal ...string) string` 488 | - `Strings(key string) (arr []string)` 489 | - `SubDataMap(key string) maputi.Data` 490 | - `StringMap(key string) (mp map[string]string)` 491 | - `Get(key string, findByPath ...bool) (value any)` 492 | 493 | **Mapping data to struct:** 494 | 495 | - `Decode(dst any) error` 496 | - `BindStruct(key string, dst any) error` 497 | - `MapOnExists(key string, dst any) error` 498 | 499 | ### Setting Values 500 | 501 | - `Set(key string, val any, setByPath ...bool) (err error)` 502 | 503 | ### Useful Methods 504 | 505 | - `Getenv(name string, defVal ...string) (val string)` 506 | - `AddDriver(driver Driver)` 507 | - `Data() map[string]any` 508 | - `SetData(data map[string]any)` set data to override the Config.Data 509 | - `Exists(key string, findByPath ...bool) bool` 510 | - `DumpTo(out io.Writer, format string) (n int64, err error)` 511 | 512 | ## Run Tests 513 | 514 | ```bash 515 | go test -cover 516 | // contains all sub-folder 517 | go test -cover ./... 518 | ``` 519 | 520 | ## Projects using config 521 | 522 | Check out these projects, which use https://github.com/gookit/config : 523 | 524 | - https://github.com/JanDeDobbeleer/oh-my-posh A prompt theme engine for any shell. 525 | - [+ See More](https://pkg.go.dev/github.com/gookit/config?tab=importedby) 526 | 527 | ## Gookit packages 528 | 529 | - [gookit/ini](https://github.com/gookit/ini) Go config management, use INI files 530 | - [gookit/rux](https://github.com/gookit/rux) Simple and fast request router for golang HTTP 531 | - [gookit/gcli](https://github.com/gookit/gcli) build CLI application, tool library, running CLI commands 532 | - [gookit/event](https://github.com/gookit/event) Lightweight event manager and dispatcher implements by Go 533 | - [gookit/cache](https://github.com/gookit/cache) Generic cache use and cache manager for golang. support File, Memory, Redis, Memcached. 534 | - [gookit/config](https://github.com/gookit/config) Go config management. support JSON, YAML, TOML, INI, HCL, ENV and Flags 535 | - [gookit/color](https://github.com/gookit/color) A command-line color library with true color support, universal API methods and Windows support 536 | - [gookit/filter](https://github.com/gookit/filter) Provide filtering, sanitizing, and conversion of golang data 537 | - [gookit/validate](https://github.com/gookit/validate) Use for data validation and filtering. support Map, Struct, Form data 538 | - [gookit/goutil](https://github.com/gookit/goutil) Some utils for the Go: string, array/slice, map, format, cli, env, filesystem, test and more 539 | - More, please see https://github.com/gookit 540 | 541 | ## See also 542 | 543 | - Ini parser [gookit/ini/parser](https://github.com/gookit/ini/tree/master/parser) 544 | - Properties parser [gookit/properties](https://github.com/gookit/properties) 545 | - Json5 parser 546 | - [yosuke-furukawa/json5](https://github.com/yosuke-furukawa/json5) 547 | - [titanous/json5](https://github.com/titanous/json5) 548 | - Json parser 549 | - [goccy/go-json](https://github.com/goccy/go-json) 550 | - [json-iterator/go](https://github.com/json-iterator/go) 551 | - Yaml parser 552 | - [goccy/go-yaml](https://github.com/goccy/go-yaml) 553 | - [go-yaml/yaml](https://github.com/go-yaml/yaml) 554 | - Toml parser [go toml](https://github.com/BurntSushi/toml) 555 | - Data merge [mergo](https://github.com/imdario/mergo) 556 | - Map structure [mapstructure](https://github.com/mitchellh/mapstructure) 557 | 558 | ## License 559 | 560 | **MIT** 561 | --------------------------------------------------------------------------------