├── testdata ├── error.ini ├── other.ini ├── refer.md ├── test.ini └── export.ini ├── dotenv ├── testdata │ ├── error.ini │ ├── a.env │ ├── b.env │ └── .env ├── README.md ├── dotenv_test.go └── dotenv.go ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ └── bug_report.md ├── changelog.yml └── workflows │ ├── codecheck.yml │ ├── go.yml │ ├── release.yml │ └── codeql.yml ├── .gitignore ├── go.mod ├── options_test.go ├── go.sum ├── LICENSE ├── issues_test.go ├── internal └── util.go ├── options.go ├── parse.go ├── parser ├── options.go ├── README.md ├── options_test.go ├── encode_test.go ├── encode.go ├── parser_test.go └── parser.go ├── manage_test.go ├── README.zh-CN.md ├── README.md ├── ini.go ├── ini_test.go └── manage.go /testdata/error.ini: -------------------------------------------------------------------------------- 1 | invalid 2 | -------------------------------------------------------------------------------- /dotenv/testdata/error.ini: -------------------------------------------------------------------------------- 1 | DONT_ENV_TEST = 2 | df -------------------------------------------------------------------------------- /dotenv/testdata/a.env: -------------------------------------------------------------------------------- 1 | ENV_KEY_IN_A = "VALUE_IN_A" 2 | -------------------------------------------------------------------------------- /dotenv/testdata/b.env: -------------------------------------------------------------------------------- 1 | ENV_KEY_IN_B = "VALUE_IN_B" 2 | -------------------------------------------------------------------------------- /testdata/other.ini: -------------------------------------------------------------------------------- 1 | ; test data 2 | otherKey = other value 3 | 4 | multi_line = """ 5 | this is a 6 | multi line string 7 | """ 8 | 9 | [other] 10 | key = value 11 | -------------------------------------------------------------------------------- /dotenv/testdata/.env: -------------------------------------------------------------------------------- 1 | # some local env settings 2 | # example: 3 | # KEY=value 4 | 5 | DONT_ENV_TEST="blog" # inline comments 6 | HTTP_URL_TEST=http://127.0.0.1:1081 # comments2 7 | -------------------------------------------------------------------------------- /testdata/refer.md: -------------------------------------------------------------------------------- 1 | # ref 2 | 3 | ```go 4 | package ini 5 | 6 | // section data in ini 7 | type MapValue map[string]string 8 | type ArrValue map[string][]string 9 | 10 | type Section1 struct { 11 | isArray bool 12 | mapValue map[string]string 13 | arrValue map[string][]string 14 | } 15 | 16 | ``` 17 | -------------------------------------------------------------------------------- /testdata/test.ini: -------------------------------------------------------------------------------- 1 | # comments 2 | name = inhere 3 | age = 28 4 | debug = true 5 | hasQuota1 = 'this is val' 6 | hasQuota2 = "this is val1" 7 | shell = ${SHELL} 8 | noEnv = ${NotExist|defValue} 9 | uri = /admin?replicaSet=mgset-4546 10 | 11 | ; comments 12 | [sec1] 13 | key = val0 14 | some = value 15 | stuff = things 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | # Check for updates to GitHub Actions every weekday 13 | interval: "daily" 14 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gookit/ini/v2 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/go-viper/mapstructure/v2 v2.4.0 7 | github.com/gookit/goutil v0.7.2 8 | ) 9 | 10 | require ( 11 | golang.org/x/sync v0.11.0 // indirect 12 | golang.org/x/sys v0.30.0 // indirect 13 | golang.org/x/term v0.29.0 // indirect 14 | golang.org/x/text v0.22.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /testdata/export.ini: -------------------------------------------------------------------------------- 1 | ; exported at 2025-04-18 23:05:27 2 | 3 | age = 28 4 | debug = true 5 | hasQuota1 = this is val 6 | hasQuota2 = "this is val1" # comments for hq2 7 | # comments in first line 8 | name = inhere 9 | noEnv = ${NotExist|defValue} 10 | shell = ${SHELL} 11 | # comments for themes 12 | themes = a,b,c 13 | 14 | [sec1] 15 | age = 23 16 | ; comments for key 17 | key = val0 18 | some = value 19 | stuff = things 20 | user_name = inhere 21 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package ini_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gookit/goutil/testutil/assert" 7 | "github.com/gookit/ini/v2" 8 | ) 9 | 10 | func TestOptions_ReplaceNl(t *testing.T) { 11 | text := ` 12 | name = inhere 13 | desc = i'm a developer, use\n go,php,java 14 | ` 15 | 16 | m := ini.NewWithOptions(ini.ReplaceNl) 17 | assert.NoErr(t, m.LoadStrings(text)) 18 | 19 | assert.Eq(t, "i'm a developer, use\n go,php,java", m.String("desc")) 20 | } 21 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= 2 | github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 3 | github.com/gookit/goutil v0.7.2 h1:NSiqWWY+BT0MwIlKDeSVPfQmr9xTkkAqwDjhplobdgo= 4 | github.com/gookit/goutil v0.7.2/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU= 5 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 6 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 7 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 8 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 9 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 10 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 11 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 12 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 13 | -------------------------------------------------------------------------------- /.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. -------------------------------------------------------------------------------- /issues_test.go: -------------------------------------------------------------------------------- 1 | package ini_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gookit/goutil/dump" 8 | "github.com/gookit/goutil/testutil" 9 | "github.com/gookit/goutil/testutil/assert" 10 | "github.com/gookit/ini/v2" 11 | ) 12 | 13 | // https://github.com/gookit/ini/issues/88 MapStruct does not parse env, when mapping all data #88 14 | func TestIssues_88(t *testing.T) { 15 | type MongoDb struct { 16 | Uri string 17 | } 18 | type Config struct { 19 | MongoDb 20 | } 21 | 22 | defer ini.ResetStd() 23 | 24 | testutil.MockOsEnv(map[string]string{ 25 | "MONGO_URI": "mongodb://localhost:27017", 26 | }, func() { 27 | err := ini.LoadStrings(` 28 | [mongodb] 29 | uri = ${MONGO_URI} 30 | `) 31 | assert.NoErr(t, err) 32 | }) 33 | 34 | dump.P(ini.Data()) 35 | assert.Eq(t, "mongodb://localhost:27017", ini.String("mongodb.uri")) 36 | 37 | // mapping by key 38 | mCfg := MongoDb{} 39 | err := ini.MapStruct("mongodb", &mCfg) 40 | assert.NoErr(t, err) 41 | assert.Eq(t, "mongodb://localhost:27017", mCfg.Uri) 42 | 43 | // mapping all data 44 | cfg := Config{} 45 | err = ini.MapStruct("", &cfg) 46 | assert.NoErr(t, err) 47 | fmt.Println(cfg.MongoDb.Uri) 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/codecheck.yml: -------------------------------------------------------------------------------- 1 | name: CodeCheck 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: Static check and lint check 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: "1.23" 29 | 30 | # - name: Revive lint check 31 | # uses: docker://morphy/revive-action:v2.5.5 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.19, '1.20', 1.21, 1.22, 1.23, 1.24] 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 -v -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 | strategy: 14 | fail-fast: true 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v6 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Setup ENV 23 | # https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable 24 | run: | 25 | echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV 26 | echo "RELEASE_NAME=$GITHUB_WORKFLOW" >> $GITHUB_ENV 27 | 28 | - name: Generate changelog 29 | run: | 30 | curl https://github.com/gookit/gitw/releases/latest/download/chlog-linux-amd64 -L -o /usr/local/bin/chlog 31 | chmod a+x /usr/local/bin/chlog 32 | chlog -c .github/changelog.yml -o changelog.md prev last 33 | 34 | # https://github.com/softprops/action-gh-release 35 | - name: Create release and upload assets 36 | uses: softprops/action-gh-release@v2 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | with: 40 | name: ${{ env.RELEASE_TAG }} 41 | tag_name: ${{ env.RELEASE_TAG }} 42 | body_path: changelog.md 43 | token: ${{ secrets.GITHUB_TOKEN }} 44 | # files: macos-chlog.exe 45 | -------------------------------------------------------------------------------- /dotenv/README.md: -------------------------------------------------------------------------------- 1 | # Dotenv 2 | 3 | Package `dotenv` that supports importing data from files (eg `.env`) to ENV 4 | 5 | - filename support simple glob pattern. eg: `.env.*`, `*.env` 6 | 7 | ## Install 8 | 9 | ```bash 10 | go get github.com/gookit/ini/v2/dotenv 11 | ``` 12 | 13 | ## Usage 14 | 15 | ### Load Env 16 | 17 | ```go 18 | err := dotenv.Load("./", ".env") 19 | // Or use 20 | // err := dotenv.LoadExists("./", ".env") 21 | ``` 22 | 23 | Load from string-map: 24 | 25 | ```go 26 | err := dotenv.LoadFromMap(map[string]string{ 27 | "ENV_KEY": "value", 28 | "LOG_LEVEL": "info", 29 | }) 30 | ``` 31 | 32 | ### Read Env 33 | 34 | ```go 35 | val := dotenv.Get("ENV_KEY") 36 | // Or use 37 | // val := os.Getenv("ENV_KEY") 38 | 39 | // get int value 40 | intVal := dotenv.Int("LOG_LEVEL") 41 | 42 | // get bool value 43 | blVal := dotenv.Bool("OPEN_DEBUG") 44 | 45 | // with default value 46 | val := dotenv.Get("ENV_KEY", "default value") 47 | ``` 48 | 49 | ## Functions API 50 | 51 | ```go 52 | func DontUpperEnvKey() 53 | // get env value 54 | func Bool(name string, defVal ...bool) (val bool) 55 | func Get(name string, defVal ...string) (val string) 56 | func Int(name string, defVal ...int) (val int) 57 | // load env files/data 58 | func Load(dir string, filenames ...string) (err error) 59 | func LoadExistFiles(filePaths ...string) error 60 | func LoadExists(dir string, filenames ...string) error 61 | func LoadFiles(filePaths ...string) (err error) 62 | func LoadFromMap(kv map[string]string) (err error) 63 | // extra methods 64 | func ClearLoaded() 65 | func LoadedFiles() []string 66 | func LoadedData() map[string]string 67 | func Reset() 68 | ``` 69 | 70 | ## License 71 | 72 | **MIT** 73 | -------------------------------------------------------------------------------- /internal/util.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "github.com/go-viper/mapstructure/v2" 4 | 5 | // FullToStruct mapping full mode data to a struct ptr. 6 | func FullToStruct(tagName, defSec string, data map[string]any, ptr any) error { 7 | // collect all default section data to top 8 | anyMap := make(map[string]any, len(data)+4) 9 | if defData, ok := data[defSec]; ok { 10 | for key, val := range defData.(map[string]any) { 11 | anyMap[key] = val 12 | } 13 | } 14 | 15 | for group, mp := range data { 16 | if group == defSec { 17 | continue 18 | } 19 | anyMap[group] = mp 20 | } 21 | return MapStruct(tagName, anyMap, ptr) 22 | } 23 | 24 | // LiteToStruct mapping lite mode data to a struct ptr. 25 | func LiteToStruct(tagName, defSec string, data map[string]map[string]string, ptr any) error { 26 | defMap, ok := data[defSec] 27 | dataNew := make(map[string]any, len(defMap)+len(data)-1) 28 | 29 | // collect default section data to top 30 | if ok { 31 | for key, val := range defMap { 32 | dataNew[key] = val 33 | } 34 | } 35 | 36 | // collect other sections 37 | for secKey, secVals := range data { 38 | if secKey != defSec { 39 | dataNew[secKey] = secVals 40 | } 41 | } 42 | return MapStruct(tagName, dataNew, ptr) 43 | } 44 | 45 | // MapStruct mapping data to a struct ptr. 46 | func MapStruct(tagName string, data any, ptr any) error { 47 | mapConf := &mapstructure.DecoderConfig{ 48 | Metadata: nil, 49 | Result: ptr, 50 | TagName: tagName, 51 | // will auto convert string to int/uint 52 | WeaklyTypedInput: true, 53 | } 54 | 55 | decoder, err := mapstructure.NewDecoder(mapConf) 56 | if err != nil { 57 | return err 58 | } 59 | return decoder.Decode(data) 60 | } 61 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package ini 2 | 3 | import "github.com/gookit/ini/v2/parser" 4 | 5 | // Options for config 6 | type Options struct { 7 | // Readonly set to read-only mode. default False 8 | Readonly bool 9 | // TagName for binding struct 10 | TagName string 11 | // ParseEnv parse ENV var name. default True 12 | ParseEnv bool 13 | // ParseVar parse variable reference "%(varName)s". default False 14 | ParseVar bool 15 | // ReplaceNl replace the "\n" to newline 16 | ReplaceNl bool 17 | 18 | // VarOpen var left open char. default "%(" 19 | VarOpen string 20 | // VarClose var right close char. default ")s" 21 | VarClose string 22 | 23 | // IgnoreCase ignore key name case. default False 24 | IgnoreCase bool 25 | // DefSection default section name. default "__default", it's allow empty string. 26 | DefSection string 27 | // SectionSep sep char for split key path. default ".", use like "section.subKey" 28 | SectionSep string 29 | } 30 | 31 | // newDefaultOptions create a new default Options 32 | // 33 | // Notice: 34 | // 35 | // Cannot use package var instead it. That will allow multiple instances to use the same Options 36 | func newDefaultOptions() *Options { 37 | return &Options{ 38 | ParseEnv: true, 39 | 40 | VarOpen: "%(", 41 | VarClose: ")s", 42 | TagName: DefTagName, 43 | 44 | DefSection: parser.DefSection, 45 | SectionSep: SepSection, 46 | } 47 | } 48 | 49 | // Readonly setting 50 | // 51 | // Usage: 52 | // 53 | // ini.NewWithOptions(ini.Readonly) 54 | func Readonly(opts *Options) { opts.Readonly = true } 55 | 56 | // ParseVar on get value 57 | // 58 | // Usage: 59 | // 60 | // ini.NewWithOptions(ini.ParseVar) 61 | func ParseVar(opts *Options) { opts.ParseVar = true } 62 | 63 | // ParseEnv will parse ENV key on get value 64 | // 65 | // Usage: 66 | // 67 | // ini.NewWithOptions(ini.ParseEnv) 68 | func ParseEnv(opts *Options) { opts.ParseEnv = true } 69 | 70 | // IgnoreCase for get/set value by key 71 | func IgnoreCase(opts *Options) { opts.IgnoreCase = true } 72 | 73 | // ReplaceNl for parse 74 | func ReplaceNl(opts *Options) { opts.ReplaceNl = true } 75 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package ini 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gookit/goutil/envutil" 7 | "github.com/gookit/ini/v2/parser" 8 | ) 9 | 10 | // parse and load ini string 11 | func (c *Ini) parse(str string) (err error) { 12 | if strings.TrimSpace(str) == "" { 13 | return 14 | } 15 | 16 | p := parser.NewLite() 17 | p.Collector = c.valueCollector 18 | p.IgnoreCase = c.opts.IgnoreCase 19 | p.DefSection = c.opts.DefSection 20 | 21 | err = p.ParseString(str) 22 | c.comments = p.Comments() 23 | p.Reset() 24 | return err 25 | } 26 | 27 | // collect value form parser 28 | func (c *Ini) valueCollector(section, key, val string, _ bool) { 29 | if c.opts.IgnoreCase { 30 | key = strings.ToLower(key) 31 | section = strings.ToLower(section) 32 | } 33 | 34 | // backup value on contains var, use for export 35 | if strings.ContainsRune(val, '$') { 36 | c.rawBak[section+"_"+key] = val 37 | // if ParseEnv is true. will parse like: "${SHELL}". 38 | if c.opts.ParseEnv { 39 | val = envutil.ParseValue(val) 40 | } 41 | } 42 | 43 | if c.opts.ReplaceNl { 44 | val = strings.ReplaceAll(val, `\n`, "\n") 45 | } 46 | 47 | if sec, ok := c.data[section]; ok { 48 | sec[key] = val 49 | c.data[section] = sec 50 | } else { 51 | // create the section if it does not exist 52 | c.data[section] = Section{key: val} 53 | } 54 | } 55 | 56 | // parse var reference 57 | func (c *Ini) parseVarReference(key, valStr string, sec Section) string { 58 | if c.opts.VarOpen != "" && strings.Index(valStr, c.opts.VarOpen) == -1 { 59 | return valStr 60 | } 61 | 62 | // http://%(host)s:%(port)s/Portal 63 | // %(section:key)s key in the section 64 | vars := c.varRegex.FindAllString(valStr, -1) 65 | if len(vars) == 0 { 66 | return valStr 67 | } 68 | 69 | varOLen, varCLen := len(c.opts.VarOpen), len(c.opts.VarClose) 70 | 71 | var name string 72 | var oldNew []string 73 | for _, fVar := range vars { 74 | realVal := fVar 75 | name = fVar[varOLen : len(fVar)-varCLen] 76 | 77 | // first, find from current section 78 | if val, ok := sec[name]; ok && key != name { 79 | realVal = val 80 | } else if val, ok = c.getValue(name); ok { 81 | realVal = val 82 | } 83 | 84 | oldNew = append(oldNew, fVar, realVal) 85 | } 86 | 87 | return strings.NewReplacer(oldNew...).Replace(valStr) 88 | } 89 | -------------------------------------------------------------------------------- /parser/options.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | // mode of parse data 4 | // 5 | // ModeFull - will parse array value and inline array 6 | // ModeLite/ModeSimple - don't parse array value line 7 | const ( 8 | ModeFull parseMode = 1 9 | ModeLite parseMode = 2 10 | ModeSimple parseMode = 2 // alias of ModeLite 11 | ) 12 | 13 | // DefSection default section key name 14 | const DefSection = "__default" 15 | 16 | type parseMode uint8 17 | 18 | // Unit8 mode value to uint8 19 | func (m parseMode) Unit8() uint8 { 20 | return uint8(m) 21 | } 22 | 23 | // TagName default tag-name of mapping data to struct 24 | var TagName = "ini" 25 | 26 | // OptFunc define 27 | type OptFunc func(opt *Options) 28 | 29 | // UserCollector custom data collector. 30 | // 31 | // Notice: in lite mode, isSlice always is false. 32 | type UserCollector func(section, key, val string, isSlice bool) 33 | 34 | // Options for parser 35 | type Options struct { 36 | // TagName of mapping data to struct 37 | TagName string 38 | // ParseMode setting. default is ModeLite 39 | ParseMode parseMode 40 | // Ignore case for key name 41 | IgnoreCase bool 42 | // ReplaceNl replace the "\n" to newline 43 | ReplaceNl bool 44 | // default section name. default is "__default" 45 | DefSection string 46 | // NoDefSection setting. NOTE: only for full parse mode 47 | NoDefSection bool 48 | // InlineComment support parse inline comments. default is false 49 | InlineComment bool 50 | // Collector allow you custom the value collector. 51 | // 52 | // Notice: in lite mode, isSlice always is false. 53 | Collector UserCollector 54 | } 55 | 56 | // NewOptions instance 57 | func NewOptions(fns ...OptFunc) *Options { 58 | opt := &Options{ 59 | TagName: TagName, 60 | ParseMode: ModeLite, 61 | DefSection: DefSection, 62 | } 63 | 64 | for _, fn := range fns { 65 | fn(opt) 66 | } 67 | return opt 68 | } 69 | 70 | // InlineComment for parse 71 | func InlineComment(opt *Options) { opt.InlineComment = true } 72 | 73 | // WithReplaceNl for parse 74 | func WithReplaceNl(opt *Options) { opt.ReplaceNl = true } 75 | 76 | // WithParseMode name for parse 77 | func WithParseMode(mode parseMode) OptFunc { 78 | return func(opt *Options) { 79 | opt.ParseMode = mode 80 | } 81 | } 82 | 83 | // WithDefSection name for parse 84 | func WithDefSection(name string) OptFunc { 85 | return func(opt *Options) { 86 | opt.DefSection = name 87 | } 88 | } 89 | 90 | // WithTagName for decode data 91 | func WithTagName(name string) OptFunc { 92 | return func(opt *Options) { 93 | opt.TagName = name 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /parser/README.md: -------------------------------------------------------------------------------- 1 | # INI Parser 2 | 3 | This is a parser for parse INI format content to golang data 4 | 5 | ## Feature 6 | 7 | - Support parse section, array value. 8 | - Support comments start with `;` `#` 9 | - Support multi line comments `/* .. */` 10 | - Support multi line value with `"""` or `'''` 11 | 12 | ## Install 13 | 14 | ```bash 15 | go get github.com/gookit/ini/v2/parser 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```go 21 | package main 22 | 23 | import ( 24 | "github.com/gookit/goutil" 25 | "github.com/gookit/goutil/dump" 26 | "github.com/gookit/ini/v2/parser" 27 | ) 28 | 29 | func main() { 30 | p := parser.New() 31 | 32 | err := p.ParseString(` 33 | # comments 1 34 | name = inhere 35 | age = 28 36 | 37 | ; comments 2 38 | [sec1] 39 | key = val0 40 | some = value 41 | `) 42 | 43 | goutil.PanicErr(err) 44 | // dump parsed data and collected comments map 45 | dump.P(p.ParsedData(), p.Comments()) 46 | } 47 | ``` 48 | 49 | Output: 50 | 51 | ```shell 52 | map[string]map[string]string { #len=2 53 | "__default": map[string]string { #len=7 54 | "name": string("inhere"), #len=6 55 | "age": string("28"), #len=2 56 | }, 57 | "sec1": map[string]string { #len=3 58 | "key": string("val0"), #len=4 59 | "some": string("value"), #len=5 60 | }, 61 | }, 62 | # collected comments 63 | map[string]string { #len=2 64 | "_sec_sec1": string("; comments 2"), #len=12 65 | "__default_name": string("# comments 1"), #len=12 66 | }, 67 | ``` 68 | 69 | ## Functions API 70 | 71 | ```go 72 | func Decode(blob []byte, ptr any) error 73 | func Encode(v any) ([]byte, error) 74 | func EncodeFull(data map[string]any, defSection ...string) (out []byte, err error) 75 | func EncodeLite(data map[string]map[string]string, defSection ...string) (out []byte, err error) 76 | func EncodeSimple(data map[string]map[string]string, defSection ...string) ([]byte, error) 77 | func EncodeWithDefName(v any, defSection ...string) (out []byte, err error) 78 | func IgnoreCase(p *Parser) 79 | func InlineComment(opt *Options) 80 | func NoDefSection(p *Parser) 81 | func WithReplaceNl(opt *Options) 82 | type OptFunc func(opt *Options) 83 | func WithDefSection(name string) OptFunc 84 | func WithParseMode(mode parseMode) OptFunc 85 | func WithTagName(name string) OptFunc 86 | type Options struct{ ... } 87 | func NewOptions(fns ...OptFunc) *Options 88 | type Parser struct{ ... } 89 | func New(fns ...OptFunc) *Parser 90 | func NewFulled(fns ...func(*Parser)) *Parser 91 | func NewLite(fns ...OptFunc) *Parser 92 | func NewSimpled(fns ...func(*Parser)) *Parser 93 | func Parse(data string, mode parseMode, opts ...func(*Parser)) (p *Parser, err error) 94 | ``` 95 | 96 | ## Related 97 | 98 | - [dombenson/go-ini](https://github.com/dombenson/go-ini) ini parser and config manage 99 | - [go-ini/ini](https://github.com/go-ini/ini) ini parser and config manage 100 | 101 | ## License 102 | 103 | **MIT** 104 | -------------------------------------------------------------------------------- /.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 | branches: [ "master" ] 19 | schedule: 20 | - cron: '25 18 * * 2' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners 29 | # Consider using larger runners for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 32 | permissions: 33 | actions: read 34 | contents: read 35 | security-events: write 36 | 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | language: [ 'go' ] 41 | # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] 42 | # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both 43 | # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 44 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 45 | 46 | steps: 47 | - name: Checkout repository 48 | uses: actions/checkout@v6 49 | 50 | # Initializes the CodeQL tools for scanning. 51 | - name: Initialize CodeQL 52 | uses: github/codeql-action/init@v4 53 | with: 54 | languages: ${{ matrix.language }} 55 | # If you wish to specify custom queries, you can do so here or in a config file. 56 | # By default, queries listed here will override any specified in a config file. 57 | # Prefix the list here with "+" to use these queries and those in the config file. 58 | 59 | # For more 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 60 | # queries: security-extended,security-and-quality 61 | 62 | 63 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 64 | # If this step fails, then you should remove it and run the build manually (see below) 65 | - name: Autobuild 66 | uses: github/codeql-action/autobuild@v4 67 | 68 | # ℹ️ Command-line programs to run using the OS shell. 69 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 70 | 71 | # If the Autobuild fails above, remove it and uncomment the following three lines. 72 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 73 | 74 | # - run: | 75 | # echo "Run, Build Application using script" 76 | # ./location_of_script_within_repo/buildscript.sh 77 | 78 | - name: Perform CodeQL Analysis 79 | uses: github/codeql-action/analyze@v4 80 | with: 81 | category: "/language:${{matrix.language}}" 82 | -------------------------------------------------------------------------------- /parser/options_test.go: -------------------------------------------------------------------------------- 1 | package parser_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gookit/goutil/dump" 7 | "github.com/gookit/goutil/testutil/assert" 8 | "github.com/gookit/ini/v2/parser" 9 | ) 10 | 11 | // User struct 12 | type User struct { 13 | Age int `ini:"age"` 14 | Name string `ini:"name"` 15 | Tags []string `ini:"tags"` 16 | } 17 | 18 | func TestNoDefSection(t *testing.T) { 19 | is := assert.New(t) 20 | 21 | // options: NoDefSection 22 | p := parser.NewFulled(parser.NoDefSection) 23 | is.Eq(parser.ModeFull, p.ParseMode) 24 | is.False(p.IgnoreCase) 25 | is.True(p.NoDefSection) 26 | 27 | err := p.ParseString(` 28 | name = inhere 29 | desc = i'm a developer`) 30 | is.Nil(err) 31 | 32 | p.Reset() 33 | is.Empty(p.ParsedData()) 34 | 35 | err = p.ParseString(` 36 | age = 345 37 | name = inhere 38 | [sec1] 39 | newKey = val5 40 | [newSec] 41 | key = val0 42 | arr[] = val0 43 | arr[] = val1 44 | [newSec] 45 | key1 = val1 46 | arr[] = val2 47 | `) 48 | is.Nil(err) 49 | is.NotEmpty(p.ParsedData()) 50 | 51 | mp := p.FullData() 52 | is.ContainsKey(mp, "age") 53 | is.ContainsKey(mp, "name") 54 | 55 | u := &User{} 56 | err = p.Decode(u) 57 | assert.NoErr(t, err) 58 | assert.Eq(t, 345, u.Age) 59 | assert.Eq(t, "inhere", u.Name) 60 | } 61 | 62 | func TestReplaceNl(t *testing.T) { 63 | text := ` 64 | name = inhere 65 | desc = i'm a developer, use\n go,php,java 66 | ` 67 | 68 | p := parser.New(parser.WithDefSection("")) 69 | err := p.ParseString(text) 70 | assert.NoErr(t, err) 71 | assert.NotEmpty(t, p.LiteData()) 72 | assert.Eq(t, `i'm a developer, use\n go,php,java`, p.LiteSection("")["desc"]) 73 | 74 | p = parser.New(parser.WithReplaceNl) 75 | err = p.ParseString(text) 76 | assert.NoErr(t, err) 77 | assert.NotEmpty(t, p.LiteData()) 78 | assert.Eq(t, "i'm a developer, use\n go,php,java", p.LiteSection(p.DefSection)["desc"]) 79 | } 80 | 81 | func TestWithParseMode_full(t *testing.T) { 82 | text := ` 83 | age = 345 84 | name = inhere 85 | tags[] = go 86 | tags[] = php 87 | tags[] = java 88 | [site] 89 | github = github.com/inhere 90 | ` 91 | 92 | // lite mode 93 | p := parser.New() 94 | err := p.ParseBytes([]byte(text)) 95 | assert.NoErr(t, err) 96 | assert.NotEmpty(t, p.LiteData()) 97 | dump.P(p.ParsedData()) 98 | 99 | u := &User{} 100 | err = p.Decode(u) 101 | assert.NoErr(t, err) 102 | assert.Eq(t, 345, u.Age) 103 | assert.Eq(t, "inhere", u.Name) 104 | assert.Empty(t, u.Tags) 105 | 106 | // full mode 107 | p = parser.New(parser.WithParseMode(parser.ModeFull)) 108 | err = p.ParseString(text) 109 | assert.NoErr(t, err) 110 | assert.NotEmpty(t, p.FullData()) 111 | dump.P(p.ParsedData()) 112 | 113 | u1 := &User{} 114 | err = p.Decode(u1) 115 | assert.NoErr(t, err) 116 | assert.Eq(t, 345, u1.Age) 117 | assert.Eq(t, "inhere", u1.Name) 118 | assert.NotEmpty(t, u1.Tags) 119 | } 120 | 121 | func TestWithTagName(t *testing.T) { 122 | text := ` 123 | age = 345 124 | name = inhere # inline comments 125 | desc = i'm a developer, use\n go,php,java 126 | [site] 127 | github = github.com/inhere 128 | ` 129 | 130 | p := parser.NewLite(parser.WithTagName("json"), parser.InlineComment) 131 | err := p.ParseString(text) 132 | assert.NoErr(t, err) 133 | assert.NotEmpty(t, p.LiteData()) 134 | // check comments 135 | assert.NotEmpty(t, p.Comments()) 136 | assert.Eq(t, "# inline comments", p.Comments()["__default_name"]) 137 | 138 | // User struct 139 | type User struct { 140 | Age int `json:"age"` 141 | Name string `json:"name"` 142 | } 143 | 144 | u := &User{} 145 | err = p.Decode(u) 146 | assert.NoErr(t, err) 147 | assert.Eq(t, 345, u.Age) 148 | assert.Eq(t, "inhere", u.Name) 149 | 150 | // UserErr struct 151 | type UserErr struct { 152 | Age map[int]string `json:"age"` 153 | } 154 | 155 | ue := &UserErr{} 156 | err = p.Decode(ue) 157 | // dump.P(ue) 158 | assert.Err(t, err) 159 | } 160 | -------------------------------------------------------------------------------- /parser/encode_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gookit/goutil/testutil/assert" 8 | "github.com/gookit/ini/v2/internal" 9 | ) 10 | 11 | func TestEncode(t *testing.T) { 12 | is := assert.New(t) 13 | 14 | out, err := Encode("invalid") 15 | is.Nil(out) 16 | is.Err(err) 17 | 18 | // empty 19 | out, err = Encode(map[string]any{}) 20 | is.Nil(out) 21 | is.Nil(err) 22 | 23 | // empty 24 | out, err = Encode(map[string]map[string]string{}) 25 | is.Nil(out) 26 | is.Nil(err) 27 | 28 | // encode simple data 29 | sData := map[string]map[string]string{ 30 | "_def": {"name": "inhere", "age": "100"}, 31 | "sec": {"key": "val", "key1": "34"}, 32 | } 33 | out, err = Encode(sData) 34 | is.Nil(err) 35 | is.NotEmpty(out) 36 | 37 | str := string(out) 38 | is.Contains(str, "[_def]") 39 | is.Contains(str, "[sec]") 40 | is.Contains(str, "name = inhere") 41 | 42 | out, err = EncodeSimple(sData, "_def") 43 | is.Nil(err) 44 | is.NotEmpty(out) 45 | 46 | str = string(out) 47 | fmt.Println("---- lite mode: ----") 48 | fmt.Println(str) 49 | is.NotContains(str, "[_def]") 50 | is.Contains(str, "[sec]") 51 | is.Contains(str, "name = inhere") 52 | 53 | // encode full data 54 | fData := map[string]any{ 55 | "name": "inhere", 56 | "age": 12, 57 | "debug": false, 58 | "defArr": []string{"a", "b"}, 59 | "defArr1": []int{1, 2}, 60 | // section 61 | "sec": map[string]any{ 62 | "key0": "val", 63 | "key1": 45, 64 | "arr0": []int{3, 4}, 65 | "arr1": []string{"c", "d"}, 66 | "invalid": map[string]int{"k": 23}, 67 | }, 68 | } 69 | 70 | out, err = Encode(fData) 71 | is.Nil(err) 72 | is.NotEmpty(out) 73 | 74 | str = string(out) 75 | fmt.Println("---- full mode: ----") 76 | fmt.Println(str) 77 | is.Contains(str, "age = 12") 78 | is.Contains(str, "debug = false") 79 | is.Contains(str, "name = inhere") 80 | is.Contains(str, "defArr[] = a") 81 | is.Contains(str, "[sec]") 82 | is.Contains(str, "arr1[] = c") 83 | 84 | out, err = EncodeFull(fData, "defSec") 85 | is.Nil(err) 86 | is.NotEmpty(out) 87 | str = string(out) 88 | is.Contains(str, "[sec]") 89 | 90 | out, err = EncodeWithDefName(fData, "sec") 91 | is.Nil(err) 92 | is.NotEmpty(out) 93 | str = string(out) 94 | is.NotContains(str, "[sec]") 95 | } 96 | 97 | func TestEncode_struct(t *testing.T) { 98 | is := assert.New(t) 99 | 100 | // encode a struct 101 | type Sample struct { 102 | Debug bool 103 | Name string `json:"name"` 104 | DefArr []string 105 | } 106 | sp := &Sample{ 107 | Debug: true, 108 | Name: "inhere", 109 | DefArr: []string{"a", "b"}, 110 | } 111 | out, err := Encode(sp) 112 | is.Nil(err) 113 | is.NotEmpty(out) 114 | str := string(out) 115 | fmt.Println(str) 116 | is.Contains(str, "Debug = true") 117 | is.Contains(str, "name = inhere") 118 | } 119 | 120 | var liteData = map[string]map[string]string{ 121 | "z_def": {"name": "inhere", "age": "100"}, 122 | "sec": {"key": "val", "key1": "34"}, 123 | } 124 | 125 | func TestEncodeLite(t *testing.T) { 126 | is := assert.New(t) 127 | 128 | out, err := EncodeLite(liteData, "z_def") 129 | is.Nil(err) 130 | is.NotEmpty(out) 131 | 132 | str := string(out) 133 | fmt.Println("---- lite mode: ----") 134 | fmt.Println(str) 135 | is.NotContains(str, "[z_def]") 136 | is.Contains(str, "[sec]") 137 | is.Contains(str, "name = inhere") 138 | } 139 | 140 | func TestEncodeWith(t *testing.T) { 141 | is := assert.New(t) 142 | 143 | // with comments and raw value 144 | out, err := EncodeWith(liteData, &EncodeOptions{ 145 | Comments: map[string]string{ 146 | "z_def": "; this is a comment", 147 | "z_def_age": "# comment for age", 148 | }, 149 | RawValueMap: map[string]string{ 150 | "sec_key": "${ENV_VAR1}", 151 | }, 152 | }) 153 | str := string(out) 154 | fmt.Println(str) 155 | is.StrContains(str, "key = ${ENV_VAR1}") 156 | is.StrContains(str, "# comment for age") 157 | is.StrContains(str, "; this is a comment") 158 | 159 | // with nil options 160 | out, err = EncodeWith(liteData, nil) 161 | is.Nil(err) 162 | is.NotEmpty(out) 163 | str = string(out) 164 | is.StrContains(str, "[z_def]") 165 | 166 | // invalid params 167 | _, err = EncodeWith("invalid", nil) 168 | is.Err(err) 169 | _, err = EncodeWith(nil, nil) 170 | is.Err(err) 171 | } 172 | 173 | func TestMapStruct_err(t *testing.T) { 174 | assert.Err(t, internal.MapStruct("json", "invalid", nil)) 175 | } 176 | -------------------------------------------------------------------------------- /dotenv/dotenv_test.go: -------------------------------------------------------------------------------- 1 | package dotenv 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "testing" 8 | 9 | "github.com/gookit/goutil/testutil/assert" 10 | ) 11 | 12 | func TestLoad(t *testing.T) { 13 | defer ClearLoaded() 14 | err := Load("./testdata", "not-exist", ".env") 15 | assert.Err(t, err) 16 | 17 | assert.Eq(t, "", os.Getenv("DONT_ENV_TEST")) 18 | assert.Eq(t, "", os.Getenv("HTTP_URL_TEST")) 19 | 20 | err = Load("./testdata") 21 | assert.NoErr(t, err) 22 | assert.Eq(t, "blog", os.Getenv("DONT_ENV_TEST")) 23 | assert.Eq(t, "blog", Get("DONT_ENV_TEST")) 24 | assert.Eq(t, "http://127.0.0.1:1081", Get("HTTP_URL_TEST")) 25 | _ = os.Unsetenv("DONT_ENV_TEST") // clear 26 | 27 | err = Load("./testdata", "error.ini") 28 | assert.Err(t, err) 29 | 30 | err = Load("./testdata", "invalid_key.ini") 31 | assert.Err(t, err) 32 | 33 | assert.Eq(t, "def-val", Get("NOT-EXIST", "def-val")) 34 | 35 | // load by glob match 36 | assert.Empty(t, Get("ENV_KEY_IN_A")) 37 | err = Load("./testdata", "*.env") 38 | assert.NoErr(t, err) 39 | assert.Eq(t, "VALUE_IN_A", Get("ENV_KEY_IN_A")) 40 | // invalid pattern 41 | assert.Err(t, Load("./testdata", "ab[[c*")) 42 | } 43 | 44 | func TestLoadMatched(t *testing.T) { 45 | defer Reset() 46 | 47 | // dir not exist 48 | assert.Nil(t, LoadMatched("./dir-not-exists", "*.env")) 49 | // not match any file 50 | assert.Nil(t, LoadMatched("./testdata", "invalid_key.ini")) 51 | assert.Empty(t, Get("ENV_KEY_IN_A")) 52 | assert.Empty(t, Get("ENV_KEY_IN_B")) 53 | 54 | assert.Nil(t, LoadMatched("./testdata")) 55 | assert.Eq(t, "VALUE_IN_A", Get("ENV_KEY_IN_A")) 56 | assert.Eq(t, "VALUE_IN_B", Get("ENV_KEY_IN_B")) 57 | 58 | // invalid pattern 59 | assert.Err(t, LoadMatched("./testdata", "ab[[c")) 60 | } 61 | 62 | func TestLoadFiles(t *testing.T) { 63 | defer Reset() 64 | assert.Err(t, LoadFiles("./testdata/not-exist")) 65 | assert.Eq(t, "", os.Getenv("DONT_ENV_TEST")) 66 | 67 | err := LoadFiles("./testdata/.env") 68 | 69 | assert.NoErr(t, err) 70 | assert.NotEmpty(t, LoadedData()) 71 | assert.NotEmpty(t, LoadedFiles()) 72 | assert.Eq(t, "blog", os.Getenv("DONT_ENV_TEST")) 73 | assert.Eq(t, "blog", Get("DONT_ENV_TEST")) 74 | } 75 | 76 | func TestLoadExists(t *testing.T) { 77 | defer Reset() 78 | assert.Eq(t, "", os.Getenv("DONT_ENV_TEST")) 79 | 80 | err := LoadExists("./testdata", "not-exist", ".env") 81 | 82 | assert.NoErr(t, err) 83 | assert.Eq(t, "blog", os.Getenv("DONT_ENV_TEST")) 84 | assert.Eq(t, "blog", Get("DONT_ENV_TEST")) 85 | } 86 | 87 | func TestLoadExistFiles(t *testing.T) { 88 | defer Reset() 89 | assert.Eq(t, "", os.Getenv("DONT_ENV_TEST")) 90 | 91 | err := LoadExistFiles("./testdata/not-exist", "./testdata/.env") 92 | 93 | assert.NoErr(t, err) 94 | assert.Eq(t, "blog", os.Getenv("DONT_ENV_TEST")) 95 | assert.Eq(t, "blog", Get("DONT_ENV_TEST")) 96 | } 97 | 98 | func TestLoadFromMap(t *testing.T) { 99 | assert.Eq(t, "", os.Getenv("DONT_ENV_TEST")) 100 | 101 | err := LoadFromMap(map[string]string{ 102 | "DONT_ENV_TEST": "blog", 103 | "dont_env_test1": "val1", 104 | "dont_env_test2": "23", 105 | "dont_env_bool": "true", 106 | }) 107 | 108 | assert.NoErr(t, err) 109 | 110 | // fmt.Println(os.Environ()) 111 | envStr := fmt.Sprint(os.Environ()) 112 | assert.Contains(t, envStr, "DONT_ENV_TEST=blog") 113 | assert.Contains(t, envStr, "DONT_ENV_TEST1=val1") 114 | 115 | assert.Eq(t, "blog", Get("DONT_ENV_TEST")) 116 | assert.Eq(t, "blog", os.Getenv("DONT_ENV_TEST")) 117 | assert.Eq(t, "val1", Get("DONT_ENV_TEST1")) 118 | assert.Eq(t, 23, Int("DONT_ENV_TEST2")) 119 | assert.True(t, Bool("dont_env_bool")) 120 | 121 | assert.Eq(t, "val1", Get("dont_env_test1")) 122 | assert.Eq(t, 23, Int("dont_env_test2")) 123 | 124 | assert.Eq(t, 20, Int("dont_env_test1", 20)) 125 | assert.Eq(t, 20, Int("dont_env_not_exist", 20)) 126 | assert.False(t, Bool("dont_env_not_exist", false)) 127 | 128 | // check cache 129 | assert.Contains(t, LoadedData(), "DONT_ENV_TEST2") 130 | 131 | // clear 132 | ClearLoaded() 133 | assert.Eq(t, "", os.Getenv("DONT_ENV_TEST")) 134 | assert.Eq(t, "", Get("DONT_ENV_TEST1")) 135 | 136 | err = LoadFromMap(map[string]string{ 137 | "": "val", 138 | }) 139 | assert.Err(t, err) 140 | } 141 | 142 | func TestDontUpperEnvKey(t *testing.T) { 143 | assert.Eq(t, "", os.Getenv("DONT_ENV_TEST")) 144 | 145 | DontUpperEnvKey() 146 | 147 | err := LoadFromMap(map[string]string{ 148 | "dont_env_test": "val", 149 | }) 150 | 151 | assert.Contains(t, fmt.Sprint(os.Environ()), "dont_env_test=val") 152 | assert.NoErr(t, err) 153 | assert.Eq(t, "val", Get("dont_env_test")) 154 | 155 | // on windows, os.Getenv() not case sensitive 156 | if runtime.GOOS == "windows" { 157 | assert.Eq(t, "val", Get("DONT_ENV_TEST")) 158 | } else { 159 | assert.Eq(t, "", Get("DONT_ENV_TEST")) 160 | } 161 | 162 | UpperEnvKey = true // revert 163 | ClearLoaded() 164 | } 165 | -------------------------------------------------------------------------------- /dotenv/dotenv.go: -------------------------------------------------------------------------------- 1 | // Package dotenv provide load .env data to os ENV 2 | package dotenv 3 | 4 | import ( 5 | "bufio" 6 | "os" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/gookit/goutil/fsutil" 12 | "github.com/gookit/ini/v2/parser" 13 | ) 14 | 15 | var ( 16 | // UpperEnvKey change key to upper on set ENV 17 | UpperEnvKey = true 18 | 19 | // DefaultName default file name 20 | DefaultName = ".env" 21 | 22 | // OnlyLoadExists only load on file exists 23 | OnlyLoadExists bool 24 | 25 | // save original Env data 26 | // originalEnv []string 27 | 28 | // cache all lib loaded ENV data 29 | loadedData = map[string]string{} 30 | loadedFiles []string // cache all loaded files 31 | ) 32 | 33 | // DontUpperEnvKey don't change key to upper on set ENV 34 | func DontUpperEnvKey() { UpperEnvKey = false } 35 | 36 | // LoadedData get all loaded data by dotenv 37 | func LoadedData() map[string]string { return loadedData } 38 | 39 | // LoadedFiles get all loaded files 40 | func LoadedFiles() []string { return loadedFiles } 41 | 42 | // Reset clear the previously set ENV value 43 | func Reset() { ClearLoaded() } 44 | 45 | // ClearLoaded clear the previously set ENV value 46 | func ClearLoaded() { 47 | for key := range loadedData { 48 | _ = os.Unsetenv(key) 49 | } 50 | 51 | // reset 52 | loadedData = map[string]string{} 53 | } 54 | 55 | // 56 | // -------------------- load env file/data -------------------- 57 | // 58 | 59 | // Load parse dotenv file data to os ENV. default load ".env" file 60 | // 61 | // - NEW: filename support simple glob pattern. eg: ".env.*", "*.env" 62 | // 63 | // Usage: 64 | // 65 | // dotenv.Load("./", ".env") 66 | func Load(dir string, filenames ...string) (err error) { 67 | if len(filenames) == 0 { 68 | filenames = []string{DefaultName} 69 | } 70 | 71 | for _, filename := range filenames { 72 | // filename support simple glob pattern. 73 | if strings.ContainsRune(filename, '*') { 74 | if err = loadMatched(dir, filename); err != nil { 75 | break 76 | } 77 | continue 78 | } 79 | 80 | filePath := filepath.Join(dir, filename) 81 | if err = loadFile(filePath); err != nil { 82 | break 83 | } 84 | } 85 | return 86 | } 87 | 88 | // LoadMatched load env files by match filename pattern. Default pattern is *.env 89 | // 90 | // Usage: 91 | // 92 | // dotenv.LoadMatched("./local") 93 | // dotenv.LoadMatched("./", "*.env") 94 | func LoadMatched(dir string, patterns ...string) error { 95 | if !fsutil.DirExist(dir) { 96 | return nil 97 | } 98 | if len(patterns) == 0 { 99 | patterns = []string{"*.env"} 100 | } 101 | 102 | for _, pattern := range patterns { 103 | if err := loadMatched(dir, pattern); err != nil { 104 | return err 105 | } 106 | } 107 | return nil 108 | } 109 | 110 | func loadMatched(dir string, pattern string) error { 111 | matches, err := filepath.Glob(filepath.Join(dir, pattern)) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | if len(matches) == 0 { 117 | return nil 118 | } 119 | return LoadFiles(matches...) 120 | } 121 | 122 | // LoadExists only load on file exists. see Load 123 | func LoadExists(dir string, filenames ...string) error { 124 | oldVal := OnlyLoadExists 125 | 126 | OnlyLoadExists = true 127 | err := Load(dir, filenames...) 128 | OnlyLoadExists = oldVal 129 | 130 | return err 131 | } 132 | 133 | // LoadFiles load ENV from given file path. 134 | func LoadFiles(filePaths ...string) (err error) { 135 | for _, filePath := range filePaths { 136 | if err = loadFile(filePath); err != nil { 137 | break 138 | } 139 | } 140 | return 141 | } 142 | 143 | // LoadExistFiles load ENV from given files, only load exists 144 | func LoadExistFiles(filePaths ...string) error { 145 | oldVal := OnlyLoadExists 146 | defer func() { 147 | OnlyLoadExists = oldVal 148 | }() 149 | 150 | OnlyLoadExists = true 151 | return LoadFiles(filePaths...) 152 | } 153 | 154 | // LoadFromMap load data from given string map 155 | func LoadFromMap(kv map[string]string) (err error) { 156 | for key, val := range kv { 157 | if UpperEnvKey { 158 | key = strings.ToUpper(key) 159 | } 160 | 161 | err = os.Setenv(key, val) 162 | if err != nil { 163 | break 164 | } 165 | 166 | // cache it 167 | loadedData[key] = val 168 | } 169 | return 170 | } 171 | 172 | // 173 | // -------------------- get env value -------------------- 174 | // 175 | 176 | // Get os ENV value by name 177 | func Get(name string, defVal ...string) (val string) { 178 | if val, ok := getVal(name); ok { 179 | return val 180 | } 181 | 182 | if len(defVal) > 0 { 183 | val = defVal[0] 184 | } 185 | return 186 | } 187 | 188 | // Bool get a bool value by key 189 | func Bool(name string, defVal ...bool) (val bool) { 190 | if str, ok := getVal(name); ok { 191 | val, err := strconv.ParseBool(str) 192 | if err == nil { 193 | return val 194 | } 195 | } 196 | 197 | if len(defVal) > 0 { 198 | val = defVal[0] 199 | } 200 | return 201 | } 202 | 203 | // Int get an int value by key 204 | func Int(name string, defVal ...int) (val int) { 205 | if str, ok := getVal(name); ok { 206 | val, err := strconv.ParseInt(str, 10, 0) 207 | if err == nil { 208 | return int(val) 209 | } 210 | } 211 | 212 | if len(defVal) > 0 { 213 | val = defVal[0] 214 | } 215 | return 216 | } 217 | 218 | func getVal(name string) (val string, ok bool) { 219 | if UpperEnvKey { 220 | name = strings.ToUpper(name) 221 | } 222 | 223 | // cached 224 | if val = loadedData[name]; val != "" { 225 | ok = true 226 | return 227 | } 228 | 229 | // NOTICE: if is windows OS, os.Getenv() Key is not case-sensitive 230 | return os.LookupEnv(name) 231 | } 232 | 233 | // load and parse .env file data to os ENV 234 | func loadFile(file string) (err error) { 235 | fd, err := os.Open(file) 236 | if err != nil { 237 | if OnlyLoadExists && os.IsNotExist(err) { 238 | return nil 239 | } 240 | return err 241 | } 242 | 243 | //noinspection GoUnhandledErrorResult 244 | defer fd.Close() 245 | 246 | // parse file contents 247 | p := parser.NewLite(parser.InlineComment) 248 | if _, err = p.ParseFrom(bufio.NewScanner(fd)); err != nil { 249 | return 250 | } 251 | 252 | // set data to os ENV 253 | if mp := p.LiteSection(p.DefSection); len(mp) > 0 { 254 | err = LoadFromMap(mp) 255 | } 256 | 257 | // add to loadedFiles 258 | loadedFiles = append(loadedFiles, file) 259 | return 260 | } 261 | -------------------------------------------------------------------------------- /parser/encode.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "sort" 8 | 9 | "github.com/gookit/goutil/structs" 10 | "github.com/gookit/goutil/timex" 11 | ) 12 | 13 | type EncodeOptions struct { 14 | // DefSection name 15 | DefSection string 16 | // AddExportDate add export date to head of file. default: true 17 | AddExportDate bool 18 | // Comments comments map, key is `section +"_"+ key`, value is comment. 19 | Comments map[string]string 20 | // RawValueMap raw value map, key is `section +"_"+ key`, value is raw value. 21 | // 22 | // TIP: if you want to set raw value to INI file, you can use this option. see `rawBak` in ini.Ini 23 | RawValueMap map[string]string 24 | } 25 | 26 | func newEncodeOptions(defSection []string) *EncodeOptions { 27 | opts := &EncodeOptions{AddExportDate: true} 28 | if len(defSection) > 0 { 29 | opts.DefSection = defSection[0] 30 | } 31 | return opts 32 | } 33 | 34 | // EncodeWith golang data(map, struct) to INI string, can with options. 35 | func EncodeWith(v any, opts *EncodeOptions) ([]byte, error) { 36 | if opts == nil { 37 | opts = &EncodeOptions{AddExportDate: true} 38 | } 39 | 40 | switch vd := v.(type) { 41 | case map[string]any: // from full mode 42 | return encodeFull(vd, opts) 43 | case map[string]map[string]string: // from lite mode 44 | return encodeLite(vd, opts) 45 | default: 46 | if vd != nil { 47 | // as struct data, use structs.ToMap convert 48 | anyMap, err := structs.StructToMap(vd) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return encodeFull(anyMap, opts) 53 | } 54 | return nil, errors.New("ini: invalid data to encode as INI") 55 | } 56 | } 57 | 58 | // Encode golang data(map, struct) to INI string. 59 | func Encode(v any) ([]byte, error) { return EncodeWithDefName(v) } 60 | 61 | // EncodeWithDefName golang data(map, struct) to INI, can set default section name 62 | func EncodeWithDefName(v any, defSection ...string) (out []byte, err error) { 63 | return EncodeWith(v, newEncodeOptions(defSection)) 64 | } 65 | 66 | // EncodeFull full mode data to INI, can set default section name 67 | func EncodeFull(data map[string]any, defSection ...string) (out []byte, err error) { 68 | return encodeFull(data, newEncodeOptions(defSection)) 69 | } 70 | 71 | // EncodeSimple data to INI 72 | func EncodeSimple(data map[string]map[string]string, defSection ...string) ([]byte, error) { 73 | return encodeLite(data, newEncodeOptions(defSection)) 74 | } 75 | 76 | // EncodeLite data to INI 77 | func EncodeLite(data map[string]map[string]string, defSection ...string) (out []byte, err error) { 78 | return encodeLite(data, newEncodeOptions(defSection)) 79 | } 80 | 81 | // EncodeFull full mode data to INI, can set default section name 82 | func encodeFull(data map[string]any, opts *EncodeOptions) (out []byte, err error) { 83 | ln := len(data) 84 | if ln == 0 { 85 | return 86 | } 87 | 88 | defSecName := opts.DefSection 89 | sortedGroups := make([]string, 0, ln) 90 | for section := range data { 91 | sortedGroups = append(sortedGroups, section) 92 | } 93 | 94 | buf := &bytes.Buffer{} 95 | buf.Grow(ln * 4) 96 | if opts.AddExportDate { 97 | buf.WriteString("; exported at " + timex.Now().Datetime() + "\n\n") 98 | } 99 | 100 | sort.Strings(sortedGroups) 101 | maxLn := len(sortedGroups) - 1 102 | secBuf := &bytes.Buffer{} 103 | 104 | for idx, section := range sortedGroups { 105 | item := data[section] 106 | switch tpData := item.(type) { 107 | case []int: 108 | case []string: // array of the default section 109 | for _, v := range tpData { 110 | buf.WriteString(fmt.Sprintf("%s[] = %v\n", section, v)) 111 | } 112 | // case map[string]string: // is section 113 | case map[string]any: // is section 114 | if section != defSecName { 115 | secBuf.WriteString("[" + section + "]\n") 116 | writeAnyMap(secBuf, tpData) 117 | } else { 118 | writeAnyMap(buf, tpData) 119 | } 120 | 121 | if idx < maxLn { 122 | secBuf.WriteByte('\n') 123 | } 124 | default: // k-v of the default section 125 | buf.WriteString(fmt.Sprintf("%s = %v\n", section, tpData)) 126 | } 127 | } 128 | 129 | buf.WriteByte('\n') 130 | buf.Write(secBuf.Bytes()) 131 | out = buf.Bytes() 132 | secBuf = nil 133 | return 134 | } 135 | 136 | func writeAnyMap(buf *bytes.Buffer, data map[string]any) { 137 | for key, item := range data { 138 | switch tpData := item.(type) { 139 | case []int: 140 | case []string: // array of the default section 141 | for _, v := range tpData { 142 | buf.WriteString(key + "[] = ") 143 | buf.WriteString(fmt.Sprint(v)) 144 | buf.WriteByte('\n') 145 | } 146 | default: // k-v of the section 147 | buf.WriteString(key + " = ") 148 | buf.WriteString(fmt.Sprint(tpData)) 149 | buf.WriteByte('\n') 150 | } 151 | } 152 | } 153 | 154 | func encodeLite(data map[string]map[string]string, opts *EncodeOptions) (out []byte, err error) { 155 | ln := len(data) 156 | if ln == 0 { 157 | return 158 | } 159 | 160 | defSecName := opts.DefSection 161 | sortedGroups := make([]string, 0, ln) 162 | for section := range data { 163 | // don't add section title for default section 164 | if section != defSecName { 165 | sortedGroups = append(sortedGroups, section) 166 | } 167 | } 168 | 169 | buf := &bytes.Buffer{} 170 | buf.Grow(ln * 4) 171 | if opts.AddExportDate { 172 | buf.WriteString("; exported at " + timex.Now().Datetime() + "\n\n") 173 | } 174 | 175 | // first, write default section values 176 | if defSec, ok := data[defSecName]; ok { 177 | writeStrMap(buf, defSec, defSecName, opts) 178 | buf.WriteByte('\n') 179 | } 180 | 181 | sort.Strings(sortedGroups) 182 | maxLn := len(sortedGroups) - 1 183 | for idx, section := range sortedGroups { 184 | // comments for section 185 | if s, ok := opts.Comments[section]; ok { 186 | buf.WriteString(s + "\n") 187 | } 188 | 189 | buf.WriteString("[" + section + "]\n") 190 | writeStrMap(buf, data[section], section, opts) 191 | 192 | if idx < maxLn { 193 | buf.WriteByte('\n') 194 | } 195 | } 196 | 197 | out = buf.Bytes() 198 | return 199 | } 200 | 201 | func writeStrMap(buf *bytes.Buffer, strMap map[string]string, section string, opts *EncodeOptions) { 202 | sortedKeys := make([]string, 0, len(strMap)) 203 | for key := range strMap { 204 | sortedKeys = append(sortedKeys, key) 205 | } 206 | 207 | sort.Strings(sortedKeys) 208 | for _, key := range sortedKeys { 209 | value := strMap[key] 210 | keyPath := section + "_" + key 211 | // add comments 212 | if s, ok := opts.Comments[keyPath]; ok { 213 | buf.WriteString(s + "\n") 214 | } 215 | 216 | if val1, ok := opts.RawValueMap[keyPath]; ok { 217 | value = val1 218 | } 219 | buf.WriteString(key + " = " + value + "\n") 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /manage_test.go: -------------------------------------------------------------------------------- 1 | package ini_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gookit/goutil/testutil/assert" 8 | "github.com/gookit/ini/v2" 9 | ) 10 | 11 | func TestIni_Get(t *testing.T) { 12 | is := assert.New(t) 13 | 14 | err := ini.LoadStrings(iniStr) 15 | is.Nil(err) 16 | is.Nil(ini.Error()) 17 | 18 | conf := ini.Default() 19 | 20 | // get value 21 | str, ok := conf.GetValue("age") 22 | is.True(ok) 23 | is.Eq("28", str) 24 | 25 | str, ok = ini.GetValue("not-exist") 26 | is.False(ok) 27 | is.Eq("", str) 28 | 29 | // get 30 | str = conf.Get("age") 31 | is.Eq("28", str) 32 | 33 | str = ini.Get("age") 34 | is.Eq("28", str) 35 | 36 | str = ini.Get("not-exist", "defval") 37 | is.Eq("defval", str) 38 | 39 | // get int 40 | iv := conf.Int("age") 41 | is.Eq(28, iv) 42 | 43 | iv = conf.Int("name") 44 | is.Eq(0, iv) 45 | 46 | iv = ini.Int("name", 23) 47 | is.True(ini.HasKey("name")) 48 | is.Eq(0, iv) 49 | is.Err(ini.Error()) 50 | 51 | iv = conf.Int("age", 34) 52 | is.Eq(28, iv) 53 | iv = conf.Int("notExist", 34) 54 | is.Eq(34, iv) 55 | 56 | iv = conf.Int("age") 57 | is.Eq(28, iv) 58 | iv = conf.Int("notExist") 59 | is.Eq(0, iv) 60 | 61 | // get bool 62 | str = conf.Get("debug") 63 | is.True(conf.HasKey("debug")) 64 | is.Eq("true", str) 65 | 66 | bv := conf.Bool("debug") 67 | is.True(bv) 68 | 69 | bv = conf.Bool("name") 70 | is.False(bv) 71 | 72 | bv = ini.Bool("debug", false) 73 | is.Eq(true, bv) 74 | bv = conf.Bool("notExist") 75 | is.Eq(false, bv) 76 | 77 | bv = conf.Bool("notExist", true) 78 | is.True(bv) 79 | 80 | // get string 81 | val := conf.Get("name") 82 | is.Eq("inhere", val) 83 | 84 | str = conf.String("notExists") 85 | is.Eq("", str) 86 | 87 | str = ini.String("notExists", "defVal") 88 | is.Eq("defVal", str) 89 | 90 | str = conf.String("name") 91 | is.Eq("inhere", str) 92 | 93 | str = conf.String("notExists") 94 | is.Eq("", str) 95 | 96 | str = conf.String("hasQuota1") 97 | is.Eq("this is val", str) 98 | 99 | str = conf.String("hasquota1") 100 | is.Eq("", str) 101 | 102 | // get by path 103 | str = conf.Get("sec1.some") 104 | is.Eq("value", str) 105 | 106 | str = conf.Get("no-sec.some") 107 | is.Eq("", str) 108 | 109 | // get string map(section data) 110 | mp := conf.StringMap("sec1") 111 | is.Eq("val0", mp["key"]) 112 | 113 | mp = ini.StringMap("sec1") 114 | is.Eq("val0", mp["key"]) 115 | 116 | mp = conf.StringMap("notExist") 117 | is.Len(mp, 0) 118 | 119 | // def section 120 | mp = conf.StringMap("") 121 | is.Eq("inhere", mp["name"]) 122 | is.NotContains(mp["notExist"], "${") 123 | 124 | str = conf.Get(" ") 125 | is.Eq("", str) 126 | 127 | ss := ini.Strings("themes") 128 | is.Eq([]string{"a", "b", "c"}, ss) 129 | 130 | ini.Reset() 131 | } 132 | 133 | func TestInt(t *testing.T) { 134 | ini.Reset() 135 | 136 | err := ini.LoadStrings(iniStr) 137 | assert.NoErr(t, err) 138 | 139 | // uint 140 | assert.Eq(t, uint(28), ini.Uint("age")) 141 | assert.Eq(t, uint(0), ini.Uint("not-exist")) 142 | assert.Eq(t, uint(10), ini.Uint("not-exist", 10)) 143 | 144 | // int64 145 | assert.Eq(t, int64(28), ini.Int64("age")) 146 | assert.Eq(t, int64(0), ini.Int64("not-exist")) 147 | assert.Eq(t, int64(10), ini.Int64("not-exist", 10)) 148 | 149 | ini.Reset() 150 | } 151 | 152 | func TestIni_Set(t *testing.T) { 153 | is := assert.New(t) 154 | 155 | err := ini.LoadStrings(iniStr) 156 | is.Nil(err) 157 | 158 | conf := ini.Default() 159 | 160 | err = conf.Set("float", 34.5) 161 | is.Nil(err) 162 | is.Eq("34.5", conf.String("float")) 163 | 164 | err = ini.Set(" ", "val") 165 | is.Err(err) 166 | is.False(conf.HasKey(" ")) 167 | 168 | err = conf.Set("key", "val", "newSec") 169 | is.Nil(err) 170 | is.True(conf.HasSection("newSec")) 171 | 172 | val := conf.Get("newSec.key") 173 | is.Eq("val", val) 174 | 175 | mp := conf.StringMap("newSec") 176 | is.Eq("val", mp["key"]) 177 | 178 | err = conf.SetSection("newSec1", map[string]string{"k0": "v0"}) 179 | is.Nil(err) 180 | is.True(conf.HasSection("newSec1")) 181 | 182 | mp = conf.Section("newSec1") 183 | is.Eq("v0", mp["k0"]) 184 | 185 | err = conf.NewSection("NewSec2", map[string]string{"kEy0": "val"}) 186 | is.Nil(err) 187 | 188 | err = conf.Set("int", 345, "newSec") 189 | is.Nil(err) 190 | iv := conf.Int("newSec.int") 191 | is.Eq(345, iv) 192 | 193 | err = conf.Set("bol", false, "newSec") 194 | is.Nil(err) 195 | bv := conf.Bool("newSec.bol") 196 | is.False(bv) 197 | 198 | err = conf.Set("bol", true, "newSec") 199 | is.Nil(err) 200 | bv = conf.Bool("newSec.bol") 201 | is.True(ini.HasKey("newSec.bol")) 202 | is.True(bv) 203 | 204 | err = conf.Set("name", "new name") 205 | is.Nil(err) 206 | str := conf.String("name") 207 | is.Eq("new name", str) 208 | 209 | err = conf.Set("can2arr", "va0,val1,val2") 210 | is.Nil(err) 211 | 212 | ss := conf.Strings("can2arr-no", ",") 213 | is.Empty(ss) 214 | 215 | ss = conf.Strings("can2arr", ",") 216 | is.Eq("[va0 val1 val2]", fmt.Sprint(ss)) 217 | } 218 | 219 | func TestIni_Delete(t *testing.T) { 220 | st := assert.New(t) 221 | 222 | err := ini.LoadStrings(iniStr) 223 | st.Nil(err) 224 | 225 | conf := ini.Default() 226 | 227 | st.True(conf.HasKey("name")) 228 | ok := ini.Delete("name") 229 | st.True(ok) 230 | st.False(conf.HasKey("name")) 231 | 232 | st.False(conf.Delete(" ")) 233 | st.False(conf.Delete("no-key")) 234 | 235 | ok = ini.Delete("sec1.notExist") 236 | st.False(ok) 237 | ok = conf.Delete("sec1.key") 238 | st.True(ok) 239 | 240 | ok = conf.Delete("no-sec.key") 241 | st.False(ok) 242 | st.True(conf.HasSection("sec1")) 243 | 244 | ok = conf.DelSection("sec1") 245 | st.True(ok) 246 | st.False(conf.HasSection("sec1")) 247 | 248 | ini.Reset() 249 | } 250 | 251 | func TestIni_MapStruct(t *testing.T) { 252 | is := assert.New(t) 253 | err := ini.LoadStrings(iniStr) 254 | is.Nil(err) 255 | 256 | type User struct { 257 | Age int 258 | Some string 259 | UserName string `ini:"user_name"` 260 | Subs struct { 261 | Id string 262 | Tag string 263 | } 264 | } 265 | 266 | u1 := &User{} 267 | is.NoErr(ini.MapStruct("sec1", u1)) 268 | is.Eq(23, u1.Age) 269 | is.Eq("inhere", u1.UserName) 270 | ini.Reset() 271 | 272 | conf := ini.NewWithOptions(func(opt *ini.Options) { 273 | opt.DefSection = "" 274 | }) 275 | err = conf.LoadStrings(` 276 | age = 23 277 | some = value 278 | user_name = inhere 279 | [subs] 280 | id = 22 281 | tag = golang 282 | `) 283 | is.NoErr(err) 284 | 285 | u2 := &User{} 286 | is.NoErr(conf.Decode(u2)) 287 | is.Eq(23, u2.Age) 288 | is.Eq("inhere", u2.UserName) 289 | is.Eq("golang", u2.Subs.Tag) 290 | is.Err(conf.MapStruct("not-exist", u2)) 291 | 292 | // UserErr struct 293 | type UserErr struct { 294 | Age map[int]string `json:"age"` 295 | } 296 | 297 | ue := &UserErr{} 298 | err = conf.Decode(ue) 299 | is.Err(err) 300 | 301 | // invalid param 302 | is.Err(conf.MapTo(nil)) 303 | } 304 | -------------------------------------------------------------------------------- /parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/gookit/goutil/dump" 10 | "github.com/gookit/goutil/maputil" 11 | "github.com/gookit/goutil/strutil/textscan" 12 | "github.com/gookit/goutil/testutil/assert" 13 | ) 14 | 15 | var iniStr = ` 16 | # comments 1 17 | name = inhere 18 | age = 28 19 | debug = true 20 | hasQuota1 = 'this is val' 21 | hasQuota2 = "this is val1" 22 | shell = ${SHELL} 23 | noEnv = ${NotExist|defValue} 24 | 25 | ; array in default section 26 | tags[] = a 27 | tags[] = b 28 | tags[] = c 29 | 30 | ; comments 2 31 | [sec1] 32 | key = val0 33 | some = value 34 | stuff = things 35 | 36 | ; array in section sec1 37 | types[] = x 38 | types[] = y 39 | ` 40 | 41 | func ExampleNewFulled() { 42 | p, err := Parse(iniStr, ModeFull) 43 | // p, err := Parse(iniStr, ModeFull, NoDefSection) 44 | if err != nil { 45 | panic(err) 46 | } 47 | 48 | fmt.Printf("full parse:\n%#v\n", p.FullData()) 49 | } 50 | 51 | func ExampleNewSimpled() { 52 | // simple mode will ignore all array values 53 | p, err := Parse(iniStr, ModeSimple) 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | fmt.Printf("simple parse:\n%#v\n", p.SimpleData()) 59 | } 60 | 61 | func TestParse(t *testing.T) { 62 | is := assert.New(t) 63 | 64 | p, err := Parse("invalid", ModeFull) 65 | is.Err(err) 66 | is.True(len(p.FullData()) == 0) 67 | 68 | p, err = Parse("invalid", ModeSimple) 69 | is.Err(err) 70 | is.True(len(p.LiteData()) == 0) 71 | is.True(len(p.SimpleData()) == 0) 72 | 73 | is.True(IsCommentChar(';')) 74 | is.True(IsCommentChar('#')) 75 | is.False(IsCommentChar('a')) 76 | } 77 | 78 | func TestDecode(t *testing.T) { 79 | is := assert.New(t) 80 | bts := []byte(` 81 | age = 23 82 | name = inhere 83 | arr[] = a 84 | arr[] = b 85 | ; comments 86 | [sec] 87 | key = val 88 | ; comments 89 | [sec1] 90 | key = val 91 | number = 2020 92 | two_words = abc def 93 | `) 94 | 95 | data := make(map[string]any) 96 | err := Decode([]byte(""), data) 97 | is.Err(err) 98 | 99 | err = Decode(bts, nil) 100 | is.Err(err) 101 | 102 | err = Decode(bts, data) 103 | is.Err(err) 104 | 105 | err = Decode([]byte(`invalid`), &data) 106 | is.Err(err) 107 | 108 | err = Decode(bts, &data) 109 | dump.P(data) 110 | 111 | is.Nil(err) 112 | is.True(len(data) > 0) 113 | is.Eq("inhere", data["name"]) 114 | is.Eq("[a b]", fmt.Sprintf("%v", data["arr"])) 115 | is.Eq("map[key:val]", fmt.Sprintf("%v", data["sec"])) 116 | 117 | type myConf struct { 118 | Age int 119 | Name string 120 | Sec1 struct { 121 | Key string 122 | Number int 123 | TwoWords string `ini:"two_words"` 124 | } 125 | } 126 | 127 | st := &myConf{} 128 | is.NoErr(Decode(bts, st)) 129 | is.Eq(23, st.Age) 130 | is.Eq("inhere", st.Name) 131 | is.Eq(2020, st.Sec1.Number) 132 | is.Eq("abc def", st.Sec1.TwoWords) 133 | dump.P(st) 134 | 135 | // Unmarshal 136 | p := NewLite(func(opt *Options) { 137 | opt.NoDefSection = true 138 | }) 139 | 140 | st = &myConf{} 141 | is.NoErr(p.Unmarshal(bts, st)) 142 | is.Eq(23, st.Age) 143 | is.Eq("inhere", st.Name) 144 | is.Eq(2020, st.Sec1.Number) 145 | is.Eq("abc def", st.Sec1.TwoWords) 146 | } 147 | 148 | func TestNewSimpled(t *testing.T) { 149 | is := assert.New(t) 150 | 151 | // simple mode will ignore all array values 152 | p := NewSimpled() 153 | is.Eq(ModeLite, p.ParseMode) 154 | is.Eq(ModeLite.Unit8(), p.ParseMode.Unit8()) 155 | is.False(p.IgnoreCase) 156 | is.False(p.NoDefSection) 157 | 158 | err := p.ParseString("invalid string") 159 | is.Err(err) 160 | is.IsType(textscan.ErrScan{}, err) 161 | // is.Contains(err.Error(), "invalid syntax, no matcher available") 162 | is.Contains(err.Error(), `line 1: "invalid string"`) 163 | 164 | err = p.ParseString("") 165 | is.NoErr(err) 166 | is.True(len(p.SimpleData()) == 0) 167 | 168 | p.Reset() 169 | err = p.ParseString(iniStr) 170 | is.Nil(err) 171 | is.NotEmpty(p.Comments()) 172 | 173 | data := p.SimpleData() 174 | dump.P(data, p.Comments()) 175 | str := fmt.Sprintf("%v", data) 176 | is.Contains(str, "hasQuota2:") 177 | is.NotContains(str, "hasquota1:") 178 | 179 | defSec := p.LiteSection(p.DefSection) 180 | is.NotEmpty(defSec) 181 | 182 | // ignore case 183 | p = NewSimpled(IgnoreCase) 184 | err = p.ParseString(iniStr) 185 | is.Nil(err) 186 | 187 | v := p.ParsedData() 188 | is.NotEmpty(v) 189 | 190 | data = p.LiteData() 191 | str = fmt.Sprintf("%v", data) 192 | is.Contains(str, "hasquota2:") 193 | is.NotContains(str, "hasQuota1:") 194 | } 195 | 196 | func TestNewFulled(t *testing.T) { 197 | is := assert.New(t) 198 | 199 | p := NewFulled() 200 | is.Eq(ModeFull, p.ParseMode) 201 | is.False(p.IgnoreCase) 202 | is.False(p.NoDefSection) 203 | 204 | err := p.ParseString("invalid string") 205 | is.Err(err) 206 | 207 | err = p.ParseString(` 208 | [__default] 209 | newKey = new val 210 | [sec1] 211 | newKey = val5 212 | [newSec] 213 | key = val0 214 | `) 215 | is.Nil(err) 216 | dump.P(p.ParsedData()) 217 | 218 | p.Reset() 219 | err = p.ParseString(iniStr) 220 | is.Nil(err) 221 | 222 | v := p.ParsedData() 223 | dump.P(v, p.Comments()) 224 | is.NotEmpty(v) 225 | is.ContainsKey(v, "sec1") 226 | 227 | // options: ignore case 228 | p = NewFulled(IgnoreCase) 229 | is.True(p.IgnoreCase) 230 | err = p.ParseString(iniStr) 231 | is.Nil(err) 232 | 233 | v = p.ParsedData() 234 | is.NotEmpty(v) 235 | 236 | data := p.FullData() 237 | str := fmt.Sprintf("%v", data) 238 | is.Contains(str, "hasquota2:") 239 | is.NotContains(str, "hasQuota1:") 240 | } 241 | 242 | func TestParser_ParseBytes(t *testing.T) { 243 | p := NewLite() 244 | 245 | is := assert.New(t) 246 | err := p.ParseBytes(nil) 247 | 248 | is.NoErr(err) 249 | is.Len(p.LiteData(), 0) 250 | } 251 | 252 | func TestParser_ParseFrom(t *testing.T) { 253 | p := New() 254 | n, err := p.ParseFrom(bufio.NewScanner(strings.NewReader(""))) 255 | assert.Eq(t, int64(0), n) 256 | assert.NoErr(t, err) 257 | } 258 | 259 | func TestParser_ParseString(t *testing.T) { 260 | p := New(WithParseMode(ModeFull)) 261 | err := p.ParseString(` 262 | key1 = val1 263 | arr = val2 264 | arr[] = val3 265 | arr[] = val4 266 | `) 267 | 268 | assert.NoErr(t, err) 269 | assert.NotEmpty(t, p.FullData()) 270 | dump.P(p.ParsedData()) 271 | 272 | p.Reset() 273 | assert.NoErr(t, p.ParseString(` 274 | # no values 275 | `)) 276 | } 277 | 278 | func TestParser_multiLineValue(t *testing.T) { 279 | p := New(WithParseMode(ModeFull)) 280 | err := p.ParseString(` 281 | ; comments 1 282 | key1 = """multi line 283 | value for key1 284 | """ 285 | 286 | arr[] = val3 287 | ; comments 2 288 | arr[] = '''multi line 289 | value at array 290 | ''' 291 | `) 292 | 293 | assert.NoErr(t, err) 294 | data := p.FullData() 295 | assert.NotEmpty(t, data) 296 | defMp := data[DefSection].(map[string]any) 297 | dump.P(defMp) 298 | assert.Eq(t, "multi line\nvalue for key1\n", defMp["key1"]) 299 | assert.Eq(t, "multi line\nvalue at array\n", maputil.DeepGet(defMp, "arr.1")) 300 | } 301 | 302 | func TestParser_valueUrl(t *testing.T) { 303 | p := NewLite() 304 | err := p.ParseString(` 305 | url_ip=http://127.0.0.1 306 | url_ip_port=http://127.0.0.1:9090 307 | url_value=https://github.com 308 | url_value1=https://github.com/inhere 309 | `) 310 | assert.NoErr(t, err) 311 | data := p.LiteData() 312 | assert.NotEmpty(t, data) 313 | defMap := data[DefSection] 314 | assert.NotEmpty(t, defMap) 315 | dump.P(defMap) 316 | 317 | sMap := maputil.SMap(defMap) 318 | assert.Eq(t, "http://127.0.0.1", sMap.Str("url_ip")) 319 | assert.Eq(t, "http://127.0.0.1:9090", sMap.Str("url_ip_port")) 320 | assert.Eq(t, "https://github.com/inhere", sMap.Str("url_value1")) 321 | } 322 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # INI 2 | 3 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/gookit/ini?style=flat-square) 4 | [![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/gookit/ini)](https://github.com/gookit/ini) 5 | [![GoDoc](https://godoc.org/github.com/gookit/ini?status.svg)](https://pkg.go.dev/github.com/gookit/ini) 6 | [![Coverage Status](https://coveralls.io/repos/github/gookit/ini/badge.svg?branch=master)](https://coveralls.io/github/gookit/ini?branch=master) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/gookit/ini)](https://goreportcard.com/report/github.com/gookit/ini) 8 | [![Unit-Tests](https://github.com/gookit/ini/actions/workflows/go.yml/badge.svg)](https://github.com/gookit/ini) 9 | 10 | INI格式内容解析; 使用INI格式作为配置,配置数据的加载,管理,使用。 11 | 12 | > **[EN README](README.md)** 13 | 14 | ## 功能简介 15 | 16 | - 使用简单(获取: `Int` `Int64` `Bool` `String` `StringMap` ..., 设置: `Set` ) 17 | - 支持多文件,数据加载 18 | - 支持数据覆盖合并 19 | - 支持将数据绑定到结构体 20 | - 支持将数据重新生成 `INI` 内容(会还原注释,环境变量等) 21 | - 支持解析 `ENV` 变量名 22 | - 支持使用 `;` `#` 注释一行, 同时支持多行注释 `/* .. */` 23 | - 支持使用 `"""` or `'''` 编写多行值 24 | - 支持变量参考引用 25 | - 默认兼容 Python 的 configParser 格式 `%(VAR)s` 26 | - 完善的单元测试(coverage > 90%) 27 | 28 | ### [Parser](./parser) 29 | 30 | 子包 `parser` - 实现了解析 `INI` 格式内容为 Go 数据 31 | 32 | ### [Dotenv](./dotenv) 33 | 34 | 子包 `dotenv` - 提供了加载解析 `.env` 文件数据为ENV环境变量 35 | 36 | - filename support simple glob pattern. eg: `.env.*`, `*.env` 37 | 38 | ## 更多格式 39 | 40 | 如果你想要更多文件内容格式的支持,推荐使用 `gookit/config` 41 | 42 | - [gookit/config](https://github.com/gookit/config) - 支持多种格式: `JSON`(default), `INI`, `YAML`, `TOML`, `HCL` 43 | 44 | ## GoDoc 45 | 46 | - [doc on gowalker](https://gowalker.org/github.com/gookit/ini) 47 | - [godoc for github](https://pkg.go.dev/github.com/gookit/ini) 48 | 49 | ## 安装 50 | 51 | ```bash 52 | go get github.com/gookit/ini/v2 53 | ``` 54 | 55 | ## 快速使用 56 | 57 | - 示例数据(`testdata/test.ini`): 58 | 59 | ```ini 60 | # comments 61 | name = inhere 62 | age = 50 63 | debug = true 64 | hasQuota1 = 'this is val' 65 | hasQuota2 = "this is val1" 66 | can2arr = val0,val1,val2 67 | shell = ${SHELL} 68 | noEnv = ${NotExist|defValue} 69 | nkey = val in default section 70 | 71 | ; comments 72 | [sec1] 73 | key = val0 74 | some = value 75 | stuff = things 76 | varRef = %(nkey)s 77 | ``` 78 | 79 | ### 载入数据 80 | 81 | ```go 82 | package main 83 | 84 | import ( 85 | "github.com/gookit/ini/v2" 86 | ) 87 | 88 | // go run ./examples/demo.go 89 | func main() { 90 | // err := ini.LoadFiles("testdata/tesdt.ini") 91 | // LoadExists 将忽略不存在的文件 92 | err := ini.LoadExists("testdata/test.ini", "not-exist.ini") 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | // 加载更多,相同的键覆盖之前数据 98 | err = ini.LoadStrings(` 99 | age = 100 100 | [sec1] 101 | newK = newVal 102 | some = change val 103 | `) 104 | // fmt.Printf("%v\n", ini.Data()) 105 | } 106 | ``` 107 | 108 | ### 获取数据 109 | 110 | - 获取整型 111 | 112 | ```go 113 | age := ini.Int("age") 114 | fmt.Print(age) // 100 115 | ``` 116 | 117 | - 获取布尔值 118 | 119 | ```go 120 | val := ini.Bool("debug") 121 | fmt.Print(val) // true 122 | ``` 123 | 124 | - 获取字符串 125 | 126 | ```go 127 | name := ini.String("name") 128 | fmt.Print(name) // inhere 129 | ``` 130 | 131 | - 获取section数据(string map) 132 | 133 | ```go 134 | val := ini.StringMap("sec1") 135 | fmt.Println(val) 136 | // map[string]string{"key":"val0", "some":"change val", "stuff":"things", "newK":"newVal"} 137 | ``` 138 | 139 | - 获取的值是环境变量 140 | 141 | ```go 142 | value := ini.String("shell") 143 | fmt.Printf("%q", value) // "/bin/zsh" 144 | ``` 145 | 146 | - 通过key path来直接获取子级值 147 | 148 | ```go 149 | value := ini.String("sec1.key") 150 | fmt.Print(value) // val0 151 | ``` 152 | 153 | - 支持变量参考 154 | 155 | ```go 156 | value := ini.String("sec1.varRef") 157 | fmt.Printf("%q", value) // "val in default section" 158 | ``` 159 | 160 | - 设置新的值 161 | 162 | ```go 163 | // set value 164 | ini.Set("name", "new name") 165 | name = ini.String("name") 166 | fmt.Printf("%q", name) // "new name" 167 | ``` 168 | 169 | ### 将数据映射到结构 170 | 171 | ```go 172 | type User struct { 173 | Name string 174 | Age int 175 | } 176 | 177 | user := &User{} 178 | ini.MapStruct(ini.DefSection(), user) 179 | 180 | dump.P(user) 181 | ``` 182 | 183 | 特殊的,绑定所有数据: 184 | 185 | ```go 186 | ini.MapStruct("", ptr) 187 | ``` 188 | 189 | ## 变量参考解析 190 | 191 | ```ini 192 | [portal] 193 | url = http://%(host)s:%(port)s/Portal 194 | host = localhost 195 | port = 8080 196 | ``` 197 | 198 | 启用变量解析后,将会解析这里的 `%(host)s` 并替换为相应的变量值 `localhost`: 199 | 200 | ```go 201 | cfg := ini.New() 202 | // 启用变量解析 203 | cfg.WithOptions(ini.ParseVar) 204 | 205 | fmt.Print(cfg.String("portal.url")) 206 | // OUT: 207 | // http://localhost:8080/Portal 208 | ``` 209 | 210 | ## 可用选项 211 | 212 | ```go 213 | type Options struct { 214 | // 设置为只读模式. default False 215 | Readonly bool 216 | // 解析 ENV 变量名称. default True 217 | ParseEnv bool 218 | // 解析变量引用 "%(varName)s". default False 219 | ParseVar bool 220 | 221 | // 变量左侧字符. default "%(" 222 | VarOpen string 223 | // 变量右侧字符. default ")s" 224 | VarClose string 225 | 226 | // 忽略键名称大小写. default False 227 | IgnoreCase bool 228 | // 默认的section名称. default "__default" 229 | DefSection string 230 | // 路径分隔符,当通过key获取子级值时. default ".", 例如 "section.subKey" 231 | SectionSep string 232 | } 233 | ``` 234 | 235 | - 应用选项 236 | 237 | ```go 238 | cfg := ini.New() 239 | cfg.WithOptions(ini.ParseEnv,ini.ParseVar, func (opts *Options) { 240 | opts.SectionSep = ":" 241 | opts.DefSection = "default" 242 | }) 243 | ``` 244 | 245 | ## Dotenv 246 | 247 | Package `dotenv` that supports importing data from files (eg `.env`) to ENV 248 | 249 | ### 使用说明 250 | 251 | ```go 252 | err := dotenv.Load("./", ".env") 253 | // err := dotenv.LoadExists("./", ".env") 254 | 255 | val := dotenv.Get("ENV_KEY") 256 | // Or use 257 | // val := os.Getenv("ENV_KEY") 258 | 259 | // get int value 260 | intVal := dotenv.Int("LOG_LEVEL") 261 | 262 | // with default value 263 | val := dotenv.Get("ENV_KEY", "default value") 264 | ``` 265 | 266 | ## 测试 267 | 268 | - 测试并输出覆盖率 269 | 270 | ```bash 271 | go test ./... -cover 272 | ``` 273 | 274 | - 运行 GoLint 检查 275 | 276 | ```bash 277 | golint ./... 278 | ``` 279 | 280 | ## Gookit packages 281 | 282 | - [gookit/ini](https://github.com/gookit/ini) Go config management, use INI files 283 | - [gookit/rux](https://github.com/gookit/rux) Simple and fast request router for golang HTTP 284 | - [gookit/gcli](https://github.com/gookit/gcli) Build CLI application, tool library, running CLI commands 285 | - [gookit/slog](https://github.com/gookit/slog) Lightweight, easy to extend, configurable logging library written in Go 286 | - [gookit/color](https://github.com/gookit/color) A command-line color library with true color support, universal API methods and Windows support 287 | - [gookit/event](https://github.com/gookit/event) Lightweight event manager and dispatcher implements by Go 288 | - [gookit/cache](https://github.com/gookit/cache) Generic cache use and cache manager for golang. support File, Memory, Redis, Memcached. 289 | - [gookit/config](https://github.com/gookit/config) Go config management. support JSON, YAML, TOML, INI, HCL, ENV and Flags 290 | - [gookit/filter](https://github.com/gookit/filter) Provide filtering, sanitizing, and conversion of golang data 291 | - [gookit/validate](https://github.com/gookit/validate) Use for data validation and filtering. support Map, Struct, Form data 292 | - [gookit/goutil](https://github.com/gookit/goutil) Some utils for the Go: string, array/slice, map, format, cli, env, filesystem, test and more 293 | - More, please see https://github.com/gookit 294 | 295 | ## 相关项目参考 296 | 297 | - [go-ini/ini](https://github.com/go-ini/ini) ini parser and config manage 298 | - [dombenson/go-ini](https://github.com/dombenson/go-ini) ini parser and config manage 299 | 300 | ## License 301 | 302 | **MIT** 303 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # INI 2 | 3 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/gookit/ini?style=flat-square) 4 | [![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/gookit/ini)](https://github.com/gookit/ini) 5 | [![Coverage Status](https://coveralls.io/repos/github/gookit/ini/badge.svg?branch=master)](https://coveralls.io/github/gookit/ini?branch=master) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/gookit/ini)](https://goreportcard.com/report/github.com/gookit/ini) 7 | [![Unit-Tests](https://github.com/gookit/ini/actions/workflows/go.yml/badge.svg)](https://github.com/gookit/ini) 8 | [![Go Reference](https://pkg.go.dev/badge/github.com/gookit/ini/v2.svg)](https://pkg.go.dev/github.com/gookit/ini/v2) 9 | 10 | `INI` contents parser by Golang, `INI` config data management. 11 | 12 | > **[中文说明](README.zh-CN.md)** 13 | 14 | ## Features 15 | 16 | - Easy to use(get: `Int` `Int64` `Bool` `String` `StringMap` ..., set: `Set`) 17 | - Support multi file, data load 18 | - Support for decode data to struct 19 | - Support for encode data to INI content 20 | - Comments, environment variables, etc. are reverted 21 | - Support data override merge 22 | - Support parse ENV variable 23 | - Support comments start with `;` `#`, multi line comments `/* .. */` 24 | - Support multi line value with `"""` or `'''` 25 | - Complete unit test(coverage > 90%) 26 | - Support variable reference, default compatible with Python's configParser format `%(VAR)s` 27 | 28 | ### [Parser](./parser) 29 | 30 | Package `parser` is a Parser for parse INI format content to golang data 31 | 32 | ### [Dotenv](./dotenv) 33 | 34 | Package `dotenv` that supports importing ENV data from files (eg `.env`) 35 | 36 | - filename support simple glob pattern. eg: `.env.*`, `*.env` 37 | 38 | ## More formats 39 | 40 | If you want more support for file content formats, recommended use `gookit/config` 41 | 42 | - [gookit/config](https://github.com/gookit/config) - Support multi formats: `JSON`(default), `INI`, `YAML`, `TOML`, `HCL` 43 | 44 | ## GoDoc 45 | 46 | - [godoc](https://pkg.go.dev/github.com/gookit/ini) 47 | 48 | ## Install 49 | 50 | ```bash 51 | go get github.com/gookit/ini/v2 52 | ``` 53 | 54 | ## Usage 55 | 56 | - example data(`testdata/test.ini`): 57 | 58 | ```ini 59 | # comments 60 | name = inhere 61 | age = 50 62 | debug = true 63 | hasQuota1 = 'this is val' 64 | hasQuota2 = "this is val1" 65 | can2arr = val0,val1,val2 66 | shell = ${SHELL} 67 | noEnv = ${NotExist|defValue} 68 | nkey = val in default section 69 | 70 | ; comments 71 | [sec1] 72 | key = val0 73 | some = value 74 | stuff = things 75 | varRef = %(nkey)s 76 | ``` 77 | 78 | ### Load data 79 | 80 | ```go 81 | package main 82 | 83 | import ( 84 | "github.com/gookit/ini/v2" 85 | ) 86 | 87 | // go run ./examples/demo.go 88 | func main() { 89 | // config, err := ini.LoadFiles("testdata/tesdt.ini") 90 | // LoadExists will ignore not exists file 91 | err := ini.LoadExists("testdata/test.ini", "not-exist.ini") 92 | if err != nil { 93 | panic(err) 94 | } 95 | 96 | // load more, will override prev data by key 97 | err = ini.LoadStrings(` 98 | age = 100 99 | [sec1] 100 | newK = newVal 101 | some = change val 102 | `) 103 | // fmt.Printf("%v\n", config.Data()) 104 | } 105 | ``` 106 | 107 | ### Read data 108 | 109 | - Get integer 110 | 111 | ```go 112 | age := ini.Int("age") 113 | fmt.Print(age) // 100 114 | ``` 115 | 116 | - Get bool 117 | 118 | ```go 119 | val := ini.Bool("debug") 120 | fmt.Print(val) // true 121 | ``` 122 | 123 | - Get string 124 | 125 | ```go 126 | name := ini.String("name") 127 | fmt.Print(name) // inhere 128 | ``` 129 | 130 | - Get section data(string map) 131 | 132 | ```go 133 | val := ini.StringMap("sec1") 134 | fmt.Println(val) 135 | // map[string]string{"key":"val0", "some":"change val", "stuff":"things", "newK":"newVal"} 136 | ``` 137 | 138 | - Value is ENV var 139 | 140 | ```go 141 | value := ini.String("shell") 142 | fmt.Printf("%q", value) // "/bin/zsh" 143 | ``` 144 | 145 | - **Get value by key path** 146 | 147 | ```go 148 | value := ini.String("sec1.key") 149 | fmt.Print(value) // val0 150 | ``` 151 | 152 | - Use var refer 153 | 154 | ```go 155 | value := ini.String("sec1.varRef") 156 | fmt.Printf("%q", value) // "val in default section" 157 | ``` 158 | 159 | - Set new value 160 | 161 | ```go 162 | // set value 163 | ini.Set("name", "new name") 164 | name = ini.String("name") 165 | fmt.Printf("%q", name) // "new name" 166 | ``` 167 | 168 | ## Mapping data to struct 169 | 170 | ```go 171 | type User struct { 172 | Name string 173 | Age int 174 | } 175 | 176 | user := &User{} 177 | ini.MapStruct(ini.DefSection(), user) 178 | 179 | dump.P(user) 180 | ``` 181 | 182 | Special, mapping all data: 183 | 184 | ```go 185 | ini.MapStruct("", ptr) 186 | ``` 187 | 188 | ## Variable reference resolution 189 | 190 | ```ini 191 | [portal] 192 | url = http://%(host)s:%(port)s/api 193 | host = localhost 194 | port = 8080 195 | ``` 196 | 197 | If variable resolution is enabled,will parse `%(host)s` and replace it: 198 | 199 | ```go 200 | cfg := ini.New() 201 | // enable ParseVar 202 | cfg.WithOptions(ini.ParseVar) 203 | 204 | fmt.Print(cfg.MustString("portal.url")) 205 | // OUT: 206 | // http://localhost:8080/api 207 | ``` 208 | 209 | ## Available options 210 | 211 | ```go 212 | type Options struct { 213 | // set to read-only mode. default False 214 | Readonly bool 215 | // parse ENV var name. default True 216 | ParseEnv bool 217 | // parse variable reference "%(varName)s". default False 218 | ParseVar bool 219 | 220 | // var left open char. default "%(" 221 | VarOpen string 222 | // var right close char. default ")s" 223 | VarClose string 224 | 225 | // ignore key name case. default False 226 | IgnoreCase bool 227 | // default section name. default "__default" 228 | DefSection string 229 | // sep char for split key path. default ".", use like "section.subKey" 230 | SectionSep string 231 | } 232 | ``` 233 | 234 | Setting options for default instance: 235 | 236 | ```go 237 | ini.WithOptions(ini.ParseEnv,ini.ParseVar) 238 | ``` 239 | 240 | Setting options with new instance: 241 | 242 | ```go 243 | cfg := ini.New() 244 | cfg.WithOptions(ini.ParseEnv, ini.ParseVar, func (opts *Options) { 245 | opts.SectionSep = ":" 246 | opts.DefSection = "default" 247 | }) 248 | ``` 249 | 250 | ## Dotenv 251 | 252 | Package `dotenv` that supports importing data from files (eg `.env`) to ENV 253 | 254 | > NOTE: filename support simple glob pattern. eg: ".env.*", "*.env" 255 | 256 | ### Usage 257 | 258 | ```go 259 | err := dotenv.Load("./", ".env") 260 | // err := dotenv.LoadExists("./", ".env") 261 | 262 | val := dotenv.Get("ENV_KEY") 263 | // Or use 264 | // val := os.Getenv("ENV_KEY") 265 | 266 | // get int value 267 | intVal := dotenv.Int("LOG_LEVEL") 268 | 269 | // with default value 270 | val := dotenv.Get("ENV_KEY", "default value") 271 | ``` 272 | 273 | ## Tests 274 | 275 | - go tests with cover 276 | 277 | ```bash 278 | go test ./... -cover 279 | ``` 280 | 281 | - run lint by GoLint 282 | 283 | ```bash 284 | golint ./... 285 | ``` 286 | 287 | ## Gookit packages 288 | 289 | - [gookit/ini](https://github.com/gookit/ini) Go config management, use INI files 290 | - [gookit/rux](https://github.com/gookit/rux) Simple and fast request router for golang HTTP 291 | - [gookit/gcli](https://github.com/gookit/gcli) Build CLI application, tool library, running CLI commands 292 | - [gookit/slog](https://github.com/gookit/slog) Lightweight, easy to extend, configurable logging library written in Go 293 | - [gookit/color](https://github.com/gookit/color) A command-line color library with true color support, universal API methods and Windows support 294 | - [gookit/event](https://github.com/gookit/event) Lightweight event manager and dispatcher implements by Go 295 | - [gookit/cache](https://github.com/gookit/cache) Generic cache use and cache manager for golang. support File, Memory, Redis, Memcached. 296 | - [gookit/config](https://github.com/gookit/config) Go config management. support JSON, YAML, TOML, INI, HCL, ENV and Flags 297 | - [gookit/filter](https://github.com/gookit/filter) Provide filtering, sanitizing, and conversion of golang data 298 | - [gookit/validate](https://github.com/gookit/validate) Use for data validation and filtering. support Map, Struct, Form data 299 | - [gookit/goutil](https://github.com/gookit/goutil) Some utils for the Go: string, array/slice, map, format, cli, env, filesystem, test and more 300 | - More, please see https://github.com/gookit 301 | 302 | ## Related 303 | 304 | - [go-ini/ini](https://github.com/go-ini/ini) ini parser and config manage 305 | - [dombenson/go-ini](https://github.com/dombenson/go-ini) ini parser and config manage 306 | 307 | ## License 308 | 309 | **MIT** 310 | -------------------------------------------------------------------------------- /ini.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package ini is a ini config file/data manage implement 3 | 4 | Source code and other details for the project are available at GitHub: 5 | 6 | https://github.com/gookit/ini 7 | 8 | INI parser is: https://github.com/gookit/ini/parser 9 | */ 10 | package ini 11 | 12 | import ( 13 | "errors" 14 | "io" 15 | "os" 16 | "regexp" 17 | "strings" 18 | "sync" 19 | ) 20 | 21 | // some default constants 22 | const ( 23 | SepSection = "." 24 | DefTagName = "ini" 25 | ) 26 | 27 | var ( 28 | errEmptyKey = errors.New("ini: key name cannot be empty") 29 | errNotFound = errors.New("ini: key does not exist in the config") 30 | errReadonly = errors.New("ini: config manager instance in 'readonly' mode") 31 | // default instance 32 | dc = New() 33 | ) 34 | 35 | // Section in INI config 36 | type Section map[string]string 37 | 38 | // Ini config data manager 39 | type Ini struct { 40 | err error 41 | opts *Options 42 | lock sync.RWMutex 43 | data map[string]Section 44 | // regex for match user var. eg: %(VAR_NAME)s 45 | varRegex *regexp.Regexp 46 | // backup raw value line map, use for encode export. 47 | // - format: `section +"_"+ key` => rawValue 48 | // 49 | // will backup the value on contains var, ENV. 50 | rawBak map[string]string 51 | // comments map, key is `section +"_"+ key`. 52 | comments map[string]string 53 | } 54 | 55 | /************************************************************* 56 | * config instance 57 | *************************************************************/ 58 | 59 | // New a config instance, with default options 60 | func New() *Ini { 61 | return &Ini{ 62 | data: make(map[string]Section), 63 | opts: newDefaultOptions(), 64 | // val backup init 65 | rawBak: make(map[string]string, 6), 66 | } 67 | } 68 | 69 | // NewWithOptions new an instance and with some options 70 | // 71 | // Usage: 72 | // 73 | // ini.NewWithOptions(ini.ParseEnv, ini.Readonly) 74 | func NewWithOptions(opts ...func(*Options)) *Ini { 75 | c := New() 76 | // apply options 77 | c.WithOptions(opts...) 78 | return c 79 | } 80 | 81 | // Default config instance 82 | func Default() *Ini { return dc } 83 | 84 | // Reset all data for the default. 85 | // If you want reset instance, use `ini.ResetStd()` 86 | func Reset() { dc.Reset() } 87 | 88 | // ResetStd instance 89 | func ResetStd() { dc = New() } 90 | 91 | func (c *Ini) ensureInit() { 92 | if !c.IsEmpty() { 93 | return 94 | } 95 | 96 | if c.data == nil { 97 | c.data = make(map[string]Section) 98 | } 99 | 100 | if c.opts == nil { 101 | c.opts = newDefaultOptions() 102 | } 103 | 104 | // build var regex. default is `%\(([\w-:]+)\)s` 105 | if c.opts.ParseVar && c.varRegex == nil { 106 | // regexStr := `%\([\w-:]+\)s` 107 | l := regexp.QuoteMeta(c.opts.VarOpen) 108 | r := regexp.QuoteMeta(c.opts.VarClose) 109 | 110 | // build like: `%\(([\w-:]+)\)s` 111 | regStr := l + `([\w-` + c.opts.SectionSep + `]+)` + r 112 | c.varRegex = regexp.MustCompile(regStr) 113 | } 114 | } 115 | 116 | /************************************************************* 117 | * options func 118 | *************************************************************/ 119 | 120 | // GetOptions get options info. 121 | // 122 | // Notice: return is value. so, cannot change Ini instance 123 | func GetOptions() Options { 124 | return dc.Options() 125 | } 126 | 127 | // Options get options info. 128 | // 129 | // Notice: return is value. so, cannot change options 130 | func (c *Ini) Options() Options { 131 | return *c.opts 132 | } 133 | 134 | // WithOptions apply some options 135 | func WithOptions(opts ...func(*Options)) { 136 | dc.WithOptions(opts...) 137 | } 138 | 139 | // WithOptions apply some options 140 | func (c *Ini) WithOptions(opts ...func(*Options)) { 141 | if !c.IsEmpty() { 142 | panic("ini: cannot set options after data has been load") 143 | } 144 | 145 | // apply options 146 | for _, opt := range opts { 147 | opt(c.opts) 148 | } 149 | } 150 | 151 | // DefSection get default section name 152 | func DefSection() string { 153 | return dc.opts.DefSection 154 | } 155 | 156 | // DefSection get default section name 157 | func (c *Ini) DefSection() string { 158 | return c.opts.DefSection 159 | } 160 | 161 | /************************************************************* 162 | * data load 163 | *************************************************************/ 164 | 165 | // LoadFiles load data from files 166 | func LoadFiles(files ...string) error { return dc.LoadFiles(files...) } 167 | 168 | // LoadFiles load data from files 169 | func (c *Ini) LoadFiles(files ...string) (err error) { 170 | c.ensureInit() 171 | 172 | for _, file := range files { 173 | err = c.loadFile(file, false) 174 | if err != nil { 175 | return 176 | } 177 | } 178 | return 179 | } 180 | 181 | // LoadExists load files, will ignore not exists 182 | func LoadExists(files ...string) error { return dc.LoadExists(files...) } 183 | 184 | // LoadExists load files, will ignore not exists 185 | func (c *Ini) LoadExists(files ...string) (err error) { 186 | c.ensureInit() 187 | 188 | for _, file := range files { 189 | err = c.loadFile(file, true) 190 | if err != nil { 191 | return 192 | } 193 | } 194 | return 195 | } 196 | 197 | // LoadStrings load data from strings 198 | func LoadStrings(strings ...string) error { return dc.LoadStrings(strings...) } 199 | 200 | // LoadStrings load data from strings 201 | func (c *Ini) LoadStrings(strings ...string) (err error) { 202 | c.ensureInit() 203 | 204 | for _, str := range strings { 205 | err = c.parse(str) 206 | if err != nil { 207 | return 208 | } 209 | } 210 | return 211 | } 212 | 213 | // LoadData load data map 214 | func LoadData(data map[string]Section) error { return dc.LoadData(data) } 215 | 216 | // LoadData load data map 217 | func (c *Ini) LoadData(data map[string]Section) (err error) { 218 | c.ensureInit() 219 | 220 | if len(c.data) == 0 { 221 | c.data = data 222 | return 223 | } 224 | 225 | // append or override setting data 226 | for name, sec := range data { 227 | err = c.SetSection(name, sec) 228 | if err != nil { 229 | return 230 | } 231 | } 232 | return 233 | } 234 | 235 | func (c *Ini) loadFile(file string, loadExist bool) (err error) { 236 | // open file 237 | fd, err := os.Open(file) 238 | if err != nil { 239 | // skip not exist file 240 | if os.IsNotExist(err) && loadExist { 241 | return nil 242 | } 243 | 244 | return 245 | } 246 | //noinspection GoUnhandledErrorResult 247 | defer fd.Close() 248 | 249 | // read file content 250 | bts, err := io.ReadAll(fd) 251 | if err == nil { 252 | err = c.parse(string(bts)) 253 | if err != nil { 254 | return 255 | } 256 | } 257 | return 258 | } 259 | 260 | /************************************************************* 261 | * helper methods 262 | *************************************************************/ 263 | 264 | // HasKey check key exists 265 | func HasKey(key string) bool { return dc.HasKey(key) } 266 | 267 | // HasKey check key exists 268 | func (c *Ini) HasKey(key string) (ok bool) { 269 | _, ok = c.GetValue(key) 270 | return 271 | } 272 | 273 | // Delete value by key 274 | func Delete(key string) bool { return dc.Delete(key) } 275 | 276 | // Delete value by key 277 | func (c *Ini) Delete(key string) (ok bool) { 278 | if c.opts.Readonly { 279 | return 280 | } 281 | 282 | key = c.formatKey(key) 283 | if key == "" { 284 | return 285 | } 286 | 287 | sec, key := c.splitSectionAndKey(key) 288 | mp, ok := c.data[sec] 289 | if !ok { 290 | return 291 | } 292 | 293 | // key in a section 294 | if _, ok = mp[key]; ok { 295 | delete(mp, key) 296 | c.data[sec] = mp 297 | } 298 | return 299 | } 300 | 301 | // Reset all loaded data 302 | func (c *Ini) Reset() { 303 | c.data = make(map[string]Section) 304 | c.rawBak = make(map[string]string, 6) 305 | } 306 | 307 | // IsEmpty config data is empty 308 | func IsEmpty() bool { return len(dc.data) == 0 } 309 | 310 | // IsEmpty config data is empty 311 | func (c *Ini) IsEmpty() bool { 312 | return len(c.data) == 0 313 | } 314 | 315 | // Data get all data from default instance 316 | func Data() map[string]Section { return dc.data } 317 | 318 | // Data get all data 319 | func (c *Ini) Data() map[string]Section { 320 | return c.data 321 | } 322 | 323 | // Error get 324 | func Error() error { return dc.Error() } 325 | 326 | // Error get 327 | func (c *Ini) Error() error { 328 | return c.err 329 | } 330 | 331 | /************************************************************* 332 | * internal helper methods 333 | *************************************************************/ 334 | 335 | func (c *Ini) splitSectionAndKey(key string) (string, string) { 336 | sep := c.opts.SectionSep 337 | // default find from default Section 338 | name := c.opts.DefSection 339 | 340 | // get val by path. eg "log.dir" 341 | if strings.Contains(key, sep) { 342 | ss := strings.SplitN(key, sep, 2) 343 | name, key = strings.TrimSpace(ss[0]), strings.TrimSpace(ss[1]) 344 | } 345 | 346 | return name, key 347 | } 348 | 349 | // format key by some options 350 | func (c *Ini) formatKey(key string) string { 351 | sep := c.opts.SectionSep 352 | key = strings.Trim(strings.TrimSpace(key), sep) 353 | 354 | if c.opts.IgnoreCase { 355 | key = strings.ToLower(key) 356 | } 357 | 358 | return key 359 | } 360 | 361 | func mapKeyToLower(src map[string]string) map[string]string { 362 | newMp := make(map[string]string) 363 | 364 | for k, v := range src { 365 | k = strings.ToLower(k) 366 | newMp[k] = v 367 | } 368 | return newMp 369 | } 370 | -------------------------------------------------------------------------------- /ini_test.go: -------------------------------------------------------------------------------- 1 | package ini_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/gookit/goutil/testutil/assert" 9 | "github.com/gookit/ini/v2" 10 | "github.com/gookit/ini/v2/parser" 11 | ) 12 | 13 | func Example() { 14 | // config, err := LoadFiles("testdata/tesdt.ini") 15 | // LoadExists will ignore not exists file 16 | err := ini.LoadExists("testdata/test.ini", "not-exist.ini") 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | config := ini.Default() 22 | 23 | // load more, will override prev data by key 24 | _ = config.LoadStrings(` 25 | age = 100 26 | [sec1] 27 | newK = newVal 28 | some = change val 29 | `) 30 | // fmt.Printf("%v\n", config.Data()) 31 | 32 | iv := config.Int("age") 33 | fmt.Printf("get int\n - val: %v\n", iv) 34 | 35 | bv := config.Bool("debug") 36 | fmt.Printf("get bool\n - val: %v\n", bv) 37 | 38 | name := config.String("name") 39 | fmt.Printf("get string\n - val: %v\n", name) 40 | 41 | sec1 := config.StringMap("sec1") 42 | fmt.Printf("get section\n - val: %#v\n", sec1) 43 | 44 | str := config.String("sec1.key") 45 | fmt.Printf("get sub-value by path 'section.key'\n - val: %s\n", str) 46 | 47 | // can parse env name(ParseEnv: true) 48 | fmt.Printf("get env 'envKey' val: %s\n", config.String("shell")) 49 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("noEnv")) 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 | // export data to file 57 | // _, err = config.WriteToFile("testdata/export.ini") 58 | // if err != nil { 59 | // panic(err) 60 | // } 61 | 62 | // Out: 63 | // get int 64 | // - val: 100 65 | // get bool 66 | // - val: true 67 | // get string 68 | // - val: inhere 69 | // get section 70 | // - val: map[string]string{"stuff":"things", "newK":"newVal", "key":"val0", "some":"change val"} 71 | // get sub-value by path 'section.key' 72 | // - val: val0 73 | // get env 'envKey' val: /bin/zsh 74 | // get env 'envKey1' val: defValue 75 | // set string 76 | // - val: new name 77 | } 78 | 79 | var iniStr = `# comments in first line 80 | name = inhere 81 | age = 28 82 | debug = true 83 | # comments for themes 84 | themes = a,b,c 85 | hasQuota1 = 'this is val' 86 | hasQuota2 = "this is val1" # comments for hq2 87 | shell = ${SHELL} 88 | noEnv = ${NotExist|defValue} 89 | 90 | ; comments for sec1 91 | [sec1] 92 | age = 23 93 | ; comments for key 94 | key = val0 95 | some = value 96 | stuff = things 97 | user_name = inhere 98 | ` 99 | 100 | func TestLoad(t *testing.T) { 101 | is := assert.New(t) 102 | 103 | err := ini.LoadFiles("testdata/test.ini") 104 | is.Nil(err) 105 | is.False(ini.IsEmpty()) 106 | is.NotEmpty(ini.Data()) 107 | 108 | err = ini.LoadFiles("no-file.ini") 109 | is.Err(err) 110 | 111 | err = ini.LoadExists("testdata/test.ini", "no-file.ini") 112 | is.Nil(err) 113 | is.NotEmpty(ini.Data()) 114 | 115 | err = ini.LoadStrings("name = inhere") 116 | is.Nil(err) 117 | is.NotEmpty(ini.Data()) 118 | is.False(ini.IsEmpty()) 119 | 120 | // reset 121 | ini.Reset() 122 | 123 | err = ini.LoadStrings(" ") 124 | is.Nil(err) 125 | is.Empty(ini.Data()) 126 | 127 | // load data 128 | err = ini.LoadData(map[string]ini.Section{ 129 | "sec0": {"k": "v"}, 130 | }) 131 | is.Nil(err) 132 | is.True(ini.HasKey("sec0.k")) 133 | 134 | // test auto init and load data 135 | conf := new(ini.Ini) 136 | err = conf.LoadData(map[string]ini.Section{ 137 | "sec0": {"k": "v"}, 138 | }) 139 | is.Nil(err) 140 | err = conf.LoadData(map[string]ini.Section{ 141 | "name": {"k": "v"}, 142 | }) 143 | is.Nil(err) 144 | 145 | // test error 146 | err = conf.LoadFiles("testdata/error.ini") 147 | is.Err(err) 148 | 149 | err = conf.LoadExists("testdata/error.ini") 150 | is.Err(err) 151 | 152 | err = conf.LoadStrings("invalid string") 153 | is.Err(err) 154 | 155 | // reset 156 | ini.Reset() 157 | } 158 | 159 | func TestBasic(t *testing.T) { 160 | is := assert.New(t) 161 | defer ini.ResetStd() 162 | 163 | conf := ini.Default() 164 | is.Eq(parser.DefSection, conf.DefSection()) 165 | is.Eq(parser.DefSection, ini.DefSection()) 166 | 167 | conf.WithOptions(func(opts *ini.Options) { 168 | opts.DefSection = "myDef" 169 | }) 170 | is.Eq("myDef", conf.DefSection()) 171 | 172 | err := conf.LoadStrings(iniStr) 173 | is.Nil(err) 174 | 175 | is.True(conf.HasKey("name")) 176 | is.False(conf.HasKey("notExist")) 177 | 178 | is.True(conf.HasSection("sec1")) 179 | is.False(conf.HasSection("notExist")) 180 | 181 | is.Panics(func() { 182 | ini.WithOptions(ini.IgnoreCase) 183 | }) 184 | } 185 | 186 | func TestIgnoreCase(t *testing.T) { 187 | is := assert.New(t) 188 | conf := ini.NewWithOptions(ini.IgnoreCase) 189 | 190 | err := conf.LoadStrings(` 191 | kEy = val 192 | [sEc] 193 | sK = val 194 | `) 195 | is.Nil(err) 196 | 197 | opts := conf.Options() 198 | is.True(opts.IgnoreCase) 199 | 200 | str := conf.String("KEY") 201 | is.Eq("val", str) 202 | 203 | str = conf.String("key") 204 | is.Eq("val", str) 205 | 206 | is.True(conf.Delete("key")) 207 | is.False(conf.HasKey("kEy")) 208 | 209 | _ = conf.Set("NK", "val1") 210 | str = conf.String("nk") 211 | is.Eq("val1", str) 212 | 213 | str = conf.String("Nk") 214 | is.Eq("val1", str) 215 | 216 | sec := conf.StringMap("sec") 217 | is.Eq("val", sec["sk"]) 218 | 219 | err = conf.NewSection("NewSec", map[string]string{"kEy0": "val"}) 220 | is.Nil(err) 221 | 222 | sec = conf.StringMap("newSec") 223 | is.Eq("val", sec["key0"]) 224 | 225 | _ = conf.SetSection("NewSec", map[string]string{"key1": "val0"}) 226 | str = conf.String("newSec.key1") 227 | is.Eq("val0", str) 228 | 229 | _ = conf.SetSection("newSec1", map[string]string{"k0": "v0"}) 230 | is.True(conf.HasSection("newSec1")) 231 | is.True(conf.HasSection("newsec1")) 232 | is.True(conf.DelSection("newsec1")) 233 | } 234 | 235 | func TestReadonly(t *testing.T) { 236 | is := assert.New(t) 237 | conf := ini.NewWithOptions(ini.Readonly) 238 | 239 | err := conf.LoadStrings(` 240 | key = val 241 | [sec] 242 | k = v 243 | `) 244 | is.Nil(err) 245 | 246 | opts := conf.Options() 247 | is.True(opts.Readonly) 248 | 249 | err = conf.Set("newK", "newV") 250 | is.Err(err) 251 | 252 | err = conf.LoadData(map[string]ini.Section{ 253 | "sec1": {"k": "v"}, 254 | }) 255 | is.Err(err) 256 | 257 | ok := conf.Delete("key") 258 | is.False(ok) 259 | 260 | ok = conf.DelSection("sec") 261 | is.False(ok) 262 | 263 | err = conf.SetSection("newSec1", map[string]string{"k0": "v0"}) 264 | is.Err(err) 265 | is.False(conf.HasSection("newSec1")) 266 | 267 | err = conf.NewSection("NewSec", map[string]string{"kEy0": "val"}) 268 | is.Err(err) 269 | 270 | // Readonly and ParseVar 271 | conf = ini.NewWithOptions(ini.Readonly, ini.ParseVar) 272 | err = conf.LoadStrings(` 273 | key = val 274 | [sec] 275 | k = v 276 | k1 = %(key)s 277 | `) 278 | is.Nil(err) 279 | 280 | opts = conf.Options() 281 | is.True(opts.ParseVar) 282 | 283 | str := conf.Get("sec.k1") 284 | is.Eq("val", str) 285 | } 286 | 287 | func TestParseEnv(t *testing.T) { 288 | st := assert.New(t) 289 | conf := ini.NewWithOptions(ini.ParseEnv) 290 | 291 | err := conf.LoadStrings(` 292 | key = ${PATH} 293 | invalid = ${invalid 294 | notExist = ${NotExist} 295 | hasDefault = ${HasDef|defValue} 296 | `) 297 | st.Nil(err) 298 | 299 | opts := conf.Options() 300 | st.True(opts.ParseEnv) 301 | st.False(opts.ParseVar) 302 | 303 | str := conf.Get("key") 304 | st.NotContains(str, "${") 305 | 306 | str = conf.Get("notExist") 307 | st.Eq("", str) 308 | 309 | str = conf.Get("invalid") 310 | st.Contains(str, "${") 311 | st.Eq("${invalid", str) 312 | 313 | str = conf.Get("hasDefault") 314 | st.NotContains(str, "${") 315 | st.Eq("defValue", str) 316 | } 317 | 318 | func TestParseVar(t *testing.T) { 319 | is := assert.New(t) 320 | conf := ini.NewWithOptions(ini.ParseVar) 321 | err := conf.LoadStrings(` 322 | key = val 323 | ref = %(sec.host)s 324 | invalid = %(secs 325 | notExist = %(varNotExist)s 326 | debug = true 327 | [sec] 328 | enable = %(debug)s 329 | url = http://%(host)s/api 330 | host = localhost 331 | `) 332 | is.Nil(err) 333 | 334 | opts := conf.Options() 335 | is.False(opts.IgnoreCase) 336 | is.True(opts.ParseVar) 337 | // fmt.Println(conf.Data()) 338 | 339 | str := conf.Get("invalid") 340 | is.Eq("%(secs", str) 341 | 342 | str = conf.Get("notExist") 343 | is.Eq("%(varNotExist)s", str) 344 | 345 | str = conf.Get("sec.host") 346 | is.Eq("localhost", str) 347 | 348 | str = conf.Get("ref") 349 | is.Eq("localhost", str) 350 | 351 | str = conf.Get("sec.enable") 352 | is.Eq("true", str) 353 | 354 | str = conf.Get("sec.url") 355 | is.Eq("http://localhost/api", str) 356 | 357 | mp := conf.StringMap("sec") 358 | is.Eq("true", mp["enable"]) 359 | } 360 | 361 | func TestParseCustomRef(t *testing.T) { 362 | is := assert.New(t) 363 | conf := ini.NewWithOptions(func(opts *ini.Options) { 364 | *opts = ini.Options{ 365 | IgnoreCase: false, 366 | ParseEnv: false, 367 | ParseVar: true, 368 | SectionSep: "|", 369 | VarOpen: "${", 370 | VarClose: "}", 371 | Readonly: true, 372 | } 373 | }) 374 | err := conf.LoadStrings(` 375 | key = val 376 | ref = ${sec|host} 377 | invalid = ${secs 378 | notExist = ${varNotExist} 379 | debug = true 380 | [sec] 381 | enable = ${debug} 382 | url = http://${host}/api 383 | host = localhost 384 | `) 385 | is.Nil(err) 386 | 387 | opts := conf.Options() 388 | is.False(opts.IgnoreCase) 389 | is.False(opts.ParseEnv) 390 | is.True(opts.ParseVar) 391 | is.Eq("|", opts.SectionSep) 392 | is.Eq("${", opts.VarOpen) 393 | is.Eq("}", opts.VarClose) 394 | 395 | str := conf.Get("invalid") 396 | is.Eq("${secs", str) 397 | 398 | str = conf.Get("notExist") 399 | is.Eq("${varNotExist}", str) 400 | 401 | str = conf.Get("sec|host") 402 | is.Eq("localhost", str) 403 | 404 | str = conf.Get("ref") 405 | is.Eq("localhost", str) 406 | 407 | str = conf.Get("sec|enable") 408 | is.Eq("true", str) 409 | 410 | str = conf.Get("sec|url") 411 | is.Eq("http://localhost/api", str) 412 | 413 | mp := conf.StringMap("sec") 414 | is.Eq("true", mp["enable"]) 415 | 416 | } 417 | 418 | func TestIni_WriteTo(t *testing.T) { 419 | is := assert.New(t) 420 | 421 | err := ini.LoadStrings(iniStr) 422 | is.Nil(err) 423 | 424 | ns := ini.SectionKeys(false) 425 | is.Contains(ns, "sec1") 426 | is.NotContains(ns, ini.GetOptions().DefSection) 427 | 428 | ns = ini.SectionKeys(true) 429 | is.Contains(ns, "sec1") 430 | is.Contains(ns, ini.GetOptions().DefSection) 431 | 432 | conf := ini.Default() 433 | 434 | // export as INI string 435 | buf := &bytes.Buffer{} 436 | _, err = conf.WriteTo(buf) 437 | is.Nil(err) 438 | 439 | str := buf.String() 440 | is.Contains(str, "inhere") 441 | is.Contains(str, "[sec1]") 442 | 443 | // export as formatted JSON string 444 | str = conf.PrettyJSON() 445 | is.Contains(str, "inhere") 446 | is.Contains(str, "sec1") 447 | 448 | // export to file 449 | _, err = conf.WriteToFile("not/exist/export.ini") 450 | is.Err(err) 451 | 452 | n, err := conf.WriteToFile("testdata/export.ini") 453 | is.True(n > 0) 454 | is.Nil(err) 455 | 456 | conf.Reset() 457 | is.Empty(conf.Data()) 458 | 459 | conf = ini.New() 460 | str = conf.PrettyJSON() 461 | is.Eq("", str) 462 | } 463 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package parser is a Parser for parse INI format content to golang data 3 | 4 | There are example data: 5 | 6 | # comments 7 | name = inhere 8 | age = 28 9 | debug = true 10 | hasQuota1 = 'this is val' 11 | hasQuota2 = "this is val1" 12 | shell = ${SHELL} 13 | noEnv = ${NotExist|defValue} 14 | 15 | ; array in def section 16 | tags[] = a 17 | tags[] = b 18 | tags[] = c 19 | 20 | ; comments 21 | [sec1] 22 | key = val0 23 | some = value 24 | stuff = things 25 | ; array in section 26 | types[] = x 27 | types[] = y 28 | 29 | how to use, please see examples: 30 | */ 31 | package parser 32 | 33 | import ( 34 | "bufio" 35 | "bytes" 36 | "fmt" 37 | "io" 38 | "reflect" 39 | "regexp" 40 | "strings" 41 | 42 | "github.com/gookit/goutil/strutil/textscan" 43 | "github.com/gookit/ini/v2/internal" 44 | ) 45 | 46 | // match: [section] 47 | var sectionRegex = regexp.MustCompile(`^\[(.*)]$`) 48 | var commentChars = []byte{'#', ';'} 49 | 50 | // TokSection for mark a section 51 | const TokSection = textscan.TokComments + 1 + iota 52 | 53 | // IsCommentChar check is comment char 54 | func IsCommentChar(ch byte) bool { 55 | for _, v := range commentChars { 56 | if ch == v { 57 | return true 58 | } 59 | } 60 | return false 61 | } 62 | 63 | // SectionMatcher match section line: [section] 64 | type SectionMatcher struct{} 65 | 66 | // Match section line: [section] 67 | func (m *SectionMatcher) Match(text string, _ textscan.Token) (textscan.Token, error) { 68 | line := strings.TrimSpace(text) 69 | 70 | if matched := sectionRegex.FindStringSubmatch(line); matched != nil { 71 | section := strings.TrimSpace(matched[1]) 72 | tok := textscan.NewStringToken(TokSection, section) 73 | return tok, nil 74 | } 75 | 76 | return nil, nil 77 | } 78 | 79 | // Parser definition for parse INI content. 80 | type Parser struct { 81 | *Options 82 | // parsed bool 83 | 84 | // comments map, key is name 85 | comments map[string]string 86 | 87 | // for full parse(allow array, map section) 88 | fullData map[string]any 89 | // for simple parse(section only allow map[string]string) 90 | liteData map[string]map[string]string 91 | } 92 | 93 | // New a lite mode Parser with some options 94 | func New(fns ...OptFunc) *Parser { 95 | return &Parser{Options: NewOptions(fns...)} 96 | } 97 | 98 | // NewLite create a lite mode Parser. alias of New() 99 | func NewLite(fns ...OptFunc) *Parser { return New(fns...) } 100 | 101 | // NewSimpled create a lite mode Parser 102 | func NewSimpled(fns ...func(*Parser)) *Parser { 103 | return New().WithOptions(fns...) 104 | } 105 | 106 | // NewFulled create a full mode Parser with some options 107 | func NewFulled(fns ...func(*Parser)) *Parser { 108 | return New(WithParseMode(ModeFull)).WithOptions(fns...) 109 | } 110 | 111 | // Parse a INI data string to golang 112 | func Parse(data string, mode parseMode, opts ...func(*Parser)) (p *Parser, err error) { 113 | p = New(WithParseMode(mode)).WithOptions(opts...) 114 | err = p.ParseString(data) 115 | return 116 | } 117 | 118 | // Decode INI content to golang data 119 | func Decode(blob []byte, ptr any) error { 120 | rv := reflect.ValueOf(ptr) 121 | if rv.Kind() != reflect.Ptr { 122 | return fmt.Errorf("ini: Decode of non-pointer %s", reflect.TypeOf(ptr)) 123 | } 124 | 125 | p, err := Parse(string(blob), ModeFull, NoDefSection) 126 | if err != nil { 127 | return err 128 | } 129 | 130 | return p.MapStruct(ptr) 131 | } 132 | 133 | // NoDefSection set don't return DefSection title 134 | // 135 | // Usage: 136 | // 137 | // Parser.NoDefSection() 138 | func NoDefSection(p *Parser) { p.NoDefSection = true } 139 | 140 | // IgnoreCase set ignore-case 141 | func IgnoreCase(p *Parser) { p.IgnoreCase = true } 142 | 143 | // WithOptions apply some options 144 | func (p *Parser) WithOptions(opts ...func(p *Parser)) *Parser { 145 | for _, opt := range opts { 146 | opt(p) 147 | } 148 | return p 149 | } 150 | 151 | // Unmarshal parse ini text and decode to struct 152 | func (p *Parser) Unmarshal(v []byte, ptr any) error { 153 | if err := p.ParseBytes(v); err != nil { 154 | return err 155 | } 156 | return p.MapStruct(ptr) 157 | } 158 | 159 | /************************************************************* 160 | * do parsing 161 | *************************************************************/ 162 | 163 | // ParseString parse from string data 164 | func (p *Parser) ParseString(str string) error { 165 | if str = strings.TrimSpace(str); str == "" { 166 | return nil 167 | } 168 | return p.ParseReader(strings.NewReader(str)) 169 | } 170 | 171 | // ParseBytes parse from bytes data 172 | func (p *Parser) ParseBytes(bts []byte) (err error) { 173 | if len(bts) == 0 { 174 | return nil 175 | } 176 | return p.ParseReader(bytes.NewBuffer(bts)) 177 | } 178 | 179 | // ParseReader parse from io reader 180 | func (p *Parser) ParseReader(r io.Reader) (err error) { 181 | _, err = p.ParseFrom(bufio.NewScanner(r)) 182 | return 183 | } 184 | 185 | // init parser 186 | func (p *Parser) init() { 187 | // if p.IgnoreCase { 188 | // p.DefSection = strings.ToLower(p.DefSection) 189 | // } 190 | p.comments = make(map[string]string) 191 | 192 | if p.ParseMode == ModeFull { 193 | p.fullData = make(map[string]any) 194 | 195 | if p.Collector == nil { 196 | p.Collector = p.collectFullValue 197 | } 198 | } else { 199 | p.liteData = make(map[string]map[string]string) 200 | 201 | if p.Collector == nil { 202 | p.Collector = p.collectLiteValue 203 | } 204 | } 205 | } 206 | 207 | // ParseFrom a data scanner 208 | func (p *Parser) ParseFrom(in *bufio.Scanner) (count int64, err error) { 209 | p.init() 210 | count = -1 211 | 212 | // create scanner 213 | ts := textscan.NewScanner(in) 214 | ts.AddKind(TokSection, "Section") 215 | ts.AddMatchers( 216 | &textscan.CommentsMatcher{ 217 | InlineChars: commentChars, 218 | }, 219 | &SectionMatcher{}, 220 | &textscan.KeyValueMatcher{ 221 | MergeComments: true, 222 | InlineComment: p.InlineComment, 223 | }, 224 | ) 225 | 226 | section := p.DefSection 227 | 228 | // scan and parsing 229 | for ts.Scan() { 230 | tok := ts.Token() 231 | 232 | // comments has been merged to value token 233 | if !tok.IsValid() || tok.Kind() == textscan.TokComments { 234 | continue 235 | } 236 | 237 | if tok.Kind() == TokSection { 238 | section = tok.Value() 239 | 240 | // collect comments 241 | if textscan.IsKindToken(textscan.TokComments, ts.PrevToken()) { 242 | p.comments["_sec_"+section] = ts.PrevToken().Value() 243 | } 244 | continue 245 | } 246 | 247 | // collect value 248 | if tok.Kind() == textscan.TokValue { 249 | vt := tok.(*textscan.ValueToken) 250 | 251 | var isSli bool 252 | key := vt.Key() 253 | 254 | // is array index 255 | if strings.HasSuffix(key, "[]") { 256 | // skip parse array on lite mode 257 | if p.ParseMode == ModeLite { 258 | continue 259 | } 260 | 261 | key = key[:len(key)-2] 262 | isSli = true 263 | } 264 | 265 | p.collectValue(section, key, vt.Value(), isSli) 266 | if vt.HasComment() { 267 | p.comments[section+"_"+key] = vt.Comment() 268 | } 269 | } 270 | } 271 | 272 | count = 0 273 | err = ts.Err() 274 | return 275 | } 276 | 277 | func (p *Parser) collectValue(section, key, val string, isSlice bool) { 278 | if p.IgnoreCase { 279 | key = strings.ToLower(key) 280 | section = strings.ToLower(section) 281 | } 282 | 283 | if p.ReplaceNl { 284 | val = strings.ReplaceAll(val, `\n`, "\n") 285 | } 286 | 287 | p.Collector(section, key, val, isSlice) 288 | } 289 | 290 | func (p *Parser) collectFullValue(section, key, val string, isSlice bool) { 291 | defSec := p.DefSection 292 | // p.NoDefSection and current section is default section 293 | if p.NoDefSection && section == defSec { 294 | if isSlice { 295 | curVal, ok := p.fullData[key] 296 | if ok { 297 | switch cd := curVal.(type) { 298 | case []string: 299 | p.fullData[key] = append(cd, val) 300 | } 301 | } else { 302 | p.fullData[key] = []string{val} 303 | } 304 | } else { 305 | p.fullData[key] = val 306 | } 307 | return 308 | } 309 | 310 | secData, exists := p.fullData[section] 311 | // first create 312 | if !exists { 313 | if isSlice { 314 | p.fullData[section] = map[string]any{key: []string{val}} 315 | } else { 316 | p.fullData[section] = map[string]any{key: val} 317 | } 318 | return 319 | } 320 | 321 | switch sd := secData.(type) { 322 | case map[string]any: // existed section 323 | if curVal, ok := sd[key]; ok { 324 | switch cv := curVal.(type) { 325 | case string: 326 | if isSlice { 327 | sd[key] = []string{cv, val} 328 | } else { 329 | sd[key] = val 330 | } 331 | case []string: 332 | sd[key] = append(cv, val) 333 | default: 334 | return 335 | } 336 | } else { 337 | if isSlice { 338 | sd[key] = []string{val} 339 | } else { 340 | sd[key] = val 341 | } 342 | } 343 | p.fullData[section] = sd 344 | case string: // found default section value 345 | if isSlice { 346 | p.fullData[section] = map[string]any{key: []string{val}} 347 | } else { 348 | p.fullData[section] = map[string]any{key: val} 349 | } 350 | } 351 | } 352 | 353 | func (p *Parser) collectLiteValue(sec, key, val string, _ bool) { 354 | if p.IgnoreCase { 355 | key = strings.ToLower(key) 356 | sec = strings.ToLower(sec) 357 | } 358 | 359 | if strMap, ok := p.liteData[sec]; ok { 360 | strMap[key] = val 361 | p.liteData[sec] = strMap 362 | } else { 363 | // create the section if it does not exist 364 | p.liteData[sec] = map[string]string{key: val} 365 | } 366 | } 367 | 368 | /************************************************************* 369 | * export data 370 | *************************************************************/ 371 | 372 | // Decode the parsed data to struct ptr 373 | func (p *Parser) Decode(ptr any) error { return p.MapStruct(ptr) } 374 | 375 | // MapStruct mapping the parsed data to struct ptr 376 | func (p *Parser) MapStruct(ptr any) (err error) { 377 | // mapping for full mode data 378 | if p.ParseMode == ModeFull { 379 | if p.NoDefSection { 380 | return internal.MapStruct(p.TagName, p.fullData, ptr) 381 | } 382 | return internal.FullToStruct(p.TagName, p.DefSection, p.fullData, ptr) 383 | } 384 | 385 | // mapping for lite mode data 386 | return internal.LiteToStruct(p.TagName, p.DefSection, p.liteData, ptr) 387 | } 388 | 389 | /************************************************************* 390 | * helper methods 391 | *************************************************************/ 392 | 393 | // Comments get all comments 394 | func (p *Parser) Comments() map[string]string { return p.comments } 395 | 396 | // ParsedData get parsed data 397 | func (p *Parser) ParsedData() any { 398 | if p.ParseMode == ModeFull { 399 | return p.fullData 400 | } 401 | return p.liteData 402 | } 403 | 404 | // FullData get parsed data by full parse 405 | func (p *Parser) FullData() map[string]any { 406 | return p.fullData 407 | } 408 | 409 | // LiteData get parsed data by simple parse 410 | func (p *Parser) LiteData() map[string]map[string]string { 411 | return p.liteData 412 | } 413 | 414 | // SimpleData get parsed data by simple parse 415 | func (p *Parser) SimpleData() map[string]map[string]string { 416 | return p.liteData 417 | } 418 | 419 | // LiteSection get parsed data by simple parse 420 | func (p *Parser) LiteSection(name string) map[string]string { 421 | return p.liteData[name] 422 | } 423 | 424 | // Reset parser, clear parsed data 425 | func (p *Parser) Reset() { 426 | // p.parsed = false 427 | p.comments = make(map[string]string) 428 | if p.ParseMode == ModeFull { 429 | p.fullData = make(map[string]any) 430 | } else { 431 | p.liteData = make(map[string]map[string]string) 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /manage.go: -------------------------------------------------------------------------------- 1 | package ini 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "os" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/gookit/goutil/maputil" 11 | "github.com/gookit/goutil/strutil" 12 | "github.com/gookit/ini/v2/internal" 13 | "github.com/gookit/ini/v2/parser" 14 | ) 15 | 16 | /************************************************************* 17 | * read config value 18 | *************************************************************/ 19 | 20 | // GetValue get a value by key string. 21 | // you can use '.' split for get value in a special section 22 | func GetValue(key string) (string, bool) { return dc.GetValue(key) } 23 | 24 | // GetValue a value by key string. 25 | // 26 | // you can use '.' split for get value in a special section 27 | func (c *Ini) GetValue(key string) (val string, ok bool) { 28 | if !c.opts.Readonly { 29 | c.lock.Lock() 30 | defer c.lock.Unlock() 31 | } 32 | 33 | if key = c.formatKey(key); key == "" { 34 | return 35 | } 36 | 37 | // get section data 38 | name, key := c.splitSectionAndKey(key) 39 | strMap, ok := c.data[name] 40 | if !ok { 41 | return 42 | } 43 | 44 | val, ok = strMap[key] 45 | 46 | // if enable parse var refer 47 | if c.opts.ParseVar { 48 | val = c.parseVarReference(key, val, strMap) 49 | } 50 | 51 | // if opts.ParseEnv is true. will parse like: "${SHELL}" 52 | // if c.opts.ParseEnv { 53 | // val = envutil.ParseEnvValue(val) 54 | // } 55 | return 56 | } 57 | 58 | func (c *Ini) getValue(key string) (val string, ok bool) { 59 | if key = c.formatKey(key); key == "" { 60 | return 61 | } 62 | 63 | // get section data 64 | name, key := c.splitSectionAndKey(key) 65 | 66 | // get value 67 | if strMap, has := c.data[name]; has { 68 | val, ok = strMap[key] 69 | } 70 | return 71 | } 72 | 73 | // Get a value by key string. 74 | // you can use '.' split for get value in a special section 75 | func Get(key string, defVal ...string) string { return dc.Get(key, defVal...) } 76 | 77 | // Get a value by key string. 78 | // 79 | // you can use '.' split for get value in a special section 80 | func (c *Ini) Get(key string, defVal ...string) string { 81 | value, ok := c.GetValue(key) 82 | 83 | if !ok && len(defVal) > 0 { 84 | value = defVal[0] 85 | } 86 | return value 87 | } 88 | 89 | // String get a string by key 90 | func String(key string, defVal ...string) string { return dc.String(key, defVal...) } 91 | 92 | // String like Get method 93 | func (c *Ini) String(key string, defVal ...string) string { 94 | return c.Get(key, defVal...) 95 | } 96 | 97 | // Int get an int by key 98 | func Int(key string, defVal ...int) int { return dc.Int(key, defVal...) } 99 | 100 | // Int get a int value, if not found return default value 101 | func (c *Ini) Int(key string, defVal ...int) (value int) { 102 | i64, exist := c.tryInt64(key) 103 | 104 | if exist { 105 | value = int(i64) 106 | } else if len(defVal) > 0 { 107 | value = defVal[0] 108 | } 109 | return 110 | } 111 | 112 | // Uint get an uint value, if not found return default value 113 | func Uint(key string, defVal ...uint) uint { return dc.Uint(key, defVal...) } 114 | 115 | // Uint get a int value, if not found return default value 116 | func (c *Ini) Uint(key string, defVal ...uint) (value uint) { 117 | i64, exist := c.tryInt64(key) 118 | 119 | if exist { 120 | value = uint(i64) 121 | } else if len(defVal) > 0 { 122 | value = defVal[0] 123 | } 124 | return 125 | } 126 | 127 | // Int64 get an int value, if not found return default value 128 | func Int64(key string, defVal ...int64) int64 { return dc.Int64(key, defVal...) } 129 | 130 | // Int64 get a int value, if not found return default value 131 | func (c *Ini) Int64(key string, defVal ...int64) (value int64) { 132 | value, exist := c.tryInt64(key) 133 | 134 | if !exist && len(defVal) > 0 { 135 | value = defVal[0] 136 | } 137 | return 138 | } 139 | 140 | // try get a int64 value by given key 141 | func (c *Ini) tryInt64(key string) (value int64, ok bool) { 142 | strVal, ok := c.GetValue(key) 143 | if !ok { 144 | return 145 | } 146 | 147 | value, err := strconv.ParseInt(strVal, 10, 0) 148 | if err != nil { 149 | c.err = err 150 | } 151 | return 152 | } 153 | 154 | // Bool get a bool value, if not found return default value 155 | func Bool(key string, defVal ...bool) bool { return dc.Bool(key, defVal...) } 156 | 157 | // Bool Looks up a value for a key in this section and attempts to parse that value as a boolean, 158 | // along with a boolean result similar to a map lookup. 159 | // 160 | // The `value` boolean will be false in the event that the value could not be parsed as a bool 161 | func (c *Ini) Bool(key string, defVal ...bool) (value bool) { 162 | rawVal, ok := c.GetValue(key) 163 | if !ok { 164 | if len(defVal) > 0 { 165 | return defVal[0] 166 | } 167 | return 168 | } 169 | 170 | var err error 171 | value, err = strutil.ToBool(rawVal) 172 | if err != nil { 173 | c.err = err 174 | } 175 | 176 | return 177 | } 178 | 179 | // Strings get a string array, by split a string 180 | func Strings(key string, sep ...string) []string { return dc.Strings(key, sep...) } 181 | 182 | // Strings get a string array, by split a string 183 | func (c *Ini) Strings(key string, sep ...string) (ss []string) { 184 | str, ok := c.GetValue(key) 185 | if !ok { 186 | return 187 | } 188 | 189 | sepChar := "," 190 | if len(sep) > 0 { 191 | sepChar = sep[0] 192 | } 193 | return strutil.Split(str, sepChar) 194 | } 195 | 196 | // StringMap get a section data map 197 | func StringMap(name string) map[string]string { return dc.StringMap(name) } 198 | 199 | // Section get a section data map. is alias of StringMap() 200 | func (c *Ini) Section(name string) Section { return c.StringMap(name) } 201 | 202 | // StringMap get a section data map by name 203 | func (c *Ini) StringMap(name string) (mp map[string]string) { 204 | name = c.formatKey(name) 205 | // empty name, return default section 206 | if name == "" { 207 | name = c.opts.DefSection 208 | } 209 | 210 | mp, ok := c.data[name] 211 | if !ok { 212 | return 213 | } 214 | 215 | // if c.opts.ParseVar || c.opts.ParseEnv { 216 | if c.opts.ParseVar { 217 | for k, v := range mp { 218 | // parser Var refer 219 | if c.opts.ParseVar { 220 | v = c.parseVarReference(k, v, mp) 221 | } 222 | 223 | // parse ENV. like: "${SHELL}" 224 | // if c.opts.ParseEnv { 225 | // v = envutil.ParseEnvValue(v) 226 | // } 227 | 228 | mp[k] = v 229 | } 230 | } 231 | 232 | return 233 | } 234 | 235 | // MapStruct get config data and binding to the structure. 236 | // If the key is empty, will bind all data to the struct ptr. 237 | func MapStruct(key string, ptr any) error { return dc.MapStruct(key, ptr) } 238 | 239 | // Decode all data to struct pointer 240 | func (c *Ini) Decode(ptr any) error { return c.MapStruct("", ptr) } 241 | 242 | // MapTo mapping all data to struct pointer. 243 | // 244 | // Deprecated: please use Decode() 245 | func (c *Ini) MapTo(ptr any) error { return c.MapStruct("", ptr) } 246 | 247 | // MapStruct get config data and binding to the structure. 248 | // If the key is empty, will bind all data to the struct ptr. 249 | // 250 | // Usage: 251 | // 252 | // user := &Db{} 253 | // ini.MapStruct("user", &user) 254 | func (c *Ini) MapStruct(key string, ptr any) error { 255 | // parts data of the config 256 | if key != "" { 257 | data := c.StringMap(key) 258 | if len(data) == 0 { 259 | return errNotFound 260 | } 261 | return internal.MapStruct(c.opts.TagName, data, ptr) 262 | } 263 | 264 | // ----- binding all data ----- 265 | data := make(map[string]map[string]string, len(c.data)) 266 | for name, value := range c.data { 267 | data[name] = value 268 | } 269 | return internal.LiteToStruct(c.opts.TagName, c.opts.DefSection, data, ptr) 270 | } 271 | 272 | /************************************************************* 273 | * write config value 274 | *************************************************************/ 275 | 276 | // Set a value to the section by key. 277 | // 278 | // if section is empty, will set to default section 279 | func Set(key string, val any, section ...string) error { 280 | return dc.Set(key, val, section...) 281 | } 282 | 283 | // Set a value to the section by key. 284 | // 285 | // if section is empty, will set to default section 286 | func (c *Ini) Set(key string, val any, section ...string) (err error) { 287 | if c.opts.Readonly { 288 | return errReadonly 289 | } 290 | 291 | c.ensureInit() 292 | c.lock.Lock() 293 | defer c.lock.Unlock() 294 | 295 | key = c.formatKey(key) 296 | if key == "" { 297 | return errEmptyKey 298 | } 299 | 300 | // section name 301 | group := c.opts.DefSection 302 | if len(section) > 0 && section[0] != "" { 303 | group = section[0] 304 | } 305 | 306 | // allow section name is empty string "" 307 | group = c.formatKey(group) 308 | strVal := strutil.QuietString(val) 309 | 310 | sec, ok := c.data[group] 311 | if ok { 312 | sec[key] = strVal 313 | } else { 314 | sec = Section{key: strVal} 315 | } 316 | 317 | c.data[group] = sec 318 | return 319 | } 320 | 321 | // SetSection if not exist, add new section. If existed, will merge to old section. 322 | func (c *Ini) SetSection(name string, values map[string]string) (err error) { 323 | if c.opts.Readonly { 324 | return errReadonly 325 | } 326 | 327 | name = c.formatKey(name) 328 | if old, ok := c.data[name]; ok { 329 | c.data[name] = maputil.MergeStringMap(values, old, c.opts.IgnoreCase) 330 | return 331 | } 332 | 333 | if c.opts.IgnoreCase { 334 | values = mapKeyToLower(values) 335 | } 336 | c.data[name] = values 337 | return 338 | } 339 | 340 | // NewSection add new section data, existed will be replaced 341 | func (c *Ini) NewSection(name string, values map[string]string) (err error) { 342 | if c.opts.Readonly { 343 | return errReadonly 344 | } 345 | 346 | if c.opts.IgnoreCase { 347 | name = strings.ToLower(name) 348 | c.data[name] = mapKeyToLower(values) 349 | } else { 350 | c.data[name] = values 351 | } 352 | return 353 | } 354 | 355 | /************************************************************* 356 | * config dump 357 | *************************************************************/ 358 | 359 | // PrettyJSON translate to pretty JSON string 360 | func (c *Ini) PrettyJSON() string { 361 | if len(c.data) == 0 { 362 | return "" 363 | } 364 | 365 | out, _ := json.MarshalIndent(c.data, "", " ") 366 | return string(out) 367 | } 368 | 369 | // WriteToFile write config data to a file 370 | func (c *Ini) WriteToFile(file string) (int64, error) { 371 | fd, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0664) 372 | if err != nil { 373 | return 0, err 374 | } 375 | 376 | defer fd.Close() 377 | return c.WriteTo(fd) 378 | } 379 | 380 | // WriteTo out an INI File representing the current state to a writer. 381 | func (c *Ini) WriteTo(out io.Writer) (n int64, err error) { 382 | mp := make(map[string]map[string]string, len(c.data)) 383 | for group, secMp := range c.data { 384 | mp[group] = secMp 385 | } 386 | 387 | bs, err := parser.EncodeWith(mp, &parser.EncodeOptions{ 388 | Comments: c.comments, 389 | DefSection: c.opts.DefSection, 390 | // raw value map 391 | RawValueMap: c.rawBak, 392 | AddExportDate: true, 393 | }) 394 | if err != nil { 395 | return 0, err 396 | } 397 | 398 | var ni int 399 | ni, err = out.Write(bs) 400 | return int64(ni), err 401 | } 402 | 403 | /************************************************************* 404 | * section operate 405 | *************************************************************/ 406 | 407 | // HasSection has section 408 | func (c *Ini) HasSection(name string) bool { 409 | name = c.formatKey(name) 410 | _, ok := c.data[name] 411 | return ok 412 | } 413 | 414 | // DelSection del section by name 415 | func (c *Ini) DelSection(name string) (ok bool) { 416 | if c.opts.Readonly { 417 | return 418 | } 419 | 420 | name = c.formatKey(name) 421 | if _, ok = c.data[name]; ok { 422 | delete(c.data, name) 423 | } 424 | return 425 | } 426 | 427 | // SectionKeys get all section names 428 | func SectionKeys(withDefSection bool) (ls []string) { 429 | return dc.SectionKeys(withDefSection) 430 | } 431 | 432 | // SectionKeys get all section names 433 | func (c *Ini) SectionKeys(withDefSection bool) (ls []string) { 434 | defaultSection := c.opts.DefSection 435 | 436 | for section := range c.data { 437 | if !withDefSection && section == defaultSection { 438 | continue 439 | } 440 | 441 | ls = append(ls, section) 442 | } 443 | return 444 | } 445 | --------------------------------------------------------------------------------