feat(支持写入): 重构代码并支持写入

This commit is contained in:
2026-07-09 19:52:53 +08:00
parent 0edc5682de
commit df639dc91b
5 changed files with 715 additions and 224 deletions
-10
View File
@@ -1,10 +0,0 @@
kind: pipeline
type: docker
name: default
steps:
- name: build
pull: if-not-exists
image: golang
commands:
- go build goini.go
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+1 -1
View File
@@ -1,6 +1,6 @@
# GoINI [![PkgGoDev](https://pkg.go.dev/badge/github.com/xiaoqidun/goini)](https://pkg.go.dev/github.com/xiaoqidun/goini) # GoINI [![PkgGoDev](https://pkg.go.dev/badge/github.com/xiaoqidun/goini)](https://pkg.go.dev/github.com/xiaoqidun/goini)
简单易用的Golang INI配置解析 简单易用、零依赖的 Go 语言 INI 配置文件读写
# 安装方法 # 安装方法
+216 -207
View File
@@ -1,185 +1,207 @@
package goini package goini
import ( import (
"bytes"
"errors" "errors"
"os" "os"
"reflect" "reflect"
"regexp"
"strconv" "strconv"
"strings" "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 { type GoINI struct {
tag string
data []byte data []byte
dataMap map[string]map[string]string filename string
nameList []string fileLines []iniLine
dataKeyList map[string][]string structTag string
commonField 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 { func NewGoINI() *GoINI {
return &GoINI{ return &GoINI{structTag: defaultTag, rootSection: defaultRootSection}
tag: "goini",
commonField: "BuiltCommon",
}
} }
// parse ini配置文件解析 // SetData 从字节内容读取配置并解析
func (ini *GoINI) parse() { // 入参: fileData 配置文件内容
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 从代码读取配置并解析
func (ini *GoINI) SetData(fileData []byte) { func (ini *GoINI) SetData(fileData []byte) {
ini.data = bytes.TrimSpace(fileData) ini.filename = ""
ini.data = append(ini.data[:0], fileData...)
ini.parse() ini.parse()
} }
// LoadFile 从文件读取配置并解析 // LoadFile 从文件读取配置并解析
func (ini *GoINI) LoadFile(fileName string) error { // 入参: filename 配置文件路径
b, err := os.ReadFile(fileName) // 返回: error 错误信息
func (ini *GoINI) LoadFile(filename string) error {
b, err := os.ReadFile(filename)
if err != nil { if err != nil {
return err return err
} }
ini.data = bytes.TrimSpace(b) ini.filename = filename
ini.data = append(ini.data[:0], b...)
ini.parse() ini.parse()
return nil 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 获取配置文件分区列表 // GetNames 获取配置文件分区列表
// 入参: match 分区名称正则表达式
// 返回: []string 分区名称列表
func (ini *GoINI) GetNames(match string) []string { func (ini *GoINI) GetNames(match string) []string {
if match == "" { return matchStrings(ini.sectionOrder, 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
} }
// GetNameKeys 获取分区下配置项列表 // GetNameKeys 获取分区下配置项列表
// 入参: name 分区名称, match 配置项名称正则表达式
// 返回: []string 配置项名称列表
func (ini *GoINI) GetNameKeys(name string, match string) []string { func (ini *GoINI) GetNameKeys(name string, match string) []string {
var keyList []string name = ini.normalizeSection(name)
if name == "" { return matchStrings(ini.sectionKeys[name], match)
name = ini.commonField }
}
if keyList, ok := ini.dataKeyList[name]; ok { // GetString 获取单个配置项的字符串值
if match == "" { // 入参: name 分区名称, key 配置项名称, value 默认值
return keyList // 返回: string 配置项值
} func (ini *GoINI) GetString(name string, key string, value string) string {
var matchKeyList []string name = ini.normalizeSection(name)
for _, key := range keyList { if v, ok := ini.sectionValues[name]; ok {
if b, e := regexp.MatchString(match, key); e == nil && b { if vv, ok := v[key]; ok {
matchKeyList = append(matchKeyList, key) return vv
} }
} }
return matchKeyList return value
}
return keyList
} }
// GetBool 获取单个配置项的布尔值 // GetBool 获取单个配置项的布尔值
// 入参: name 分区名称, key 配置项名称, value 默认值
// 返回: bool 配置项值
func (ini *GoINI) GetBool(name string, key string, value bool) bool { func (ini *GoINI) GetBool(name string, key string, value bool) bool {
boolStr := ini.GetString(name, key, "") boolStr := ini.GetString(name, key, "")
switch strings.ToLower(boolStr) { switch strings.ToLower(boolStr) {
@@ -194,7 +216,9 @@ func (ini *GoINI) GetBool(name string, key string, value bool) bool {
return value return value
} }
// GetInt64 获取单个配置项的数 // GetInt64 获取单个配置项的数值
// 入参: name 分区名称, key 配置项名称, value 默认值
// 返回: int64 配置项值
func (ini *GoINI) GetInt64(name string, key string, value int64) int64 { func (ini *GoINI) GetInt64(name string, key string, value int64) int64 {
int64Str := ini.GetString(name, key, "") int64Str := ini.GetString(name, key, "")
if i, e := strconv.ParseInt(int64Str, 10, 64); e == nil { 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 return value
} }
// GetString 获取单个配置项的字符 // GetFloat64 获取单个配置项的浮点数
func (ini *GoINI) GetString(name string, key string, value string) string { // 入参: name 分区名称, key 配置项名称, value 默认值
if 0 == len(name) { // 返回: float64 配置项值
name = ini.commonField
}
if v, ok := ini.dataMap[name]; ok {
if vv, ok := v[key]; ok {
return vv
}
}
return value
}
// GetFloat64 获取单个配置项的小数值
func (ini *GoINI) GetFloat64(name string, key string, value float64) float64 { func (ini *GoINI) GetFloat64(name string, key string, value float64) float64 {
float64Str := ini.GetString(name, key, "") float64Str := ini.GetString(name, key, "")
if f, e := strconv.ParseFloat(float64Str, 64); e == nil { 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 return value
} }
// MapToStruct 将配置映射到一个结构体 // GetComment 获取配置项注释
func (ini *GoINI) MapToStruct(ptr interface{}) (err error) { // 入参: name 分区名称, key 配置项名称
t := reflect.TypeOf(ptr) // 返回: string 注释内容
v := reflect.ValueOf(ptr) func (ini *GoINI) GetComment(name string, key string) string {
if t.Kind() != reflect.Ptr { if key == "" {
err = errors.New("input struct ptr") return ini.GetSectionComment(name)
return
} }
t = t.Elem() ini.ensureParsed()
v = v.Elem() name = ini.normalizeSection(name)
for i := 0; i < t.NumField(); i++ { index, ok := ini.findSectionKeyLineIndex(name, strings.TrimSpace(key))
if !v.CanInterface() { if !ok {
continue return ""
} }
k := t.Field(i).Tag.Get(ini.tag) return ini.commentBefore(index)
if k == "" { }
k = t.Field(i).Name
// 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())
} }
switch v.Field(i).Kind() { index, ok := ini.findSectionLineIndex(name)
case reflect.Struct: if !ok {
tt := v.Field(i).Type() return ""
vv := v.Field(i)
for ii := 0; ii < tt.NumField(); ii++ {
if !v.CanInterface() {
continue
} }
kk := tt.Field(ii).Tag.Get(ini.tag) return ini.commentBefore(index)
if kk == "" { }
kk = t.Field(ii).Name
// 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
} }
switch vv.Field(ii).Kind() { v = v.Elem()
case reflect.Bool: if v.Kind() != reflect.Struct {
vv.Field(ii).SetBool(ini.GetBool(k, kk, false)) return errStructPointerRequired
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)))
} }
} ini.mapStruct(v)
case reflect.Bool: return nil
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)))
}
}
return
} }
+492
View File
@@ -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
}