From 3bf7cca329b473a85719c65b075330ed3c72d6f8 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Wed, 11 Nov 2026 16:28:21 +0000 Subject: [PATCH 1/4] Make File.Section stop adding missing sections File.Section and SectionWithIndex return a section that reads as empty for a name, or an index of it, the file doesn't have. A change to one of its keys, NewKey, NewBooleanKey, SetComment or ReflectFrom adds it. Until then, reads and changes through it follow any section of that name and index the file gains, as missing keys do. Reading a missing section no longer writes to the file, so HasSection, Sections and Write don't see it, and concurrent reads are safe with Locking(false). --- docs/migrate-v1-to-v2.md | 9 ++++- file.go | 41 ++++++++++++------- file_test.go | 85 ++++++++++++++++++++++++++++++++++++++++ key.go | 12 +++--- section.go | 61 +++++++++++++++++++++++----- struct.go | 3 ++ 6 files changed, 180 insertions(+), 31 deletions(-) diff --git a/docs/migrate-v1-to-v2.md b/docs/migrate-v1-to-v2.md index 15ff2ec..43d79fc 100644 --- a/docs/migrate-v1-to-v2.md +++ b/docs/migrate-v1-to-v2.md @@ -21,9 +21,14 @@ port := sec.Key("PORT").MustInt(3000) sec.Key("PORT").SetValue(strconv.Itoa(port)) ``` -### `Section.Key` no longer adds missing keys +### `File.Section` and `Section.Key` no longer add what's missing -In v1, `sec.Key("PORT")` added an empty `PORT` key when the section had none, so `HasKey`, `Keys` and `Write` saw it after a mere read. In v2 it returns a key that reads as empty, and only `SetValue` or `SetComment` adds it, or `AddShadow` and `AddNestedValue` when the file allows shadows or nested values. `sec.Key(name).SetValue(v)` works as before. +In v1, `f.Section("server").Key("PORT")` added an empty `[server]` section and `PORT` key when they didn't exist, so `HasSection`, `HasKey`, `Keys` and `Write` saw them after a mere read. In v2 they return a section and key that read as empty. Only a change adds them: + +- a key: `SetValue` or `SetComment`, or `AddShadow` and `AddNestedValue` when the file allows shadows or nested values +- a section: a change to one of its keys, `NewKey`, `NewBooleanKey`, `SetComment` or `ReflectFrom` + +`f.Section(name).Key(key).SetValue(v)` works as before. `SectionWithIndex` follows the same rule, and a change adds the section after the last one of its name. ### `Reload` and `Append` diff --git a/file.go b/file.go index efd6ca4..96b7cfd 100644 --- a/file.go +++ b/file.go @@ -190,30 +190,41 @@ func (f *File) sectionsByName(name string) ([]*Section, error) { return secs, nil } -// Section assumes named section exists and returns a zero-value when not. +// Section returns the section with the given name, or the default section +// for "". When there is none, it returns a section that reads as empty +// without adding it to the file. Section.SetComment, Section.NewKey, +// Section.ReflectFrom or a change to one of its keys adds it. func (f *File) Section(name string) *Section { - sec, err := f.GetSection(name) - if err != nil { - if name == "" { - // The name the lookup used, so the next lookup finds it. - name = f.defaultSectionName() - } - sec, _ = f.NewSection(name) + if sec, err := f.GetSection(name); err == nil { return sec } - return sec + return f.missingSection(name, 0) } -// SectionWithIndex assumes named section exists and returns a new section when not. +// SectionWithIndex returns the index-th section with the given name. When +// there is none, it returns a section that reads as empty, as Section does, +// and a change adds it after the last one of its name. Without +// AllowNonUniqueSections, it returns the section of the name for any index. func (f *File) SectionWithIndex(name string, index int) *Section { + if sec, err := f.sectionWithIndex(name, index); err == nil { + return sec + } + if !f.options.AllowNonUniqueSections { + return f.Section(name) + } + return f.missingSection(name, index) +} + +// missingSection returns a section for the index-th section with the given +// name that isn't in the file. +func (f *File) missingSection(name string, index int) *Section { if name == "" { name = f.defaultSectionName() + } else if (f.options.Insensitive || f.options.InsensitiveSections) && name != f.options.DefaultSection { + name = strings.ToLower(name) } - sec, err := f.sectionWithIndex(name, index) - if err != nil { - newSec, _ := f.NewSection(name) - return newSec - } + sec := newSection(f, name) + sec.missing, sec.index = true, index return sec } diff --git a/file_test.go b/file_test.go index 3a1e384..f688b24 100644 --- a/file_test.go +++ b/file_test.go @@ -291,6 +291,91 @@ VERSION = v1`), CaseInsensitive(true)) } }) + t.Run("reading a missing section doesn't add it", func(t *testing.T) { + const data = "[app]\nname = demo\n" + f, err := Load([]byte(data)) + require.NoError(t, err) + + sec := f.Section("server") + assert.Equal(t, "server", sec.Name()) + assert.Equal(t, 8080, sec.Key("port").MustInt(8080)) + assert.False(t, sec.HasKey("port")) + assert.Empty(t, sec.Keys()) + assert.Empty(t, sec.KeyValues()) + assert.Empty(t, sec.Comment()) + assert.Empty(t, sec.Body()) + sec.DeleteKey("port") + sec.SetBody("ignored, as the section isn't raw") + var cfg struct{ Port int } + require.NoError(t, sec.MapTo(&cfg)) + _ = f.SectionWithIndex("server", 3) + + assert.False(t, f.HasSection("server")) + assert.Equal(t, []string{DefaultSection, "app"}, f.SectionNames()) + var buf bytes.Buffer + _, err = f.WriteTo(&buf) + require.NoError(t, err) + assert.Equal(t, data, buf.String()) + }) + + t.Run("changing a missing section adds it", func(t *testing.T) { + f := Empty() + f.Section("a").SetComment("note") + _, err := f.Section("b").NewKey("k", "v") + require.NoError(t, err) + _, err = f.Section("c").NewBooleanKey("k") + require.NoError(t, err) + f.Section("d").Key("k").SetValue("v") + require.NoError(t, f.Section("e").ReflectFrom(&struct{}{})) + assert.Equal(t, []string{DefaultSection, "a", "b", "c", "d", "e"}, f.SectionNames()) + + _, err = f.Section("f").NewKey("", "v") + require.Error(t, err) + assert.False(t, f.HasSection("f"), "a failed change doesn't add it") + + sec := f.Section("g") + sec.Key("k").SetValue("1") + sec.Key("k").SetValue("2") + assert.Same(t, sec, f.Section("g"), "the section added is the one returned") + assert.Equal(t, "2", f.Section("g").Key("k").String()) + }) + + t.Run("a missing section follows the section its file gains", func(t *testing.T) { + f, err := Load([]byte(""), CaseInsensitiveSections(true)) + require.NoError(t, err) + + s1, s2 := f.Section("Server"), f.Section("server") + assert.Equal(t, "server", s1.Name()) + s1.Key("port").SetValue("1") + assert.Equal(t, "1", s2.Key("port").String()) + s2.Key("host").SetValue("h") + assert.Equal(t, []string{"port", "host"}, s1.KeyNames()) + + s3 := f.Section("log") + k := s3.Key("level") + _, err = f.NewSection("log") + require.NoError(t, err) + k.SetValue("info") + assert.Equal(t, "info", f.Section("log").Key("level").String()) + assert.Equal(t, []string{"default", "server", "log"}, f.SectionNames()) + }) + + t.Run("a missing index of non-unique sections", func(t *testing.T) { + f, err := Load([]byte("[Peer]\nk = 0\n"), AllowNonUniqueSections(true)) + require.NoError(t, err) + + sec := f.SectionWithIndex("Peer", 1) + assert.Empty(t, sec.Keys()) + sec.Key("k").SetValue("1") + secs, err := f.SectionsByName("Peer") + require.NoError(t, err) + require.Len(t, secs, 2) + assert.Same(t, sec, secs[1]) + + g, err := Load([]byte("[Peer]\nk = 0\n")) + require.NoError(t, err) + assert.Same(t, g.Section("Peer"), g.SectionWithIndex("Peer", 1), "without AllowNonUniqueSections") + }) } func TestFile_Sections(t *testing.T) { diff --git a/key.go b/key.go index a901d9e..5eeb854 100644 --- a/key.go +++ b/key.go @@ -46,7 +46,7 @@ func newKey(s *Section, name, val string) *Key { // holds the lock. func (k *Key) current() *Key { if k.missing { - if key := k.s.byName[k.name]; key != nil { + if key := k.s.current().byName[k.name]; key != nil { return key } } @@ -54,14 +54,16 @@ func (k *Key) current() *Key { } // attach adds k to its section if it's missing and the section still has no -// key of its name, and returns the key to change: k, or the key of its name -// the section gained. The caller holds the write lock. +// key of its name, adding the section to the file too if it's missing, and +// returns the key to change: k, or the key of its name the section gained. +// The caller holds the write lock. func (k *Key) attach() *Key { c := k.current() if c == k && k.missing { k.missing = false - k.s.keys = append(k.s.keys, k) - k.s.byName[k.name] = k + sec := k.s.attach() + sec.keys = append(sec.keys, k) + sec.byName[k.name] = k } return c } diff --git a/section.go b/section.go index 20646c2..008b100 100644 --- a/section.go +++ b/section.go @@ -18,6 +18,13 @@ type Section struct { isRawSection bool rawBody string + + // missing is set on a section File.Section or File.SectionWithIndex + // returned for a name, or an index of it, the file doesn't have. The + // section isn't in the file until a change adds it. index is the index + // of the name it stands for. + missing bool + index int } func newSection(f *File, name string) *Section { @@ -29,6 +36,31 @@ func newSection(f *File, name string) *Section { } } +// current returns the section s reads and changes. That is s, unless s is +// missing and its file has since gained a section of its name and index. +// The caller holds the lock. +func (s *Section) current() *Section { + if s.missing { + if secs := s.f.byName[s.name]; s.index >= 0 && s.index < len(secs) { + return secs[s.index] + } + } + return s +} + +// attach adds s to its file if it's missing and the file still has no +// section of its name and index, and returns the section to change. The +// caller holds the write lock. +func (s *Section) attach() *Section { + c := s.current() + if c == s && s.missing { + s.missing = false + s.f.sections = append(s.f.sections, s) + s.f.byName[s.name] = append(s.f.byName[s.name], s) + } + return c +} + // Name returns name of Section. func (s *Section) Name() string { return s.name @@ -39,7 +71,7 @@ func (s *Section) Name() string { func (s *Section) Comment() string { s.f.maybeRLock() defer s.f.maybeRUnlock() - return s.comment + return s.current().comment } // SetComment sets the comment lines written before the section header. @@ -47,7 +79,7 @@ func (s *Section) Comment() string { func (s *Section) SetComment(comment string) { s.f.maybeLock() defer s.f.maybeUnlock() - s.comment = comment + s.attach().comment = comment } // Body returns rawBody of Section if the section was marked as unparseable. @@ -55,14 +87,14 @@ func (s *Section) SetComment(comment string) { func (s *Section) Body() string { s.f.maybeRLock() defer s.f.maybeRUnlock() - return strings.TrimSpace(s.rawBody) + return strings.TrimSpace(s.current().rawBody) } // SetBody updates body content only if section is raw. func (s *Section) SetBody(body string) { s.f.maybeLock() defer s.f.maybeUnlock() - if !s.isRawSection { + if s = s.current(); !s.isRawSection { return } s.rawBody = body @@ -72,7 +104,11 @@ func (s *Section) SetBody(body string) { func (s *Section) NewKey(name, val string) (*Key, error) { s.f.maybeLock() defer s.f.maybeUnlock() - return s.addKey(name, val) + key, err := s.current().addKey(name, val) + if err == nil { + s.attach() + } + return key, err } // addKey is NewKey for a caller that holds the lock. @@ -104,7 +140,11 @@ func (s *Section) addKey(name, val string) (*Key, error) { func (s *Section) NewBooleanKey(name string) (*Key, error) { s.f.maybeLock() defer s.f.maybeUnlock() - return s.addBooleanKey(name) + key, err := s.current().addBooleanKey(name) + if err == nil { + s.attach() + } + return key, err } // addBooleanKey is NewBooleanKey for a caller that holds the lock. @@ -126,7 +166,7 @@ func (s *Section) GetKey(name string) (*Key, error) { if s.f.options.Insensitive || s.f.options.InsensitiveKeys { name = strings.ToLower(name) } - key := s.byName[name] + key := s.current().byName[name] if s.f.locking { s.f.mu.RUnlock() } @@ -163,7 +203,7 @@ func (s *Section) HasValue(value string) bool { defer s.f.mu.RUnlock() } - for _, k := range s.keys { + for _, k := range s.current().keys { if value == k.value { return true } @@ -196,7 +236,7 @@ func (s *Section) Key(name string) *Key { func (s *Section) Keys() []*Key { s.f.maybeRLock() defer s.f.maybeRUnlock() - return slices.Clone(s.keys) + return slices.Clone(s.current().keys) } // ParentKeys returns list of keys of parent section. @@ -223,6 +263,7 @@ func (s *Section) ParentKeys() []*Key { func (s *Section) KeyNames() []string { s.f.maybeRLock() defer s.f.maybeRUnlock() + s = s.current() names := make([]string, len(s.keys)) for i, k := range s.keys { names[i] = k.name @@ -237,6 +278,7 @@ func (s *Section) KeyValues() map[string]string { defer s.f.mu.RUnlock() } + s = s.current() hash := make(map[string]string, len(s.keys)) for _, k := range s.keys { hash[k.name] = k.value @@ -254,6 +296,7 @@ func (s *Section) DeleteKey(name string) { defer s.f.mu.Unlock() } + s = s.current() if key := s.byName[name]; key != nil { delete(s.byName, name) s.keys = slices.DeleteFunc(s.keys, func(k *Key) bool { return k == key }) diff --git a/struct.go b/struct.go index 149906f..7183438 100644 --- a/struct.go +++ b/struct.go @@ -904,6 +904,9 @@ func (s *Section) ReflectFrom(v any) error { if typ.Kind() != reflect.Pointer || typ.Elem().Kind() != reflect.Struct { return errors.New("not a pointer to a struct") } + s.f.maybeLock() + s = s.attach() + s.f.maybeUnlock() return s.encodeStruct(val.Elem(), nil) } From 145371801f6f36c6d6ee89aafe9a246ec949f707 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Wed, 11 Nov 2026 16:28:22 +0000 Subject: [PATCH 2/4] Format the package doc with headings, links and lists Split the overview into sections with headings, link the identifiers it names, show a short loading example, and list the unchecked outputs. --- ini.go | 77 ++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/ini.go b/ini.go index cf5f779..09e7035 100644 --- a/ini.go +++ b/ini.go @@ -1,37 +1,60 @@ // Package ini reads and writes INI files. // -// Load parses one source, a file name, []byte or io.Reader, and LoadSources -// merges several, later ones overriding earlier ones. Without options, every -// input parses exactly as it did with gopkg.in/ini.v1. LoadOption values -// such as AllowShadows, CaseInsensitive or IgnoreInlineComment change the -// syntax that is accepted. +// # Loading // -// A File holds Sections in order, and each Section holds Keys in order. The -// default section, named DefaultSection, holds the keys before the first -// section header. Pass "" to refer to it. Key returns values as strings or -// parses them, as in Key.Int or Key.Duration, and the generic List, In and -// Range parse lists and candidates of any Scalar type, and ranges of any -// Ordered one. A value can -// refer to another key of its section or of the default section as +// [Load] parses one source: a file name, a []byte or an [io.Reader]. +// [LoadSources] merges several, later ones overriding earlier ones. +// +// f, err := ini.Load("app.ini") +// if err != nil { +// return err +// } +// port := f.Section("server").Key("port").MustInt(8080) +// +// Without options, every input parses exactly as it did with +// gopkg.in/ini.v1. [LoadOption] values such as [AllowShadows], +// [CaseInsensitive] and [IgnoreInlineComment] change the syntax that is +// accepted. +// +// # Sections and keys +// +// A [File] holds sections in order, and each [Section] holds keys in order. +// The default section, named [DefaultSection], holds the keys before the +// first section header. Pass "" to refer to it. +// +// [File.Section] and [Section.Key] return a section or key that reads as +// empty when there is none, and a change such as [Key.SetValue] adds it. +// +// A [Key] returns its value as a string or parses it, as in [Key.Int] or +// [Key.Duration]. The generic [List], [In] and [Range] parse lists and +// candidates of any [Scalar] type, and ranges of any [Ordered] one. A value +// can refer to another key of its section or of the default section as // %(name)s. // -// MapTo and StrictMapTo fill a struct from a data source or a File, and -// File.ReflectFrom fills a File from a struct. Field names map to key and -// section names unchanged unless NameMapping sets a mapper, and the ini, -// delim and comment struct tags adjust each field. A type that implements -// Marshaler or Unmarshaler formats or parses its own values. +// # Structs +// +// [MapTo] and [StrictMapTo] fill a struct from a data source or a [File], +// and [File.ReflectFrom] fills a File from a struct. Field names map to key +// and section names unchanged unless [NameMapping] sets a mapper, and the +// ini, delim and comment struct tags adjust each field. A type that +// implements [Marshaler] or [Unmarshaler] formats or parses its own values. +// +// # Writing +// +// [File.Write] and [File.SaveTo] write a File back with its comments and in +// its order, and [WriteOption] values set the formatting. What Write writes +// loads back to the same sections, keys, values and comment text, or Write +// returns an error. The spacing after a comment symbol can change. +// +// Output meant for other readers isn't checked: +// - keys written with a [WriteDelimiter] the file wasn't loaded with +// - boolean keys of a file loaded without [AllowBooleanKeys] +// - raw sections of a file loaded without [RawSections] for them // -// File.Write and File.SaveTo write a File back with its comments and in its -// order, and WriteOption values set the formatting. What Write writes loads -// back to the same sections, keys, values and comment text, or Write returns -// an error. The spacing after a comment symbol can change. -// Output meant for other readers isn't checked: keys written with a -// WriteDelimiter that the file wasn't loaded with, boolean keys of a file -// loaded without AllowBooleanKeys, and raw sections of a file loaded without -// RawSections for them. +// # Concurrency and errors // -// A File is safe for concurrent use unless loaded with Locking(false). -// Parse errors are *ParseError values that give the line and column. +// A File is safe for concurrent use unless loaded with [Locking](false). +// Parse errors are [*ParseError] values that give the line and column. package ini import ( From b358b7b8db7513a0234890e1ec2d602a3df38bdf Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Wed, 11 Nov 2026 11:43:20 -0500 Subject: [PATCH 3/4] Keep missing sections for different indexes apart A section SectionWithIndex added for one index no longer becomes the section a held handle for another index reads and changes. Also list NewBooleanKey in the File.Section doc, and note in the migration guide that setting a key only a parent section has no longer adds the child. --- docs/migrate-v1-to-v2.md | 2 +- file.go | 3 ++- file_test.go | 11 +++++++++++ section.go | 14 +++++++++----- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/docs/migrate-v1-to-v2.md b/docs/migrate-v1-to-v2.md index 43d79fc..d3ba877 100644 --- a/docs/migrate-v1-to-v2.md +++ b/docs/migrate-v1-to-v2.md @@ -28,7 +28,7 @@ In v1, `f.Section("server").Key("PORT")` added an empty `[server]` section and ` - a key: `SetValue` or `SetComment`, or `AddShadow` and `AddNestedValue` when the file allows shadows or nested values - a section: a change to one of its keys, `NewKey`, `NewBooleanKey`, `SetComment` or `ReflectFrom` -`f.Section(name).Key(key).SetValue(v)` works as before. `SectionWithIndex` follows the same rule, and a change adds the section after the last one of its name. +`f.Section(name).Key(key).SetValue(v)` works as before, except that when only a parent section has the key, it changes the parent's value without adding an empty child section. `SectionWithIndex` follows the same rule, and a change adds the section after the last one of its name. ### `Reload` and `Append` diff --git a/file.go b/file.go index 96b7cfd..1e81438 100644 --- a/file.go +++ b/file.go @@ -193,7 +193,8 @@ func (f *File) sectionsByName(name string) ([]*Section, error) { // Section returns the section with the given name, or the default section // for "". When there is none, it returns a section that reads as empty // without adding it to the file. Section.SetComment, Section.NewKey, -// Section.ReflectFrom or a change to one of its keys adds it. +// Section.NewBooleanKey, Section.ReflectFrom or a change to one of its keys +// adds it. func (f *File) Section(name string) *Section { if sec, err := f.GetSection(name); err == nil { return sec diff --git a/file_test.go b/file_test.go index f688b24..2b955e7 100644 --- a/file_test.go +++ b/file_test.go @@ -372,6 +372,17 @@ VERSION = v1`), CaseInsensitive(true)) require.Len(t, secs, 2) assert.Same(t, sec, secs[1]) + h, err := Load([]byte(""), AllowNonUniqueSections(true)) + require.NoError(t, err) + h0, h1 := h.SectionWithIndex("Peer", 0), h.SectionWithIndex("Peer", 1) + h1.Key("k").SetValue("1") + h0.Key("k").SetValue("0") + secs, err = h.SectionsByName("Peer") + require.NoError(t, err) + require.Len(t, secs, 2, "a section added for one index isn't another index's") + assert.Equal(t, "1", h1.Key("k").String()) + assert.Equal(t, "0", h0.Key("k").String()) + g, err := Load([]byte("[Peer]\nk = 0\n")) require.NoError(t, err) assert.Same(t, g.Section("Peer"), g.SectionWithIndex("Peer", 1), "without AllowNonUniqueSections") diff --git a/section.go b/section.go index 008b100..265a94e 100644 --- a/section.go +++ b/section.go @@ -22,8 +22,10 @@ type Section struct { // missing is set on a section File.Section or File.SectionWithIndex // returned for a name, or an index of it, the file doesn't have. The // section isn't in the file until a change adds it. index is the index - // of the name it stands for. + // of the name it stands for. added is set once a change adds it, so a + // missing section for another index doesn't take it for its own. missing bool + added bool index int } @@ -37,12 +39,14 @@ func newSection(f *File, name string) *Section { } // current returns the section s reads and changes. That is s, unless s is -// missing and its file has since gained a section of its name and index. -// The caller holds the lock. +// missing and its file has since gained a section of its name and index, +// other than one added for another index. The caller holds the lock. func (s *Section) current() *Section { if s.missing { if secs := s.f.byName[s.name]; s.index >= 0 && s.index < len(secs) { - return secs[s.index] + if c := secs[s.index]; !c.added || c.index == s.index { + return c + } } } return s @@ -54,7 +58,7 @@ func (s *Section) current() *Section { func (s *Section) attach() *Section { c := s.current() if c == s && s.missing { - s.missing = false + s.missing, s.added = false, true s.f.sections = append(s.f.sections, s) s.f.byName[s.name] = append(s.f.byName[s.name], s) } From b0f853c8683c768d5978a0b8428385f8f68432f6 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Wed, 11 Nov 2026 18:35:17 +0000 Subject: [PATCH 4/4] Move the package doc to doc.go --- doc.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ini.go | 57 --------------------------------------------------------- 2 files changed, 58 insertions(+), 57 deletions(-) create mode 100644 doc.go diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..d65a548 --- /dev/null +++ b/doc.go @@ -0,0 +1,58 @@ +// Package ini reads and writes INI files. +// +// # Loading +// +// [Load] parses one source: a file name, a []byte or an [io.Reader]. +// [LoadSources] merges several, later ones overriding earlier ones. +// +// f, err := ini.Load("app.ini") +// if err != nil { +// return err +// } +// port := f.Section("server").Key("port").MustInt(8080) +// +// Without options, every input parses exactly as it did with +// gopkg.in/ini.v1. [LoadOption] values such as [AllowShadows], +// [CaseInsensitive] and [IgnoreInlineComment] change the syntax that is +// accepted. +// +// # Sections and keys +// +// A [File] holds sections in order, and each [Section] holds keys in order. +// The default section, named [DefaultSection], holds the keys before the +// first section header. Pass "" to refer to it. +// +// [File.Section] and [Section.Key] return a section or key that reads as +// empty when there is none, and a change such as [Key.SetValue] adds it. +// +// A [Key] returns its value as a string or parses it, as in [Key.Int] or +// [Key.Duration]. The generic [List], [In] and [Range] parse lists and +// candidates of any [Scalar] type, and ranges of any [Ordered] one. A value +// can refer to another key of its section or of the default section as +// %(name)s. +// +// # Structs +// +// [MapTo] and [StrictMapTo] fill a struct from a data source or a [File], +// and [File.ReflectFrom] fills a File from a struct. Field names map to key +// and section names unchanged unless [NameMapping] sets a mapper, and the +// ini, delim and comment struct tags adjust each field. A type that +// implements [Marshaler] or [Unmarshaler] formats or parses its own values. +// +// # Writing +// +// [File.Write] and [File.SaveTo] write a File back with its comments and in +// its order, and [WriteOption] values set the formatting. What Write writes +// loads back to the same sections, keys, values and comment text, or Write +// returns an error. The spacing after a comment symbol can change. +// +// Output meant for other readers isn't checked: +// - keys written with a [WriteDelimiter] the file wasn't loaded with +// - boolean keys of a file loaded without [AllowBooleanKeys] +// - raw sections of a file loaded without [RawSections] for them +// +// # Concurrency and errors +// +// A File is safe for concurrent use unless loaded with [Locking](false). +// Parse errors are [*ParseError] values that give the line and column. +package ini diff --git a/ini.go b/ini.go index 09e7035..42a7d36 100644 --- a/ini.go +++ b/ini.go @@ -1,60 +1,3 @@ -// Package ini reads and writes INI files. -// -// # Loading -// -// [Load] parses one source: a file name, a []byte or an [io.Reader]. -// [LoadSources] merges several, later ones overriding earlier ones. -// -// f, err := ini.Load("app.ini") -// if err != nil { -// return err -// } -// port := f.Section("server").Key("port").MustInt(8080) -// -// Without options, every input parses exactly as it did with -// gopkg.in/ini.v1. [LoadOption] values such as [AllowShadows], -// [CaseInsensitive] and [IgnoreInlineComment] change the syntax that is -// accepted. -// -// # Sections and keys -// -// A [File] holds sections in order, and each [Section] holds keys in order. -// The default section, named [DefaultSection], holds the keys before the -// first section header. Pass "" to refer to it. -// -// [File.Section] and [Section.Key] return a section or key that reads as -// empty when there is none, and a change such as [Key.SetValue] adds it. -// -// A [Key] returns its value as a string or parses it, as in [Key.Int] or -// [Key.Duration]. The generic [List], [In] and [Range] parse lists and -// candidates of any [Scalar] type, and ranges of any [Ordered] one. A value -// can refer to another key of its section or of the default section as -// %(name)s. -// -// # Structs -// -// [MapTo] and [StrictMapTo] fill a struct from a data source or a [File], -// and [File.ReflectFrom] fills a File from a struct. Field names map to key -// and section names unchanged unless [NameMapping] sets a mapper, and the -// ini, delim and comment struct tags adjust each field. A type that -// implements [Marshaler] or [Unmarshaler] formats or parses its own values. -// -// # Writing -// -// [File.Write] and [File.SaveTo] write a File back with its comments and in -// its order, and [WriteOption] values set the formatting. What Write writes -// loads back to the same sections, keys, values and comment text, or Write -// returns an error. The spacing after a comment symbol can change. -// -// Output meant for other readers isn't checked: -// - keys written with a [WriteDelimiter] the file wasn't loaded with -// - boolean keys of a file loaded without [AllowBooleanKeys] -// - raw sections of a file loaded without [RawSections] for them -// -// # Concurrency and errors -// -// A File is safe for concurrent use unless loaded with [Locking](false). -// Parse errors are [*ParseError] values that give the line and column. package ini import (