From df639dc91b5084375ce86e70468dd39668f2d574 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Thu, 9 Jul 2026 19:52:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=94=AF=E6=8C=81=E5=86=99=E5=85=A5):=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=BB=A3=E7=A0=81=E5=B9=B6=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=86=99=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .drone.yml | 10 -- LICENSE | 2 +- README.md | 2 +- goini.go | 433 +++++++++++++++++++++++---------------------- internal.go | 492 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 715 insertions(+), 224 deletions(-) delete mode 100644 .drone.yml create mode 100644 internal.go diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 471c8d2..0000000 --- a/.drone.yml +++ /dev/null @@ -1,10 +0,0 @@ -kind: pipeline -type: docker -name: default - -steps: - - name: build - pull: if-not-exists - image: golang - commands: - - go build goini.go \ No newline at end of file diff --git a/LICENSE b/LICENSE index cd27e86..78162ad 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 xiaoqidun@gmail.com +Copyright (c) 2018-2026 肖其顿 (XIAO QI DUN) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 25777da..abb9936 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # GoINI [![PkgGoDev](https://pkg.go.dev/badge/github.com/xiaoqidun/goini)](https://pkg.go.dev/github.com/xiaoqidun/goini) -简单易用的Golang INI配置解析库 +简单易用、零依赖的 Go 语言 INI 配置文件读写库 # 安装方法 diff --git a/goini.go b/goini.go index 97a763f..c138aa3 100644 --- a/goini.go +++ b/goini.go @@ -1,185 +1,207 @@ package goini import ( - "bytes" "errors" "os" "reflect" - "regexp" "strconv" "strings" ) -// GoINI GoINI数据结构 +const ( + defaultTag = "goini" + defaultSeparator = " = " + defaultRootSection = "BuiltCommon" +) + +var ( + errFilenameRequired = errors.New("filename required") + errStructPointerRequired = errors.New("struct pointer required") +) + +// GoINI 表示INI配置解析器 type GoINI struct { - tag string - data []byte - dataMap map[string]map[string]string - nameList []string - dataKeyList map[string][]string - commonField string + data []byte + filename string + fileLines []iniLine + structTag string + rootSection string + sectionKeys map[string][]string + sectionOrder []string + sectionValues map[string]map[string]string + sectionLineIndexes map[string]int + sectionKeyLineIndexes map[string]map[string]int } -// NewGoINI 获取GoINI对象 +// NewGoINI 创建GoINI对象 +// 返回: *GoINI GoINI对象 func NewGoINI() *GoINI { - return &GoINI{ - tag: "goini", - commonField: "BuiltCommon", - } + return &GoINI{structTag: defaultTag, rootSection: defaultRootSection} } -// parse ini配置文件解析器 -func (ini *GoINI) parse() { - currentName := ini.commonField - iniData := make(map[string]map[string]string) - iniData[currentName] = make(map[string]string) - ini.nameList = append(ini.nameList, currentName) - ini.dataKeyList = make(map[string][]string) - for _, v := range bytes.Split(ini.data, []byte("\n")) { - line := bytes.TrimSpace(v) - lineLen := len(line) - if line == nil || lineLen < 3 || - line[0] == 35 || line[0] == 59 { - continue - } - if line[0] == 91 && line[lineLen-1] == 93 { - name := bytes.TrimSpace(line[1 : lineLen-1]) - if name != nil { - currentName = string(name) - if _, ok := iniData[currentName]; !ok { - iniData[currentName] = make(map[string]string) - ini.nameList = append(ini.nameList, currentName) - } - } - continue - } - split := bytes.IndexByte(line, 61) - if split == -1 { - split = bytes.IndexByte(line, 32) - if split == -1 { - continue - } - } - key := bytes.TrimSpace(line[0:split]) - value := bytes.TrimSpace(line[split+1:]) - valueLen := len(value) - keyStr, valueStr := string(key), string(value) - if keyStr == "" { - continue - } - if _, ok := iniData[currentName][keyStr]; ok { - continue - } - ini.dataKeyList[currentName] = append(ini.dataKeyList[currentName], keyStr) - if value == nil { - iniData[currentName][keyStr] = "" - continue - } - if valueLen >= 2 && - ((value[0] == 34 && value[valueLen-1] == 34) || - (value[0] == 39 && value[valueLen-1] == 39) || - (value[0] == 96 && value[valueLen-1] == 96)) { - iniData[currentName][keyStr] = string(value[1 : valueLen-1]) - continue - } - iniData[currentName][keyStr] = valueStr - } - ini.dataMap = iniData -} - -// String 获取GoINI对象字符串形式 -func (ini *GoINI) String() string { - var iniLines []string - for _, name := range ini.GetNames("") { - if name != ini.commonField { - if len(iniLines) < 1 { - iniLines = append(iniLines, "["+name+"]") - } else { - iniLines = append(iniLines, "\n["+name+"]") - } - } - for _, key := range ini.GetNameKeys(name, "") { - nameValue := ini.GetString(name, key, "") - if ok, _ := regexp.MatchString("^\\s|\\s$", nameValue); ok { - iniLines = append(iniLines, key+" = \""+nameValue+"\"") - continue - } - nameValueLen := len(nameValue) - if nameValueLen >= 2 && - ((nameValue[0] == 34 && nameValue[nameValueLen-1] == 34) || - (nameValue[0] == 39 && nameValue[nameValueLen-1] == 39) || - (nameValue[0] == 96 && nameValue[nameValueLen-1] == 96)) { - tag := `"` - if nameValue[0] == 34 { - tag = "`" - } - iniLines = append(iniLines, key+" = "+tag+nameValue+tag) - } else { - iniLines = append(iniLines, key+" = "+nameValue) - } - } - } - return strings.Join(iniLines, "\n") -} - -// SetTag 设置结构体的tag键名称 -func (ini *GoINI) SetTag(tag string) { - ini.tag = tag -} - -// SetData 从代码读取配置并解析 +// SetData 从字节内容读取配置并解析 +// 入参: fileData 配置文件内容 func (ini *GoINI) SetData(fileData []byte) { - ini.data = bytes.TrimSpace(fileData) + ini.filename = "" + ini.data = append(ini.data[:0], fileData...) ini.parse() } // LoadFile 从文件读取配置并解析 -func (ini *GoINI) LoadFile(fileName string) error { - b, err := os.ReadFile(fileName) +// 入参: filename 配置文件路径 +// 返回: error 错误信息 +func (ini *GoINI) LoadFile(filename string) error { + b, err := os.ReadFile(filename) if err != nil { return err } - ini.data = bytes.TrimSpace(b) + ini.filename = filename + ini.data = append(ini.data[:0], b...) ini.parse() return nil } +// Save 将配置写回LoadFile读取的文件 +// 返回: error 错误信息 +func (ini *GoINI) Save() error { + if ini.filename == "" { + return errFilenameRequired + } + return ini.SaveFile(ini.filename) +} + +// SaveFile 将配置写入指定文件 +// 入参: filename 配置文件路径 +// 返回: error 错误信息 +func (ini *GoINI) SaveFile(filename string) error { + if filename == "" { + return errFilenameRequired + } + if err := os.WriteFile(filename, []byte(ini.String()), 0o666); err != nil { + return err + } + ini.filename = filename + return nil +} + +// String 获取GoINI对象的字符串形式 +// 返回: string 配置文件内容 +func (ini *GoINI) String() string { + if len(ini.fileLines) == 0 { + return "" + } + renderedLines := make([]string, len(ini.fileLines)) + for i, line := range ini.fileLines { + renderedLines[i] = renderLine(line) + } + return strings.Join(renderedLines, "\n") +} + +// SetString 设置单个配置项的字符串值 +// 入参: name 分区名称, key 配置项名称, value 配置项值 +func (ini *GoINI) SetString(name string, key string, value string) { + ini.ensureParsed() + name = ini.normalizeSection(name) + key = strings.TrimSpace(key) + if key == "" { + return + } + if index, ok := ini.findSectionKeyLineIndex(name, key); ok { + ini.fileLines[index].keyValue = value + ini.fileLines[index].modified = true + ini.rebuildIndexes() + return + } + ini.ensureSectionLine(name) + ini.insertLine(ini.findSectionInsertIndex(name), iniLine{kind: lineKeyValue, sectionName: name, keyName: key, keyValue: value, separator: defaultSeparator, modified: true}) +} + +// SetBool 设置单个配置项的布尔值 +// 入参: name 分区名称, key 配置项名称, value 配置项值 +func (ini *GoINI) SetBool(name string, key string, value bool) { + ini.SetString(name, key, strconv.FormatBool(value)) +} + +// SetInt64 设置单个配置项的整数值 +// 入参: name 分区名称, key 配置项名称, value 配置项值 +func (ini *GoINI) SetInt64(name string, key string, value int64) { + ini.SetString(name, key, strconv.FormatInt(value, 10)) +} + +// SetFloat64 设置单个配置项的浮点数值 +// 入参: name 分区名称, key 配置项名称, value 配置项值 +func (ini *GoINI) SetFloat64(name string, key string, value float64) { + ini.SetString(name, key, strconv.FormatFloat(value, 'f', -1, 64)) +} + +// SetComment 设置配置项注释,空注释会删除已有注释 +// 入参: name 分区名称, key 配置项名称, comment 注释内容 +// 返回: bool 是否设置成功 +func (ini *GoINI) SetComment(name string, key string, comment string) bool { + if key == "" { + return ini.SetSectionComment(name, comment) + } + ini.ensureParsed() + name = ini.normalizeSection(name) + key = strings.TrimSpace(key) + index, ok := ini.findSectionKeyLineIndex(name, key) + if !ok { + return false + } + ini.replaceCommentBefore(index, comment) + return true +} + +// SetSectionComment 设置分区注释,空注释会删除已有注释 +// 入参: name 分区名称, comment 注释内容 +// 返回: bool 是否设置成功 +func (ini *GoINI) SetSectionComment(name string, comment string) bool { + ini.ensureParsed() + name = ini.normalizeSection(name) + if name == ini.rootSection { + ini.replaceCommentBefore(ini.firstRootLineIndex(), comment) + return true + } + ini.ensureSectionLine(name) + index, ok := ini.findSectionLineIndex(name) + if !ok { + return false + } + ini.replaceCommentBefore(index, comment) + return true +} + // GetNames 获取配置文件分区列表 +// 入参: match 分区名称正则表达式 +// 返回: []string 分区名称列表 func (ini *GoINI) GetNames(match string) []string { - if match == "" { - return ini.nameList - } - var matchNameList []string - for _, name := range ini.nameList { - if b, e := regexp.MatchString(match, name); e == nil && b { - matchNameList = append(matchNameList, name) - } - } - return matchNameList + return matchStrings(ini.sectionOrder, match) } // GetNameKeys 获取分区下配置项列表 +// 入参: name 分区名称, match 配置项名称正则表达式 +// 返回: []string 配置项名称列表 func (ini *GoINI) GetNameKeys(name string, match string) []string { - var keyList []string - if name == "" { - name = ini.commonField - } - if keyList, ok := ini.dataKeyList[name]; ok { - if match == "" { - return keyList + name = ini.normalizeSection(name) + return matchStrings(ini.sectionKeys[name], match) +} + +// GetString 获取单个配置项的字符串值 +// 入参: name 分区名称, key 配置项名称, value 默认值 +// 返回: string 配置项值 +func (ini *GoINI) GetString(name string, key string, value string) string { + name = ini.normalizeSection(name) + if v, ok := ini.sectionValues[name]; ok { + if vv, ok := v[key]; ok { + return vv } - var matchKeyList []string - for _, key := range keyList { - if b, e := regexp.MatchString(match, key); e == nil && b { - matchKeyList = append(matchKeyList, key) - } - } - return matchKeyList } - return keyList + return value } // GetBool 获取单个配置项的布尔值 +// 入参: name 分区名称, key 配置项名称, value 默认值 +// 返回: bool 配置项值 func (ini *GoINI) GetBool(name string, key string, value bool) bool { boolStr := ini.GetString(name, key, "") switch strings.ToLower(boolStr) { @@ -194,7 +216,9 @@ func (ini *GoINI) GetBool(name string, key string, value bool) bool { return value } -// GetInt64 获取单个配置项的数字值 +// GetInt64 获取单个配置项的整数值 +// 入参: name 分区名称, key 配置项名称, value 默认值 +// 返回: int64 配置项值 func (ini *GoINI) GetInt64(name string, key string, value int64) int64 { int64Str := ini.GetString(name, key, "") if i, e := strconv.ParseInt(int64Str, 10, 64); e == nil { @@ -203,20 +227,9 @@ func (ini *GoINI) GetInt64(name string, key string, value int64) int64 { return value } -// GetString 获取单个配置项的字符值 -func (ini *GoINI) GetString(name string, key string, value string) string { - if 0 == len(name) { - name = ini.commonField - } - if v, ok := ini.dataMap[name]; ok { - if vv, ok := v[key]; ok { - return vv - } - } - return value -} - -// GetFloat64 获取单个配置项的小数值 +// GetFloat64 获取单个配置项的浮点数值 +// 入参: name 分区名称, key 配置项名称, value 默认值 +// 返回: float64 配置项值 func (ini *GoINI) GetFloat64(name string, key string, value float64) float64 { float64Str := ini.GetString(name, key, "") if f, e := strconv.ParseFloat(float64Str, 64); e == nil { @@ -225,60 +238,56 @@ func (ini *GoINI) GetFloat64(name string, key string, value float64) float64 { return value } -// MapToStruct 将配置映射到一个结构体 -func (ini *GoINI) MapToStruct(ptr interface{}) (err error) { - t := reflect.TypeOf(ptr) - v := reflect.ValueOf(ptr) - if t.Kind() != reflect.Ptr { - err = errors.New("input struct ptr") - return +// GetComment 获取配置项注释 +// 入参: name 分区名称, key 配置项名称 +// 返回: string 注释内容 +func (ini *GoINI) GetComment(name string, key string) string { + if key == "" { + return ini.GetSectionComment(name) } - t = t.Elem() - v = v.Elem() - for i := 0; i < t.NumField(); i++ { - if !v.CanInterface() { - continue - } - k := t.Field(i).Tag.Get(ini.tag) - if k == "" { - k = t.Field(i).Name - } - switch v.Field(i).Kind() { - case reflect.Struct: - tt := v.Field(i).Type() - vv := v.Field(i) - for ii := 0; ii < tt.NumField(); ii++ { - if !v.CanInterface() { - continue - } - kk := tt.Field(ii).Tag.Get(ini.tag) - if kk == "" { - kk = t.Field(ii).Name - } - switch vv.Field(ii).Kind() { - case reflect.Bool: - vv.Field(ii).SetBool(ini.GetBool(k, kk, false)) - case reflect.String: - vv.Field(ii).SetString(ini.GetString(k, kk, "")) - case reflect.Float32, reflect.Float64: - vv.Field(ii).SetFloat(ini.GetFloat64(k, kk, 0)) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - vv.Field(ii).SetInt(ini.GetInt64(k, kk, 0)) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - vv.Field(ii).SetUint(uint64(ini.GetInt64(k, kk, 0))) - } - } - case reflect.Bool: - v.Field(i).SetBool(ini.GetBool(ini.commonField, k, false)) - case reflect.String: - v.Field(i).SetString(ini.GetString(ini.commonField, k, "")) - case reflect.Float32, reflect.Float64: - v.Field(i).SetFloat(ini.GetFloat64(ini.commonField, k, 0)) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - v.Field(i).SetInt(ini.GetInt64(ini.commonField, k, 0)) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - v.Field(i).SetUint(uint64(ini.GetInt64(ini.commonField, k, 0))) - } + ini.ensureParsed() + name = ini.normalizeSection(name) + index, ok := ini.findSectionKeyLineIndex(name, strings.TrimSpace(key)) + if !ok { + return "" } - return + return ini.commentBefore(index) +} + +// GetSectionComment 获取分区注释 +// 入参: name 分区名称 +// 返回: string 注释内容 +func (ini *GoINI) GetSectionComment(name string) string { + ini.ensureParsed() + name = ini.normalizeSection(name) + if name == ini.rootSection { + return ini.commentBefore(ini.firstRootLineIndex()) + } + index, ok := ini.findSectionLineIndex(name) + if !ok { + return "" + } + return ini.commentBefore(index) +} + +// SetTag 设置结构体的tag键名称 +// 入参: tag tag键名称 +func (ini *GoINI) SetTag(tag string) { + ini.structTag = tag +} + +// MapToStruct 将配置映射到一个结构体 +// 入参: ptr 结构体指针 +// 返回: error 错误信息 +func (ini *GoINI) MapToStruct(ptr interface{}) error { + v := reflect.ValueOf(ptr) + if !v.IsValid() || v.Kind() != reflect.Ptr || v.IsNil() { + return errStructPointerRequired + } + v = v.Elem() + if v.Kind() != reflect.Struct { + return errStructPointerRequired + } + ini.mapStruct(v) + return nil } diff --git a/internal.go b/internal.go new file mode 100644 index 0000000..1d950d5 --- /dev/null +++ b/internal.go @@ -0,0 +1,492 @@ +package goini + +import ( + "reflect" + "regexp" + "strconv" + "strings" + "unicode" +) + +type lineKind int + +const ( + lineBlank lineKind = iota + lineComment + lineSection + lineKeyValue + lineRaw +) + +type iniLine struct { + kind lineKind + text string + sectionName string + keyName string + keyValue string + indent string + separator string + modified bool +} + +type keyValueParts struct { + key string + value string + indent string + separator string +} + +func (ini *GoINI) parse() { + ini.fileLines = nil + currentSection := ini.rootSection + if len(ini.data) == 0 { + ini.rebuildIndexes() + return + } + for _, rawLine := range strings.Split(string(ini.data), "\n") { + line, section := parseRawLine(rawLine, currentSection) + currentSection = section + ini.fileLines = append(ini.fileLines, line) + } + ini.rebuildIndexes() +} + +func parseRawLine(rawLine string, sectionName string) (iniLine, string) { + rawLine = strings.TrimSuffix(rawLine, "\r") + line := iniLine{kind: lineRaw, text: rawLine, sectionName: sectionName} + trimmed := strings.TrimSpace(rawLine) + if trimmed == "" { + line.kind = lineBlank + return line, sectionName + } + if isComment(trimmed) { + line.kind = lineComment + return line, sectionName + } + if nextSection, ok := parseSection(trimmed); ok { + line.kind = lineSection + line.sectionName = nextSection + return line, nextSection + } + if parts, ok := parseKeyValue(rawLine); ok { + line.kind = lineKeyValue + line.keyName = parts.key + line.keyValue = trimSurroundingQuotes(parts.value) + line.indent = parts.indent + line.separator = parts.separator + return line, sectionName + } + return line, sectionName +} + +func isComment(line string) bool { + return strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") +} + +func parseSection(line string) (string, bool) { + if !strings.HasPrefix(line, "[") || !strings.HasSuffix(line, "]") { + return "", false + } + name := strings.TrimSpace(line[1 : len(line)-1]) + return name, name != "" +} + +func parseKeyValue(line string) (keyValueParts, bool) { + indentLen := leadingSpaceLen(line) + indent := line[:indentLen] + body := line[indentLen:] + if split := strings.IndexByte(body, '='); split >= 0 { + return parseKeyValueParts(body, split, split+1, indent) + } + if split := strings.IndexFunc(body, unicode.IsSpace); split >= 0 { + return parseKeyValueParts(body, split, split, indent) + } + return keyValueParts{}, false +} + +func parseKeyValueParts(body string, keyEnd int, valueStart int, indent string) (keyValueParts, bool) { + keyPart := body[:keyEnd] + valuePart := body[valueStart:] + key := strings.TrimSpace(keyPart) + if key == "" { + return keyValueParts{}, false + } + separatorStart := len(strings.TrimRightFunc(keyPart, unicode.IsSpace)) + separatorEnd := valueStart + leadingSpaceLen(valuePart) + return keyValueParts{ + key: key, + value: strings.TrimSpace(valuePart), + indent: indent, + separator: body[separatorStart:separatorEnd], + }, true +} + +func leadingSpaceLen(text string) int { + return len(text) - len(strings.TrimLeftFunc(text, unicode.IsSpace)) +} + +func trimSurroundingQuotes(value string) string { + if !hasSurroundingQuotes(value) { + return value + } + return value[1 : len(value)-1] +} + +func hasSurroundingQuotes(value string) bool { + if len(value) < 2 { + return false + } + first, last := value[0], value[len(value)-1] + return first == last && (first == '"' || first == '\'' || first == '`') +} + +func (ini *GoINI) rebuildIndexes() { + ini.sectionKeys = make(map[string][]string) + ini.sectionOrder = nil + ini.sectionValues = make(map[string]map[string]string) + ini.sectionLineIndexes = make(map[string]int) + ini.sectionKeyLineIndexes = make(map[string]map[string]int) + ini.addSection(ini.rootSection) + for i, line := range ini.fileLines { + switch line.kind { + case lineSection: + ini.addSectionLineIndex(line.sectionName, i) + case lineKeyValue: + ini.addSectionKey(line.sectionName, line.keyName, line.keyValue, i) + } + } +} + +func (ini *GoINI) addSection(sectionName string) { + if _, ok := ini.sectionValues[sectionName]; ok { + return + } + ini.sectionValues[sectionName] = make(map[string]string) + ini.sectionOrder = append(ini.sectionOrder, sectionName) +} + +func (ini *GoINI) addSectionLineIndex(sectionName string, lineIndex int) { + if _, ok := ini.sectionLineIndexes[sectionName]; !ok { + ini.sectionLineIndexes[sectionName] = lineIndex + } + ini.addSection(sectionName) +} + +func (ini *GoINI) addSectionKey(sectionName string, keyName string, keyValue string, lineIndex int) { + ini.addSection(sectionName) + if _, ok := ini.sectionValues[sectionName][keyName]; ok { + return + } + ini.sectionValues[sectionName][keyName] = keyValue + ini.sectionKeys[sectionName] = append(ini.sectionKeys[sectionName], keyName) + if ini.sectionKeyLineIndexes[sectionName] == nil { + ini.sectionKeyLineIndexes[sectionName] = make(map[string]int) + } + ini.sectionKeyLineIndexes[sectionName][keyName] = lineIndex +} + +func (ini *GoINI) ensureParsed() { + if ini.sectionValues == nil { + ini.parse() + } +} + +func (ini *GoINI) normalizeSection(sectionName string) string { + if sectionName == "" { + return ini.rootSection + } + return sectionName +} + +func (ini *GoINI) findSectionLineIndex(sectionName string) (int, bool) { + index, ok := ini.sectionLineIndexes[sectionName] + return index, ok +} + +func (ini *GoINI) findSectionKeyLineIndex(sectionName string, keyName string) (int, bool) { + if ini.sectionKeyLineIndexes[sectionName] == nil { + return 0, false + } + index, ok := ini.sectionKeyLineIndexes[sectionName][keyName] + return index, ok +} + +func (ini *GoINI) ensureSectionLine(sectionName string) { + if sectionName == ini.rootSection { + ini.addSection(sectionName) + return + } + if _, ok := ini.sectionValues[sectionName]; ok { + return + } + if len(ini.fileLines) > 0 && renderLine(ini.fileLines[len(ini.fileLines)-1]) != "" { + ini.fileLines = append(ini.fileLines, iniLine{kind: lineBlank}) + } + ini.fileLines = append(ini.fileLines, iniLine{kind: lineSection, sectionName: sectionName, text: "[" + sectionName + "]"}) + ini.rebuildIndexes() +} + +func (ini *GoINI) findSectionInsertIndex(sectionName string) int { + if sectionName == ini.rootSection { + return ini.findRootInsertIndex() + } + current := ini.rootSection + insert := len(ini.fileLines) + for i, line := range ini.fileLines { + if line.kind == lineSection { + current = line.sectionName + if current == sectionName { + insert = i + 1 + } + continue + } + if current == sectionName { + insert = i + 1 + } + } + return insert +} + +func (ini *GoINI) findRootInsertIndex() int { + for i, line := range ini.fileLines { + if line.kind == lineSection { + return i + } + } + return len(ini.fileLines) +} + +func (ini *GoINI) insertLine(index int, line iniLine) { + if index < 0 || index > len(ini.fileLines) { + index = len(ini.fileLines) + } + ini.fileLines = append(ini.fileLines, iniLine{}) + copy(ini.fileLines[index+1:], ini.fileLines[index:]) + ini.fileLines[index] = line + ini.rebuildIndexes() +} + +func (ini *GoINI) replaceLines(start int, end int, replacement []iniLine) { + next := make([]iniLine, 0, len(ini.fileLines)-(end-start)+len(replacement)) + next = append(next, ini.fileLines[:start]...) + next = append(next, replacement...) + next = append(next, ini.fileLines[end:]...) + ini.fileLines = next + ini.rebuildIndexes() +} + +func (ini *GoINI) replaceCommentBefore(index int, comment string) { + start, end := ini.commentRangeBefore(index) + ini.replaceLines(start, end, commentLines(comment)) +} + +func (ini *GoINI) commentRangeBefore(index int) (int, int) { + start := index + for start > 0 && ini.fileLines[start-1].kind == lineComment { + start-- + } + return start, index +} + +func (ini *GoINI) commentBefore(index int) string { + start, end := ini.commentRangeBefore(index) + comments := make([]string, 0, end-start) + for _, line := range ini.fileLines[start:end] { + comments = append(comments, stripComment(line.text)) + } + return strings.Join(comments, "\n") +} + +func (ini *GoINI) firstRootLineIndex() int { + for i, line := range ini.fileLines { + if line.kind == lineKeyValue && line.sectionName == ini.rootSection { + return i + } + if line.kind == lineSection { + return i + } + } + return 0 +} + +func commentLines(comment string) []iniLine { + if comment == "" { + return nil + } + parts := strings.Split(strings.ReplaceAll(comment, "\r\n", "\n"), "\n") + lines := make([]iniLine, 0, len(parts)) + for _, part := range parts { + lines = append(lines, iniLine{kind: lineComment, text: formatComment(part)}) + } + return lines +} + +func formatComment(comment string) string { + trimmed := strings.TrimLeftFunc(comment, unicode.IsSpace) + if strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, ";") { + return comment + } + if comment == "" { + return "#" + } + return "# " + comment +} + +func stripComment(comment string) string { + trimmed := strings.TrimSpace(comment) + if !isComment(trimmed) { + return trimmed + } + trimmed = strings.TrimPrefix(strings.TrimPrefix(trimmed, "#"), ";") + return strings.TrimPrefix(trimmed, " ") +} + +func renderLine(line iniLine) string { + switch line.kind { + case lineSection: + if line.text != "" && !line.modified { + return line.text + } + return "[" + line.sectionName + "]" + case lineKeyValue: + if line.text != "" && !line.modified { + return line.text + } + return renderKeyValue(line) + default: + return line.text + } +} + +func renderKeyValue(line iniLine) string { + separator := line.separator + if separator == "" { + separator = defaultSeparator + } + return line.indent + line.keyName + separator + formatValue(line.keyValue) +} + +func formatValue(value string) string { + if strings.TrimSpace(value) == value && !hasSurroundingQuotes(value) { + return value + } + quote := `"` + if strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) { + quote = "`" + } + return quote + value + quote +} + +func matchStrings(items []string, match string) []string { + if match == "" { + return cloneStrings(items) + } + re, err := regexp.Compile(match) + if err != nil { + return nil + } + matches := make([]string, 0, len(items)) + for _, item := range items { + if re.MatchString(item) { + matches = append(matches, item) + } + } + return matches +} + +func cloneStrings(items []string) []string { + if items == nil { + return nil + } + cloned := make([]string, len(items)) + copy(cloned, items) + return cloned +} + +func (ini *GoINI) mapStruct(v reflect.Value) { + t := v.Type() + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + if !field.CanSet() { + continue + } + name, ok := ini.fieldName(t.Field(i)) + if !ok { + continue + } + if field.Kind() == reflect.Struct { + ini.mapSection(field, name) + continue + } + ini.setField(field, ini.rootSection, name) + } +} + +func (ini *GoINI) mapSection(v reflect.Value, section string) { + t := v.Type() + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + if !field.CanSet() || field.Kind() == reflect.Struct { + continue + } + name, ok := ini.fieldName(t.Field(i)) + if !ok { + continue + } + ini.setField(field, section, name) + } +} + +func (ini *GoINI) fieldName(field reflect.StructField) (string, bool) { + if ini.structTag != "" { + if tag, ok := field.Tag.Lookup(ini.structTag); ok { + name := strings.SplitN(tag, ",", 2)[0] + if name == "-" { + return "", false + } + if name != "" { + return name, true + } + } + } + return field.Name, true +} + +func (ini *GoINI) setField(field reflect.Value, section string, key string) { + switch field.Kind() { + case reflect.Bool: + field.SetBool(ini.GetBool(section, key, false)) + case reflect.String: + field.SetString(ini.GetString(section, key, "")) + case reflect.Float32, reflect.Float64: + field.SetFloat(ini.parseFloat(section, key, field.Type().Bits())) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + field.SetInt(ini.parseInt(section, key, field.Type().Bits())) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + field.SetUint(ini.parseUint(section, key, field.Type().Bits())) + } +} + +func (ini *GoINI) parseFloat(section string, key string, bitSize int) float64 { + value, err := strconv.ParseFloat(ini.GetString(section, key, ""), bitSize) + if err != nil { + return 0 + } + return value +} + +func (ini *GoINI) parseInt(section string, key string, bitSize int) int64 { + value, err := strconv.ParseInt(ini.GetString(section, key, ""), 10, bitSize) + if err != nil { + return 0 + } + return value +} + +func (ini *GoINI) parseUint(section string, key string, bitSize int) uint64 { + value, err := strconv.ParseUint(ini.GetString(section, key, ""), 10, bitSize) + if err != nil { + return 0 + } + return value +}