Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
@@ -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
9 changes: 7 additions & 2 deletions docs/migrate-v1-to-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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`

Expand Down
42 changes: 27 additions & 15 deletions file.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,30 +190,42 @@ 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.NewBooleanKey, 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
}

Expand Down
96 changes: 96 additions & 0 deletions file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,102 @@ 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])

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")
})
}

func TestFile_Sections(t *testing.T) {
Expand Down
34 changes: 0 additions & 34 deletions ini.go
Original file line number Diff line number Diff line change
@@ -1,37 +1,3 @@
// 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.
//
// 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
// %(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.
//
// 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.
//
// 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 (
Expand Down
12 changes: 7 additions & 5 deletions key.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,24 @@ 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
}
}
return k
}

// 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
}
Expand Down
Loading
Loading