mirror of
https://github.com/xiaoqidun/goini.git
synced 2026-07-12 20:58:26 +08:00
feat(支持写入): 重构代码并支持写入
This commit is contained in:
+492
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user