diff --git a/go.mod b/go.mod index 91a884582..cda1c6704 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 require github.com/openshift/ptp-operator v0.0.0-20230207052655-ede9197d99ca require ( + github.com/bigkevmcd/go-configparser v0.0.0-20240808124832-fc81059ea0bd github.com/facebook/time v0.0.0-20230529151911-512b3b30ab23 github.com/golang/glog v1.0.0 github.com/google/goexpect v0.0.0-20210430020637-ab937bf7fd6f @@ -19,6 +20,7 @@ require ( k8s.io/apimachinery v0.25.4 k8s.io/client-go v0.25.4 k8s.io/utils v0.0.0-20221012122500-cfd413dd9e85 + sigs.k8s.io/yaml v1.3.0 ) require ( @@ -88,5 +90,4 @@ require ( sigs.k8s.io/controller-runtime v0.13.0 // indirect sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index 71c7d5946..11cea931d 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bigkevmcd/go-configparser v0.0.0-20240808124832-fc81059ea0bd h1:MsTk4yo6KVYdulsDscuH4AwiZN1CyuCJAg59EWE7HPQ= +github.com/bigkevmcd/go-configparser v0.0.0-20240808124832-fc81059ea0bd/go.mod h1:vzEQfW+A1T+AMJmTIX+SXNLNECHOM7GEinHhw0IjykI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= @@ -217,6 +219,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -247,7 +251,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -631,8 +634,9 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= diff --git a/pkg/daemon/daemon.go b/pkg/daemon/daemon.go index e0e60f724..ff1d7cd2f 100644 --- a/pkg/daemon/daemon.go +++ b/pkg/daemon/daemon.go @@ -17,6 +17,7 @@ import ( "github.com/openshift/linuxptp-daemon/pkg/config" "github.com/openshift/linuxptp-daemon/pkg/dpll" + "github.com/openshift/linuxptp-daemon/pkg/leap" "github.com/openshift/linuxptp-daemon/pkg/event" ptpnetwork "github.com/openshift/linuxptp-daemon/pkg/network" @@ -456,6 +457,7 @@ func (dn *Daemon) applyNodePtpProfile(runID int, nodeProfile *ptpv1.PtpProfile) configFile = fmt.Sprintf("ts2phc.%d.config", runID) configPath = fmt.Sprintf("/var/run/%s", configFile) messageTag = fmt.Sprintf("[ts2phc.%d.config:{level}]", runID) + leap.LeapMgr.SetPtp4lConfigPath(fmt.Sprintf("ptp4l.%d.config", runID)) } if configOpts == nil || *configOpts == "" { @@ -887,9 +889,11 @@ func (p *ptpProcess) cmdRun(stdoutToSocket bool) { // for ts2phc along with processing metrics need to identify event func (p *ptpProcess) processPTPMetrics(output string) { - if p.name == ts2phcProcessName && (strings.Contains(output, NMEASourceDisabledIndicator) || - strings.Contains(output, InvalidMasterTimestampIndicator) || - strings.Contains(output, NMEASourceDisabledIndicator2)) { //TODO identify which interface lost nmea or 1pps + if p.name == ts2phcProcessName && + !leap.LeapMgr.IsLeapInWindow(time.Now().UTC(), -2*time.Second, time.Second) && + (strings.Contains(output, NMEASourceDisabledIndicator) || + strings.Contains(output, InvalidMasterTimestampIndicator) || + strings.Contains(output, NMEASourceDisabledIndicator2)) { //TODO identify which interface lost nmea or 1pps iface := p.ifaces.GetGMInterface().Name p.ProcessTs2PhcEvents(faultyOffset, ts2phcProcessName, iface, map[event.ValueType]interface{}{event.NMEA_STATUS: int64(0)}) glog.Error("nmea string lost") //TODO: add for 1pps lost diff --git a/pkg/daemon/daemon_test.go b/pkg/daemon/daemon_test.go index 8fbc96177..df4dbbe7f 100644 --- a/pkg/daemon/daemon_test.go +++ b/pkg/daemon/daemon_test.go @@ -3,14 +3,16 @@ package daemon_test import ( "flag" "fmt" - ptpv1 "github.com/openshift/ptp-operator/api/v1" - "k8s.io/utils/pointer" "os" "strings" "testing" + ptpv1 "github.com/openshift/ptp-operator/api/v1" + "k8s.io/utils/pointer" + "github.com/openshift/linuxptp-daemon/pkg/config" "github.com/openshift/linuxptp-daemon/pkg/daemon" + "github.com/openshift/linuxptp-daemon/pkg/leap" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/assert" @@ -231,7 +233,8 @@ func TestMain(m *testing.M) { os.Exit(code) } func Test_ProcessPTPMetrics(t *testing.T) { - + assert.NoError(t, leap.MockLeapFile()) + defer close(leap.LeapMgr.Close) assert := assert.New(t) for _, tc := range testCases { tc.node = MYNODE diff --git a/pkg/event/event_test.go b/pkg/event/event_test.go index bb5fa480c..efd808171 100644 --- a/pkg/event/event_test.go +++ b/pkg/event/event_test.go @@ -207,7 +207,8 @@ func TestEventHandler_ProcessEvents(t *testing.T) { eventManager := event.Init("node", true, "/tmp/go.sock", eChannel, closeChn, nil, nil, nil) eventManager.MockEnable() go eventManager.ProcessEvents() - assert.NoError(t, mockLeap()) + assert.NoError(t, leap.MockLeapFile()) + defer close(leap.LeapMgr.Close) time.Sleep(1 * time.Second) for _, test := range tests { select { diff --git a/pkg/leap/leap-file.go b/pkg/leap/leap-file.go index 5f0b3364e..038efbae2 100644 --- a/pkg/leap/leap-file.go +++ b/pkg/leap/leap-file.go @@ -15,9 +15,12 @@ import ( "time" "github.com/golang/glog" + "github.com/openshift/linuxptp-daemon/pkg/pmc" "github.com/openshift/linuxptp-daemon/pkg/ublox" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + fake "k8s.io/client-go/kubernetes/fake" ) const ( @@ -29,6 +32,8 @@ const ( leapSourceGps = 2 leapConfigMapName = "leap-configmap" MaintenancePeriod = time.Minute * 1 + pmcWindowStartHours = 12 + pmcWindowEndSeconds = 60 ) type LeapManager struct { @@ -49,8 +54,10 @@ type LeapManager struct { leapFilePath string leapFileName string // UTC offset and its validity time - utcOffset int - utcOffsetTime time.Time + utcOffset int + utcOffsetTime time.Time + ptp4lConfigPath string + pmcLeapSent bool } type LeapEvent struct { @@ -93,27 +100,6 @@ func New(kubeclient kubernetes.Interface, namespace string) (*LeapManager, error return LeapMgr, nil } -func (l *LeapManager) Run() { - glog.Info("starting Leap file manager") - ticker := time.NewTicker(MaintenancePeriod) - defer ticker.Stop() - for { - select { - case v := <-l.UbloxLsInd: - l.handleLeapIndication(&v) - case <-l.Close: - LeapMgr = nil - return - case <-ticker.C: - if l.retryUpdate { - l.updateLeapConfigmap() - } - // TODO: if current time is within -12h ... +60s from leap event: - // Send PMC command - } - } -} - func GetUtcOffset() int { if LeapMgr != nil { if time.Now().UTC().After(LeapMgr.utcOffsetTime) { @@ -172,6 +158,11 @@ func parseLeapFile(b []byte) (*LeapFile, error) { return &l, nil } +func (l *LeapManager) SetPtp4lConfigPath(path string) { + glog.Info("set Leap manager ptp4l config file name to ", path) + l.ptp4lConfigPath = path +} + func (l *LeapManager) renderLeapData() (*bytes.Buffer, error) { templateStr := `# Do not edit # This file is generated automatically by linuxptp-daemon @@ -246,6 +237,55 @@ func (l *LeapManager) populateLeapData() error { return nil } +func (l *LeapManager) Run() { + glog.Info("starting Leap file manager") + ticker := time.NewTicker(MaintenancePeriod) + for { + select { + case v := <-l.UbloxLsInd: + l.handleLeapIndication(&v) + case <-l.Close: + LeapMgr = nil + return + case <-ticker.C: + if l.retryUpdate { + l.updateLeapConfigmap() + } + if l.IsLeapInWindow(time.Now().UTC(), -pmcWindowStartHours*time.Hour, -pmcWindowEndSeconds*time.Second) { + if !l.pmcLeapSent { + g, err := pmc.RunPMCExpGetGMSettings(l.ptp4lConfigPath) + if err != nil { + glog.Error("error in Leap:", err) + continue + } + leapDiff := l.leapFile.LeapEvents[len(l.leapFile.LeapEvents)-1].LeapSec - int(g.TimePropertiesDS.CurrentUtcOffset) + if leapDiff > 0 { + g.TimePropertiesDS.Leap59 = false + g.TimePropertiesDS.Leap61 = true + } else if leapDiff < 0 { + g.TimePropertiesDS.Leap59 = true + g.TimePropertiesDS.Leap61 = false + } else { + // No actual change in leap seconds, don't send anything + l.pmcLeapSent = true + continue + } + glog.Info("Sending PMC command in Leap window") + glog.Infof("Leap time properties: %++v", g.TimePropertiesDS) + err = pmc.RunPMCExpSetGMSettings(l.ptp4lConfigPath, g) + if err != nil { + glog.Error("failed to send PMC for Leap: ", err) + continue + } + l.pmcLeapSent = true + } + } else { + l.pmcLeapSent = false + } + } + } +} + // updateLeapFile updates a new leap event to the list of leap events, if provided func (l *LeapManager) updateLeapFile(leapTime time.Time, leapSec int, currentTime time.Time) { @@ -366,7 +406,8 @@ func (l *LeapManager) processLeapIndication(data *ublox.TimeLs) (*leapIndResult, return nil, fmt.Errorf("failed to parse time duration: Leap: %v", err) } if data.LsChange == 0 && data.TimeToLsEvent >= 0 { - result.leapTime = currentTime + // shift leap date out of pmc window, so no pmc commands will be sent + result.leapTime = currentTime.Add(-pmcWindowEndSeconds * time.Second) } else { result.leapTime = gpsStartTime.Add(deltaHours) } @@ -377,3 +418,42 @@ func (l *LeapManager) processLeapIndication(data *ublox.TimeLs) (*leapIndResult, } return nil, nil } + +// IsLeapInWindow() returns whether a leap event is occuring within the specified time window from now +func (l *LeapManager) IsLeapInWindow(now time.Time, startOffset, endOffset time.Duration) bool { + startTime := time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC) + lastLeap := l.leapFile.LeapEvents[len(l.leapFile.LeapEvents)-1] + lastLeapTime, err := strconv.Atoi(lastLeap.LeapTime) + if err != nil { + return false + } + leapTime := startTime.Add(time.Second * time.Duration(lastLeapTime)) + leapWindowStart := leapTime.Add(startOffset) + leapWindowEnd := leapTime.Add(endOffset) + if now.After(leapWindowStart) && now.Before(leapWindowEnd) { + glog.Info("Leap in window: ", startOffset, " ", endOffset) + return true + } + return false +} + +func MockLeapFile() error { + cm := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Namespace: "openshift-ptp", Name: "leap-configmap"}, + Data: map[string]string{ + "test-node-name": `# Do not edit +# This file is generated automatically by linuxptp-daemon +#$ 3927775672 +#@ 4291747200 +3692217600 37 # 1 Jan 2017`, + }, + } + os.Setenv("NODE_NAME", "test-node-name") + client := fake.NewSimpleClientset(cm) + lm, err := New(client, "openshift-ptp") + if err != nil { + return err + } + go lm.Run() + return nil +} diff --git a/pkg/leap/leap-file_test.go b/pkg/leap/leap-file_test.go index 1ca6300f2..3051236a6 100644 --- a/pkg/leap/leap-file_test.go +++ b/pkg/leap/leap-file_test.go @@ -131,7 +131,7 @@ func Test_processLeapIndication_MissedLeapZero(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, res) assert.WithinDuration(t, time.Now().UTC(), res.updateTime, 1*time.Second) - assert.WithinDuration(t, res.leapTime, time.Now().UTC(), 1*time.Second) + assert.WithinDuration(t, res.leapTime, time.Now().UTC().Add(-60*time.Second), 1*time.Second) assert.Equal(t, int(ind.CurrLs+ind.LsChange+19), res.leapSec) } @@ -300,6 +300,50 @@ func Test_handleLeapIndication(t *testing.T) { assert.Equal(t, "4291747200", lm.leapFile.ExpirationTime) } +func Test_IsLeapInWindow_Pos(t *testing.T) { + now := time.Now().UTC() + startTime := time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC) + diff := now.Sub(startTime) + leapTime := fmt.Sprintf("%d", int(diff.Seconds())) + lm := &LeapManager{ + + leapFile: LeapFile{ + LeapEvents: []LeapEvent{ + { + LeapTime: leapTime, + }, + }, + }, + } + res := lm.IsLeapInWindow(now, -1*time.Second, time.Second) + assert.True(t, res) +} +func Test_IsLeapInWindow_Neg(t *testing.T) { + now := time.Now().UTC() + startTime := time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC) + diff := now.Sub(startTime) + leapTime := fmt.Sprintf("%d", int(diff.Seconds())-1) + lm := &LeapManager{ + + leapFile: LeapFile{ + LeapEvents: []LeapEvent{ + { + LeapTime: leapTime, + }, + }, + }, + } + res := lm.IsLeapInWindow(now, -1*time.Second, time.Second) + assert.False(t, res) +} + +func Test_SetPtp4lConfigPath(t *testing.T) { + path := "test" + lm := &LeapManager{} + lm.SetPtp4lConfigPath(path) + assert.Equal(t, path, lm.ptp4lConfigPath) +} + func Test_New_Good(t *testing.T) { cm := &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{Namespace: "openshift-ptp", Name: "leap-configmap"}, diff --git a/vendor/github.com/bigkevmcd/go-configparser/LICENSE b/vendor/github.com/bigkevmcd/go-configparser/LICENSE new file mode 100644 index 000000000..942e3a3d9 --- /dev/null +++ b/vendor/github.com/bigkevmcd/go-configparser/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013-15 Kevin McDermott + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE diff --git a/vendor/github.com/bigkevmcd/go-configparser/README.md b/vendor/github.com/bigkevmcd/go-configparser/README.md new file mode 100644 index 000000000..cf9520d44 --- /dev/null +++ b/vendor/github.com/bigkevmcd/go-configparser/README.md @@ -0,0 +1,135 @@ +# go-configparser [![Go](https://github.com/bigkevmcd/go-configparser/actions/workflows/go.yml/badge.svg)](https://github.com/bigkevmcd/go-configparser/actions/workflows/go.yml) +Go implementation of the Python ConfigParser class. + +This can parse Python-compatible ConfigParser config files, including support for option interpolation. + +## Setup +```Go + import ( + "github.com/bigkevmcd/go-configparser" + ) +``` + +## Parsing configuration files +It's easy to parse a configuration file. +```Go + p, err := configparser.NewConfigParserFromFile("example.cfg") + if err != nil { + ... + } +``` + +## Methods +The ConfigParser implements most of the Python ConfigParser API +```Go + v, err := p.Get("section", "option") + err = p.Set("section", "newoption", "value") + + s := p.Sections() +``` + +## Interpolation +The ConfigParser implements interpolation in the same format as the Python implementation. + +Given the configuration + +``` + [DEFAULTS] + dir: testing + + [testing] + something: %(dir)s/whatever +``` + +```Go + v, err := p.GetInterpolated("testing, something") +``` + +It's also possible to override the values to use when interpolating values by providing a Dict to lookup values in. +``` + d := make(configparser.Dict) + d["dir"] = "/a/non/existent/path" + result, err := p.GetInterpolatedWithVars("testing", "something", d) +``` + +Will get ```testing/whatever``` as the value + +## Options +The ConfigParser supports almost all custom options available in the Python version. + +* Delimiters - allows to set custom **key-value** pair delimiters. +* CommentPrefixes - allows to set custom comment line prefix. If line starts with one of the given `Prefixes` it will be passed during parsing. +* InlineCommentPrefixes - allows to set custom inline comment delimiter. This option checks if the line contains any of the given `Prefixes` and if so, splits the string by the prefix and returns the 0 index of the slice. +* MultilinePrefixes - allows to set custom multiline values prefixes. This option checks if the line starts with one of the given `Prefixes` and if so, counts it as a part of the current value. +* Strict - if set to `true`, parser will return new wrapped `ErrAlreadyExist` for duplicates of *sections* or *options* in one source. +* AllowEmptyLines - if set to `true` allows multiline values to include empty lines as their part. Otherwise the value will be parsed until an empty line or the line which does not start with one of the allowed multiline prefixes. +* Interpolation - allows to set custom behaviour for values interpolation. Interface was added, which defaults to `chainmap.ChainMap` instance. +```go +type Interpolator interface { + Add(...chainmap.Dict) + Len() int + Get(string) string +} +``` +* Converters - allows to set custom values parsers. +```go +type ConvertFunc func(string) (any, error) +``` +`ConvertFunc` can modify requested value if needed e.g., +```go +package main + +import ( + "fmt" + "strings" + + "github.com/bigkevmcd/go-configparser" +) + +func main() { + stringConv := func(s string) (any, error) { + return s + "_updated", nil + } + + conv := configparser.Converter{ + configparser.String: stringConv, + } + + p, err := configparser.ParseReaderWithOptions( + strings.NewReader("[section]\noption=value\n\n"), + configparser.Converters(conv), + ) + // handle err + + v, err := p.Get("section", "option") + // handle err + + fmt.Println(v == "value_updated") // true +} +``` +Those functions triggered inside `ConfigParser.Get*` methods if presented and wraps the return value. +> NOTE: Since `ConvertFunc` returns `any`, the caller should guarantee type assertion to the requested type after custom processing! +```go +type Converter map[string]ConvertFunc +``` +`Converter` is a `map` type, which supports *int* (for `int64`), *string*, *bool*, *float* (for `float64`) keys. + +--- +Default options, which are always preset: +```go +func defaultOptions() *options { + return &options{ + interpolation: chainmap.New(), + defaultSection: defaultSectionName, + delimiters: ":=", + commentPrefixes: Prefixes{"#", ";"}, + multilinePrefixes: Prefixes{"\t", " "}, + converters: Converter{ + StringConv: defaultGet, + IntConv: defaultGetInt64, + FloatConv: defaultGetFloat64, + BoolConv: defaultGetBool, + }, + } +} +``` diff --git a/vendor/github.com/bigkevmcd/go-configparser/TODO.md b/vendor/github.com/bigkevmcd/go-configparser/TODO.md new file mode 100644 index 000000000..7847942ce --- /dev/null +++ b/vendor/github.com/bigkevmcd/go-configparser/TODO.md @@ -0,0 +1,6 @@ +* Fixup RemoveOption +* Improve godoc +* Implement saving of files +* ItemsInterpolatedWithVars implemented +* Continuations +* ItemsWithDefaults? \ No newline at end of file diff --git a/vendor/github.com/bigkevmcd/go-configparser/chainmap/chainmap.go b/vendor/github.com/bigkevmcd/go-configparser/chainmap/chainmap.go new file mode 100644 index 000000000..43fa9f83f --- /dev/null +++ b/vendor/github.com/bigkevmcd/go-configparser/chainmap/chainmap.go @@ -0,0 +1,42 @@ +package chainmap + +// Dict is a simple string->string map. +type Dict map[string]string + +// ChainMap contains a slice of Dicts for interpolation values. +type ChainMap struct { + maps []Dict +} + +// New creates a new ChainMap. +func New(dicts ...Dict) *ChainMap { + chainMap := &ChainMap{ + maps: make([]Dict, 0), + } + chainMap.maps = append(chainMap.maps, dicts...) + + return chainMap +} + +// Add adds given dicts to the ChainMap. +func (c *ChainMap) Add(dicts ...Dict) { + c.maps = append(c.maps, dicts...) +} + +// Len returns the ammount of Dicts in the ChainMap. +func (c *ChainMap) Len() int { + return len(c.maps) +} + +// Get gets the last value with the given key from the ChainMap. +// If key does not exist returns empty string. +func (c *ChainMap) Get(key string) string { + var value string + + for _, dict := range c.maps { + if result, present := dict[key]; present { + value = result + } + } + return value +} diff --git a/vendor/github.com/bigkevmcd/go-configparser/configparser.go b/vendor/github.com/bigkevmcd/go-configparser/configparser.go new file mode 100644 index 000000000..988f45c60 --- /dev/null +++ b/vendor/github.com/bigkevmcd/go-configparser/configparser.go @@ -0,0 +1,316 @@ +package configparser + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "os" + "regexp" + "sort" + "strings" + "unicode" +) + +var ( + sectionHeader = regexp.MustCompile(`^\[([^]]+)\]`) + interpolater = regexp.MustCompile(`%\(([^)]*)\)s`) +) + +var boolMapping = map[string]bool{ + "1": true, + "true": true, + "on": true, + "yes": true, + "0": false, + "false": false, + "off": false, + "no": false, +} + +// Dict is a simple string->string map. +type Dict map[string]string + +// Config represents a Python style configuration file. +type Config map[string]*Section + +// ConfigParser ties together a Config and default values for use in +// interpolated configuration values. +type ConfigParser struct { + config Config + defaults *Section + opt *options +} + +// Keys returns a sorted slice of keys +func (d Dict) Keys() []string { + keys := make([]string, 0, len(d)) + + for key := range d { + keys = append(keys, key) + } + sort.Strings(keys) + + return keys +} + +func getNoSectionError(section string) error { + return fmt.Errorf("no section: %q", section) +} + +func getNoOptionError(section, option string) error { + return fmt.Errorf("no option %q in section: %q", option, section) +} + +// New creates a new ConfigParser. +func New() *ConfigParser { + return &ConfigParser{ + config: make(Config), + defaults: newSection(defaultSectionName), + opt: defaultOptions(), + } +} + +// NewWithOptions creates a new ConfigParser with options. +func NewWithOptions(opts ...optFunc) *ConfigParser { + opt := defaultOptions() + for _, fn := range opts { + fn(opt) + } + + return &ConfigParser{ + config: make(Config), + defaults: newSection(opt.defaultSection), + opt: opt, + } +} + +// NewWithDefaults allows creation of a new ConfigParser with a pre-existing Dict. +func NewWithDefaults(defaults Dict) (*ConfigParser, error) { + p := New() + for key, value := range defaults { + if err := p.defaults.Add(key, value); err != nil { + return nil, fmt.Errorf("failed to add %q to %q: %w", key, value, err) + } + } + return p, nil +} + +// NewConfigParserFromFile creates a new ConfigParser struct populated from the +// supplied filename. +func NewConfigParserFromFile(filename string) (*ConfigParser, error) { + p, err := Parse(filename) + if err != nil { + return nil, err + } + return p, nil +} + +// ParseReader parses a ConfigParser from the provided input. +func ParseReader(in io.Reader) (*ConfigParser, error) { + p := New() + err := p.ParseReader(in) + + return p, err +} + +// ParseReaderWithOptions parses a ConfigParser from the provided input with given options. +func ParseReaderWithOptions(in io.Reader, opts ...optFunc) (*ConfigParser, error) { + p := NewWithOptions(opts...) + err := p.ParseReader(in) + + return p, err +} + +// Parse takes a filename and parses it into a ConfigParser value. +func Parse(filename string) (*ConfigParser, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + p, err := ParseReader(file) + if err != nil { + return nil, err + } + return p, nil +} + +// ParseWithOptions takes a filename and parses it into a ConfigParser value with given options. +func ParseWithOptions(filename string, opts ...optFunc) (*ConfigParser, error) { + p := NewWithOptions(opts...) + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + err = p.ParseReader(bytes.NewReader(data)) + return p, err +} + +func writeSection(file *os.File, delimiter string, section *Section) error { + _, err := file.WriteString(fmt.Sprintf("[%s]\n", section.Name)) + if err != nil { + return err + } + + for _, option := range section.Options() { + _, err = file.WriteString(fmt.Sprintf("%s %s %s\n", option, delimiter, section.options[option])) + if err != nil { + return err + } + } + _, err = file.WriteString("\n") + return err +} + +// SaveWithDelimiter writes the current state of the ConfigParser to the named +// file with the specified delimiter. +func (p *ConfigParser) SaveWithDelimiter(filename, delimiter string) error { + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + if len(p.defaults.Options()) > 0 { + err = writeSection(f, delimiter, p.defaults) + if err != nil { + return err + } + } + + for _, s := range p.Sections() { + err = writeSection(f, delimiter, p.config[s]) + if err != nil { + return err + } + } + + return nil +} + +// ParseReader parses data into ConfigParser from provided reader. +func (p *ConfigParser) ParseReader(in io.Reader) error { + reader := bufio.NewReader(in) + var lineNo int + var curSect *Section + var key, value string + + keyValue := regexp.MustCompile( + fmt.Sprintf( + `([^%[1]s\s][^%[1]s]*)\s*(?P[%[1]s]+)\s*(.*)$`, + p.opt.delimiters, + ), + ) + keyWNoValue := regexp.MustCompile( + fmt.Sprintf( + `([^%[1]s\s][^%[1]s]*)\s*((?P[%[1]s]+)\s*(.*)$)?`, + p.opt.delimiters, + ), + ) + + for { + l, _, err := reader.ReadLine() + if err != nil { + // If error is end of file, then current key should be checked before return. + if errors.Is(err, io.EOF) { + if key != "" { + if err := curSect.Add(key, value); err != nil { + return fmt.Errorf("failed to add %q = %q: %w", key, value, err) + } + } + + return nil + } + + return err + } + lineNo++ + + // Ensures regex will match and get copy of the line without space characters. + line := strings.TrimFunc(string(l), unicode.IsSpace) + + // Skip comment lines. + if p.opt.commentPrefixes.HasPrefix(line) { + continue + } + + // Check if key-value pair is currently in parsing process. + if key != "" { + if p.opt.multilinePrefixes.HasPrefix(string(l)) || + (line == "" && p.opt.emptyLines) { + // If current key was defined and line starts with one of the + // multiline prefixes or it is an empty string which is allowed within values, + // then adding this line to the value. + if curSect == nil { + return fmt.Errorf("missing section header: %d %s", lineNo, line) + } + + value += "\n" + p.opt.inlineCommentPrefixes.Split(line) + // If current line is added as a value part, may continue. + continue + } else { + // If key was defined, but current line does not start with any of the + // multiline prefixes or it is an empty line which is not allowed within values, + // then it counts as the value parsing is finished and it can be added + // to the current section. + if err := curSect.Add(key, value); err != nil { + return fmt.Errorf("failed to add %q = %q: %w", key, value, err) + } + + // Drop key-value pair to empty strings. + key, value = "", "" + } + } + + // If key was not defined and current line is empty it can be skipped. + if line == "" { + continue + } + + if match := sectionHeader.FindStringSubmatch(line); len(match) > 0 { + section := p.opt.inlineCommentPrefixes.Split(match[1]) + if section == p.opt.defaultSection { + curSect = p.defaults + } else if _, present := p.config[section]; !present { + curSect = newSection(section) + p.config[section] = curSect + } else if p.opt.strict { + return fmt.Errorf( + "section %q already exists and strict flag was set", section, + ) + } + + // Since section was defined on current line, may continue. + continue + } + + if match := keyValue.FindStringSubmatch(line); len(match) > 0 { + if curSect == nil { + return fmt.Errorf("missing section header: %d %s", lineNo, line) + } + key = strings.TrimSpace(match[1]) + if p.opt.strict { + if err := p.inOptions(key); err != nil { + return err + } + } + + value = p.opt.inlineCommentPrefixes.Split(match[3]) + } else if match = keyWNoValue.FindStringSubmatch(line); len(match) > 0 && + p.opt.allowNoValue { + if curSect == nil { + return fmt.Errorf("missing section header: %d %s", lineNo, line) + } + key = strings.TrimSpace(match[1]) + if p.opt.strict { + if err := p.inOptions(key); err != nil { + return err + } + } + + value = p.opt.inlineCommentPrefixes.Split(match[4]) + } + } +} diff --git a/vendor/github.com/bigkevmcd/go-configparser/interpolation.go b/vendor/github.com/bigkevmcd/go-configparser/interpolation.go new file mode 100644 index 000000000..101055ca7 --- /dev/null +++ b/vendor/github.com/bigkevmcd/go-configparser/interpolation.go @@ -0,0 +1,79 @@ +package configparser + +import ( + "strings" + + "github.com/bigkevmcd/go-configparser/chainmap" +) + +const maxInterpolationDepth int = 10 + +func (p *ConfigParser) getInterpolated(section, option string, c Interpolator) (string, error) { + val, err := p.get(section, option) + if err != nil { + return "", err + } + return p.interpolate(val, c), nil +} + +// GetInterpolated returns a string value for the named option. +// +// All % interpolations are expanded in the return values, based on +// the defaults passed into the constructor and the DEFAULT section. +func (p *ConfigParser) GetInterpolated(section, option string) (string, error) { + o, err := p.Items(section) + if err != nil { + return "", err + } + p.opt.interpolation.Add(chainmap.Dict(p.Defaults()), chainmap.Dict(o)) + return p.getInterpolated(section, option, p.opt.interpolation) +} + +// GetInterpolatedWithVars returns a string value for the named option. +// +// All % interpolations are expanded in the return values, based on the defaults passed +// into the constructor and the DEFAULT section. Additional substitutions may be +// provided using the 'v' argument, which must be a Dict whose contents contents +// override any pre-existing defaults. +func (p *ConfigParser) GetInterpolatedWithVars(section, option string, v Dict) (string, error) { + o, err := p.Items(section) + if err != nil { + return "", err + } + p.opt.interpolation.Add(chainmap.Dict(p.Defaults()), chainmap.Dict(o), chainmap.Dict(v)) + return p.getInterpolated(section, option, p.opt.interpolation) +} + +// Private method which does the work of interpolating a value +// interpolates the value using the values in the ChainMap +// returns the interpolated string. +func (p *ConfigParser) interpolate(value string, options Interpolator) string { + for i := 0; i < maxInterpolationDepth; i++ { + if strings.Contains(value, "%(") { + value = interpolater.ReplaceAllStringFunc(value, func(m string) string { + // No ReplaceAllStringSubMatchFunc so apply the regexp twice + match := interpolater.FindAllStringSubmatch(m, 1)[0][1] + replacement := options.Get(match) + return replacement + }) + } + } + return value +} + +// ItemsWithDefaultsInterpolated returns a copy of the dict for the section. +func (p *ConfigParser) ItemsWithDefaultsInterpolated(section string) (Dict, error) { + s, err := p.ItemsWithDefaults(section) + if err != nil { + return nil, err + } + // TOOD: Optimise this...instantiate the ChainMap and delegate to interpolate() + for k := range s { + v, err := p.GetInterpolated(section, k) + if err != nil { + return nil, err + } + s[k] = v + } + return s, nil +} diff --git a/vendor/github.com/bigkevmcd/go-configparser/methods.go b/vendor/github.com/bigkevmcd/go-configparser/methods.go new file mode 100644 index 000000000..fbd9dbf98 --- /dev/null +++ b/vendor/github.com/bigkevmcd/go-configparser/methods.go @@ -0,0 +1,318 @@ +package configparser + +import ( + "fmt" + "sort" + "strconv" +) + +func (p *ConfigParser) isDefaultSection(section string) bool { + return section == p.opt.defaultSection +} + +// Defaults returns the items in the map used for default values. +func (p *ConfigParser) Defaults() Dict { + return p.defaults.Items() +} + +// Sections returns a list of section names, excluding [DEFAULT]. +func (p *ConfigParser) Sections() []string { + sections := make([]string, 0) + for section := range p.config { + sections = append(sections, section) + } + sort.Strings(sections) + + return sections +} + +// AddSection creates a new section in the configuration. +// +// Returns an error if a section by the specified name +// already exists. +// Returns an error if the specified name DEFAULT or any of its +// case-insensitive variants. +// Returns nil if no error and the section is created +func (p *ConfigParser) AddSection(section string) error { + if p.isDefaultSection(section) { + return fmt.Errorf("invalid section name: %q", section) + } else if p.HasSection(section) { + return fmt.Errorf("section %q already exists", section) + } + p.config[section] = newSection(section) + + return nil +} + +// HasSection returns true if the named section is present in the +// configuration. +// +// The DEFAULT section is not acknowledged. +func (p *ConfigParser) HasSection(section string) bool { + _, present := p.config[section] + + return present +} + +// Options returns a list of option mames for the given section name. +// +// Returns an error if the section does not exist. +func (p *ConfigParser) Options(section string) ([]string, error) { + if !p.HasSection(section) { + return nil, getNoSectionError(section) + } + seenOptions := make(map[string]bool) + for _, option := range p.config[section].Options() { + seenOptions[option] = true + } + for _, option := range p.defaults.Options() { + seenOptions[option] = true + } + options := make([]string, 0) + for option := range seenOptions { + options = append(options, option) + } + sort.Strings(options) + + return options, nil +} + +// Get returns string value for the named option. +// +// Returns an error if a section does not exist. +// Returns an error if the option does not exist either in the section or in +// the defaults. +func (p *ConfigParser) Get(section, option string) (string, error) { + result, err := p.get(section, option) + if err != nil { + return "", err + } + + value, err := p.opt.converters[StringConv](result) + if err != nil { + return "", err + } + + return value.(string), nil +} + +func (p *ConfigParser) get(section, option string) (string, error) { + if !p.HasSection(section) { + if !p.isDefaultSection(section) { + return "", getNoSectionError(section) + } + if value, err := p.defaults.Get(option); err != nil { + return "", getNoOptionError(section, option) + } else { + return value, nil + } + } else if value, err := p.config[section].Get(option); err == nil { + return value, nil + } else if value, err := p.defaults.Get(option); err == nil { + return value, nil + } + + return "", getNoOptionError(section, option) +} + +// ItemsWithDefaults returns a copy of the named section Dict including +// any values from the Defaults. +// +// NOTE: This is different from the Python version which returns a list of +// tuples +func (p *ConfigParser) ItemsWithDefaults(section string) (Dict, error) { + if !p.HasSection(section) { + return nil, getNoSectionError(section) + } + s := make(Dict) + + for k, v := range p.defaults.Items() { + s[k] = v + } + for k, v := range p.config[section].Items() { + s[k] = v + } + + return s, nil +} + +// Items returns a copy of the section Dict not including the Defaults. +// +// NOTE: This is different from the Python version which returns a list of +// tuples. +func (p *ConfigParser) Items(section string) (Dict, error) { + if section == p.opt.defaultSection { + return p.defaults.Items(), nil + } + + if !p.HasSection(section) { + return nil, getNoSectionError(section) + } + + return p.config[section].Items(), nil +} + +// Set puts the given option into the named section. +// +// Returns an error if the section does not exist. +func (p *ConfigParser) Set(section, option, value string) error { + var setSection *Section + + if p.isDefaultSection(section) { + setSection = p.defaults + } else if _, present := p.config[section]; !present { + return getNoSectionError(section) + } else { + setSection = p.config[section] + } + + return setSection.Add(option, value) +} + +// GetInt64 returns int64 representation of the named option. +// +// Returns an error if a section does not exist. +// Returns an error if the option does not exist either in the section or in +// the defaults. +func (p *ConfigParser) GetInt64(section, option string) (int64, error) { + result, err := p.get(section, option) + if err != nil { + return 0, err + } + + value, err := p.opt.converters[IntConv](result) + if err != nil { + return 0, err + } + + return value.(int64), nil +} + +// GetFloat64 returns float64 representation of the named option. +// +// Returns an error if a section does not exist. +// Returns an error if the option does not exist either in the section or in +// the defaults. +func (p *ConfigParser) GetFloat64(section, option string) (float64, error) { + result, err := p.get(section, option) + if err != nil { + return 0, err + } + + value, err := p.opt.converters[FloatConv](result) + if err != nil { + return 0, err + } + + return value.(float64), nil +} + +// GetBool returns bool representation of the named option. +// +// Returns an error if a section does not exist. +// Returns an error if the option does not exist either in the section or in +// the defaults. +func (p *ConfigParser) GetBool(section, option string) (bool, error) { + result, err := p.get(section, option) + if err != nil { + return false, err + } + + value, err := p.opt.converters[BoolConv](result) + if err != nil { + return false, err + } + + return value.(bool), nil +} + +// RemoveSection removes given section from the ConfigParser. +func (p *ConfigParser) RemoveSection(section string) error { + if !p.HasSection(section) { + return getNoSectionError(section) + } + delete(p.config, section) + + return nil +} + +// HasOption checks if section contains option. +func (p *ConfigParser) HasOption(section, option string) (bool, error) { + var s *Section + if p.isDefaultSection(section) { + s = p.defaults + } else if _, present := p.config[section]; !present { + return false, getNoSectionError(section) + } else { + s = p.config[section] + } + _, err := s.Get(option) + + return err == nil, nil +} + +// RemoveOption removes option from the section. +func (p *ConfigParser) RemoveOption(section, option string) error { + var s *Section + if p.isDefaultSection(section) { + s = p.defaults + } else if _, present := p.config[section]; !present { + return getNoSectionError(section) + } else { + s = p.config[section] + } + + return s.Remove(option) +} + +func (p *ConfigParser) inOptions(key string) error { + opts, err := p.allOptions() + if err != nil { + return err + } + + for _, o := range opts { + if key == o { + return fmt.Errorf( + "option %q already exists and strict flag was set", + key, + ) + } + } + + return nil +} + +func (p *ConfigParser) allOptions() ([]string, error) { + sections := p.Sections() + options := make([]string, 0) + for _, s := range sections { + o, err := p.Options(s) + if err != nil { + return nil, err + } + + options = append(options, o...) + } + + return options, nil +} + +func defaultGet(value string) (any, error) { return value, nil } + +func defaultGetInt64(value string) (any, error) { + return strconv.ParseInt(value, 10, 64) +} + +func defaultGetFloat64(value string) (any, error) { + return strconv.ParseFloat(value, 64) +} + +func defaultGetBool(value string) (any, error) { + booleanValue, present := boolMapping[value] + if !present { + return false, fmt.Errorf("not a boolean: %q", value) + } + + return booleanValue, nil +} diff --git a/vendor/github.com/bigkevmcd/go-configparser/options.go b/vendor/github.com/bigkevmcd/go-configparser/options.go new file mode 100644 index 000000000..4404ac0f1 --- /dev/null +++ b/vendor/github.com/bigkevmcd/go-configparser/options.go @@ -0,0 +1,159 @@ +package configparser + +import ( + "strings" + + "github.com/bigkevmcd/go-configparser/chainmap" +) + +const defaultSectionName = "DEFAULT" + +// options allows to control parser behavior. +type options struct { + interpolation Interpolator + commentPrefixes Prefixes + inlineCommentPrefixes Prefixes + multilinePrefixes Prefixes + defaultSection string + delimiters string + converters Converter + allowNoValue bool + emptyLines bool + strict bool +} + +// Converter contains custom convert functions for available types. +// The caller should guarantee type assertion to the requested type +// after custom processing! +type Converter map[int]ConvertFunc + +// Predefined types for Converter. +const ( + StringConv = iota + IntConv + FloatConv + BoolConv +) + +// ConvertFunc is a custom datatype converter. +type ConvertFunc func(string) (any, error) + +// Prefixes stores available prefixes for comments. +type Prefixes []string + +// HasPrefix checks if str starts with any of the prefixes. +func (pr Prefixes) HasPrefix(str string) bool { + for _, p := range pr { + if strings.HasPrefix(str, p) { + return true + } + } + + return false +} + +// Split splits str with the first prefix found. +// Returns original string if no matches. +func (pr Prefixes) Split(str string) string { + for _, p := range pr { + if strings.Contains(str, p) { + return strings.Split(str, p)[0] + } + } + + return str +} + +// Interpolator defines interpolation instance. +// For more details, check [chainmap.ChainMap] realisation. +type Interpolator interface { + Add(...chainmap.Dict) + Len() int + Get(string) string +} + +// defaultOptions returns the struct of preset required options. +func defaultOptions() *options { + return &options{ + interpolation: chainmap.New(), + defaultSection: defaultSectionName, + delimiters: ":=", + commentPrefixes: Prefixes{"#", ";"}, + multilinePrefixes: Prefixes{"\t", " "}, + converters: Converter{ + StringConv: defaultGet, + IntConv: defaultGetInt64, + FloatConv: defaultGetFloat64, + BoolConv: defaultGetBool, + }, + } +} + +type optFunc func(*options) + +// Interpolation sets custom interpolator. +func Interpolation(i Interpolator) optFunc { + return func(o *options) { + o.interpolation = i + } +} + +// CommentPrefixes sets a slice of comment prefixes. +// Lines of configuration file which starts with +// the first match in this slice will be skipped. +func CommentPrefixes(pr Prefixes) optFunc { + return func(o *options) { + o.commentPrefixes = pr + } +} + +// InlineCommentPrefixes sets a slice of inline comment delimiters. +// When parsing a value, it will be split with +// the first match in this slice. +func InlineCommentPrefixes(pr Prefixes) optFunc { + return func(o *options) { + o.inlineCommentPrefixes = pr + } +} + +// MultilinePrefixes sets a slice of prefixes, which will define +// multiline value. +func MultilinePrefixes(pr Prefixes) optFunc { + return func(o *options) { + o.multilinePrefixes = pr + } +} + +// DefaultSection sets the name of the default section. +func DefaultSection(n string) optFunc { + return func(o *options) { + o.defaultSection = n + } +} + +// Delimiters sets a string of delimiters for option-value pairs. +func Delimiters(d string) optFunc { + return func(o *options) { + o.delimiters = d + } +} + +// Converters sets custom convertion functions. Will apply to return +// value of the Get* methods instead of the default convertion. +// +// NOTE: the caller should guarantee type assetion to the requested type +// after custom processing or the method will panic. +func Converters(conv Converter) optFunc { + return func(o *options) { + o.converters = conv + } +} + +// AllowNoValue allows option with no value to be saved as empty line. +func AllowNoValue(o *options) { o.allowNoValue = true } + +// Strict prohibits the duplicates of options and values. +func Strict(o *options) { o.strict = true } + +// AllowEmptyLines allows empty lines in multiline values. +func AllowEmptyLines(o *options) { o.emptyLines = true } diff --git a/vendor/github.com/bigkevmcd/go-configparser/section.go b/vendor/github.com/bigkevmcd/go-configparser/section.go new file mode 100644 index 000000000..00187d354 --- /dev/null +++ b/vendor/github.com/bigkevmcd/go-configparser/section.go @@ -0,0 +1,79 @@ +package configparser + +import "strings" + +// Section represent each section of the configuration file. +type Section struct { + Name string + options Dict + lookup Dict +} + +// Add adds new key-value pair to the section. +func (s *Section) Add(key, value string) error { + lookupKey := s.safeKey(key) + s.options[key] = s.safeValue(value) + s.lookup[lookupKey] = key + + return nil +} + +// Get returns value of an option with the given key. +// +// Returns an error if the option does not exist either in the section or in +// the defaults. +func (s *Section) Get(key string) (string, error) { + lookupKey, present := s.lookup[s.safeKey(key)] + if !present { + return "", getNoOptionError(s.Name, key) + } + if value, present := s.options[lookupKey]; present { + return value, nil + } + + return "", getNoOptionError(s.Name, key) +} + +// Options returns a slice of option names. +func (s *Section) Options() []string { + return s.options.Keys() +} + +// Items returns a Dict with the key-value pairs. +func (s *Section) Items() Dict { + return s.options +} + +func (s *Section) safeValue(in string) string { + return strings.TrimSpace(in) +} + +func (s *Section) safeKey(in string) string { + return strings.ToLower(strings.TrimSpace(in)) +} + +// Remove removes option with the given name from the section. +// +// Returns an error if the option does not exist either in the section or in +// the defaults. +func (s *Section) Remove(key string) error { + _, present := s.options[key] + if !present { + return getNoOptionError(s.Name, key) + } + + // delete doesn't return anything, but this does require + // that the passed key to be removed matches the options key. + delete(s.lookup, s.safeKey(key)) + delete(s.options, key) + + return nil +} + +func newSection(name string) *Section { + return &Section{ + Name: name, + options: make(Dict), + lookup: make(Dict), + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 301612956..1543363e5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -10,6 +10,10 @@ github.com/StackExchange/wmi # github.com/beorn7/perks v1.0.1 ## explicit; go 1.11 github.com/beorn7/perks/quantile +# github.com/bigkevmcd/go-configparser v0.0.0-20240808124832-fc81059ea0bd +## explicit; go 1.21 +github.com/bigkevmcd/go-configparser +github.com/bigkevmcd/go-configparser/chainmap # github.com/cespare/xxhash/v2 v2.1.2 ## explicit; go 1.11 github.com/cespare/xxhash/v2