--- /dev/null
+*~
+\#*
+*.[68]
+[68].out
+_testmain.go
+_test/**
+_obj/**
--- /dev/null
+include $(GOROOT)/src/Make.inc
+
+TARG=github.com/kuroneko/configureit
+
+GOFILES=\
+ configureit.go\
+ string.go\
+ int.go\
+ user.go\
+ pathlist.go\
+
+
+include $(GOROOT)/src/Make.pkg
--- /dev/null
+// configureit.go
+//
+// configureit: A library for parsing configuration files.
+//
+// Copyright (C) 2011, Chris Collins <chris.collins@anchor.net.au>
+
+package configureit
+
+import (
+ "strings"
+ "io"
+ "os"
+ "bufio"
+ "unicode"
+ "fmt"
+)
+
+// ParseErrors are returned by ConfigNodes when they encounter a
+// problem with their input, or by the config reader when it
+// has problems.
+type ParseError struct {
+ LineNumber int
+ InnerError os.Error
+}
+
+var MissingEqualsOperator = os.NewError("No equals (=) sign on non-blank line")
+
+func (err *ParseError) String() string {
+ return fmt.Sprintf("%s (at line %d)", err.InnerError, err.LineNumber)
+}
+
+func NewParseError(lineNumber int, inner os.Error) os.Error {
+ err := new(ParseError)
+
+ err.LineNumber = lineNumber
+ err.InnerError = inner
+
+ return err
+}
+
+// Unknown option errors are thrown when the key name (left-hand side
+// of a config item) is unknown.
+type UnknownOptionError struct {
+ LineNumber int
+ Key string
+}
+
+func (err *UnknownOptionError) String() string {
+ return fmt.Sprintf("Unknown Key \"%s\" at line %d", err.Key, err.LineNumber)
+}
+
+func NewUnknownOptionError(lineNumber int, key string) os.Error {
+ err := new(UnknownOptionError)
+
+ err.LineNumber = lineNumber
+ err.Key = key
+
+ return err
+}
+
+// A configuration is made up of many ConfigNodes.
+//
+// ConfigNodes are typed, and are handled by their own node
+// implementations.
+type ConfigNode interface {
+ // returns the value formatted as a string. Must be parsable with
+ // Parse() to produce the same value.
+ String() string
+
+ // parses the string and set the value. Clears default.
+ // Returns errors if the results can't be read.
+ Parse(newValue string) os.Error
+
+ // is the current value the default?
+ IsDefault() bool
+
+ // reset to the default value.
+ Reset()
+}
+
+// This represents a configuration.
+type Config struct {
+ structure map[string]ConfigNode
+}
+
+// Create a new configuration object.
+func New() (config *Config) {
+ config = new(Config)
+ config.structure = make(map[string]ConfigNode)
+
+ return config
+}
+
+// Add the specified ConfigNode to the configuration
+func (config *Config) Add(keyname string, newNode ConfigNode) {
+ keyname = strings.ToLower(keyname)
+ config.structure[keyname] = newNode
+}
+
+// Reset the entire configuration.
+func (config *Config) Reset() {
+ for _, v := range config.structure {
+ v.Reset()
+ }
+}
+
+// Get the named node
+func (config *Config) Get(keyName string) ConfigNode {
+ keyName = strings.ToLower(keyName)
+ citem, found := config.structure[keyName]
+ if found {
+ return citem
+ }
+ return nil
+}
+
+// Save spits out the configuration to the nominated writer.
+// if emitDefaults is true, values that are set to the default
+// will be omitted, otherwise they will be omitted.
+//
+// When in doubt, you probably want emitDefaults == false.
+func (config *Config) Write(out io.Writer, emitDefaults bool) {
+ for k,v := range config.structure {
+ if !v.IsDefault() || emitDefaults {
+ // non-default value, must write!
+ line := fmt.Sprintf("%s=%s\n", k, v)
+ io.WriteString(out, line)
+ }
+ }
+}
+
+// Read the configuration from the specified reader.
+//
+// Special behaviour to note:
+//
+// Lines beginning with '#' or ';' are treated as comments. They are
+// not comments anywhere else on the line unless the config node parser
+// handles it itself.
+//
+// Whitespace surrounding the name of a configuration key will be ignored.
+//
+// Configuration key names will be tested case insensitively.
+//
+// firstLineNumber specifies the actual first line number in the file (for
+// partial file reads, or resume from error)
+func (config *Config) Read(in io.Reader, firstLineNumber int) os.Error {
+ bufin := bufio.NewReader(in)
+
+ // position the line number before the 'first' line.
+ var lineNumber int = (firstLineNumber-1)
+
+ for {
+ var bline []byte = nil
+ var isPrefix bool
+ var err os.Error
+
+ // get a whole line of input, and handle buffer exhausation
+ // correctly.
+ bline, isPrefix, err = bufin.ReadLine()
+ if err != nil {
+ if err == os.EOF {
+ break
+ } else {
+ return err
+ }
+ }
+ for isPrefix {
+ var contline []byte
+
+ contline, isPrefix, err = bufin.ReadLine()
+ if err != nil {
+ return err
+ }
+ bline = append(bline, contline...)
+ }
+ // advance the line number
+ lineNumber++
+
+ // back convert the bytearray to a native string.
+ line := string(bline)
+
+ // now, start doing unspreakable things to it! (bwahaha)
+
+ // remove left space
+ line = strings.TrimLeftFunc(line, unicode.IsSpace)
+
+ // if empty, skip.
+ if line == "" {
+ continue
+ }
+
+ // if a comment, skip.
+ if line[0] == '#' || line[0] == ';' {
+ continue
+ }
+
+ // since it is neither, look for an equals sign.
+ epos := strings.Index(line, "=")
+ if epos < 0 {
+ // no =. Throw a parse error.
+ return NewParseError(lineNumber, MissingEqualsOperator)
+ }
+
+ // take the two slices.
+ keyname := line[0:epos]
+ rawvalue := line[epos+1:len(line)]
+
+ // clean up the keyname
+ keyname = strings.TrimRightFunc(keyname,unicode.IsSpace)
+ keyname = strings.ToLower(keyname)
+
+ // find the correct key in the config.
+ cnode := config.Get(keyname)
+ if nil == cnode {
+ return NewUnknownOptionError(lineNumber, keyname)
+ } else {
+ err := cnode.Parse(rawvalue)
+ if (err != nil) {
+ return NewParseError(lineNumber, err)
+ }
+ }
+ // and we're done!
+ }
+ return nil
+}
\ No newline at end of file
--- /dev/null
+
+package configureit
+
+import (
+ "testing"
+ "os"
+)
+
+func makeSimpleConfig() *Config {
+ testConfig := New()
+ testConfig.Add("key_a", NewStringOption("default 1"))
+ testConfig.Add("key_b", NewIntOption(2))
+ testConfig.Add("user_test", NewUserOption(""))
+ testConfig.Add("user test 2", NewUserOption(""))
+ return testConfig
+}
+
+func TestConfig(t *testing.T) {
+ testConfig := makeSimpleConfig()
+
+ tv := testConfig.Get("key_a")
+ if nil == tv {
+ t.Errorf("Couldn't find key_a in testConfig")
+ } else {
+ if !tv.IsDefault() {
+ t.Errorf("key_a reported non-default without changes")
+ }
+ sopt, ok := tv.(*StringOption)
+ if !ok {
+ t.Errorf("Failed return assertion for key_a back to StringOption")
+ }
+ if sopt.Value != "default 1" {
+ t.Errorf("key_a Value doesn't match initial configured value.")
+ }
+ }
+
+ tv = testConfig.Get("key_b")
+ if nil == tv {
+ t.Errorf("Couldn't find key_b in testConfig")
+ } else {
+ if !tv.IsDefault() {
+ t.Errorf("key_b reported non-default without changes")
+ }
+ iopt, ok := tv.(*IntOption)
+ if !ok {
+ t.Errorf("Failed return assertion for key_b back to IntOption")
+ }
+ if iopt.Value != 2 {
+ t.Errorf("key_b Value doesn't match initial configured value.")
+ }
+ }
+
+ tv = testConfig.Get("user_test")
+ if nil == tv {
+ t.Errorf("Couldn't find user_test in testConfig")
+ } else {
+ if !tv.IsDefault() {
+ t.Errorf("user_test reported non-default without changes")
+ }
+ uopt, ok := tv.(*UserOption)
+ if !ok {
+ t.Errorf("Failed return assertion for user_test back to UserOption")
+ }
+ _, err := uopt.User()
+ if err != EmptyUserSet {
+ t.Errorf("user_test didn't claim it set empty.")
+ }
+ }
+
+ tv = testConfig.Get("user test 2")
+ if nil == tv {
+ t.Errorf("Couldn't find \"user test 2\" in testConfig")
+ } else {
+ if !tv.IsDefault() {
+ t.Errorf("user test 2 reported non-default without changes")
+ }
+ uopt, ok := tv.(*UserOption)
+ if !ok {
+ t.Errorf("Failed return assertion for user test 2 back to UserOption")
+ }
+ _, err := uopt.User()
+ if err != EmptyUserSet {
+ t.Errorf("user test 2 didn't claim it set empty.")
+ }
+ }
+
+ tv = testConfig.Get("key_c")
+ if nil != tv {
+ t.Errorf("Found non-existant key_c in testConfig")
+ }
+}
+
+func TestFileRead(t *testing.T) {
+ testConfig := makeSimpleConfig()
+ fh, err := os.Open("sample.conf")
+ if err != nil {
+ t.Fatalf("Failed to open sample.conf: %s", err)
+ }
+ err = testConfig.Read(fh, 1)
+ if err != nil {
+ t.Fatalf("Got error reading config: %s", err)
+ }
+ fh.Close()
+
+ tv := testConfig.Get("key_a")
+ if nil == tv {
+ t.Errorf("Couldn't find key_a in testConfig")
+ } else {
+ if tv.IsDefault() {
+ t.Errorf("key_a reported default despite config file")
+ }
+ sopt, ok := tv.(*StringOption)
+ if !ok {
+ t.Errorf("Failed return assertion for key_a back to StringOption")
+ }
+ if sopt.Value != "Alternate Value" {
+ t.Errorf("key_a Value doesn't match expected value.")
+ }
+ }
+
+ tv = testConfig.Get("key_b")
+ if nil == tv {
+ t.Errorf("Couldn't find key_b in testConfig")
+ } else {
+ if tv.IsDefault() {
+ t.Errorf("key_b reported default despite config file")
+ }
+ iopt, ok := tv.(*IntOption)
+ if !ok {
+ t.Errorf("Failed return assertion for key_b back to IntOption")
+ }
+ if iopt.Value != 27 {
+ t.Errorf("key_b Value doesn't match expected value.")
+ }
+ }
+
+ tv = testConfig.Get("user_test")
+ if nil == tv {
+ t.Errorf("Couldn't find user_test in testConfig")
+ } else {
+ if tv.IsDefault() {
+ t.Errorf("user_test reported default despite changes")
+ }
+ uopt, ok := tv.(*UserOption)
+ if !ok {
+ t.Errorf("Failed return assertion for user_test back to UserOption")
+ }
+ uinfo, err := uopt.User()
+ if err != nil {
+ t.Errorf("Error whilst looking up UID: %s", err)
+ }
+ if uinfo.Uid != 0 {
+ t.Errorf("user_test Value doesn't match expected value.")
+ }
+ }
+
+ tv = testConfig.Get("user test 2")
+ if nil == tv {
+ t.Errorf("Couldn't find \"user test 2\" in testConfig")
+ } else {
+ if tv.IsDefault() {
+ t.Errorf("user test 2 reported default despite changes")
+ }
+ uopt, ok := tv.(*UserOption)
+ if !ok {
+ t.Errorf("Failed return assertion for user test 2 back to UserOption")
+ }
+ uinfo, err := uopt.User()
+ if err != nil {
+ t.Errorf("Error whilst looking up UID: %s", err)
+ }
+ if uinfo.Uid != 1 {
+ t.Errorf("user test 2 Value doesn't match expected value.")
+ }
+ if uinfo.Username != "daemon" {
+ t.Errorf("user test 2 name lookup didn't match expected value (ignore if not debian/ubuntu/linux?).")
+ }
+ }
+}
+
--- /dev/null
+// int.go
+//
+// Integer Type
+
+package configureit
+
+import (
+ "strings"
+ "strconv"
+ "os"
+ "fmt"
+)
+
+type IntOption struct {
+ defaultvalue int
+ isset bool
+ Value int
+}
+
+func NewIntOption(defaultValue int) ConfigNode {
+ opt := new(IntOption)
+
+ opt.defaultvalue = defaultValue
+ opt.Reset()
+
+ return opt
+}
+
+func (opt *IntOption) String() string {
+ return fmt.Sprintf("%d", opt.Value)
+}
+
+func (opt *IntOption) Parse(newValue string) os.Error {
+ nativenv, err := strconv.Atoi(strings.TrimSpace(newValue))
+ if err != nil {
+ return err
+ }
+ opt.Value = nativenv
+ opt.isset = true
+
+ return nil
+}
+
+func (opt *IntOption) IsDefault() bool {
+ return !opt.isset
+}
+
+func (opt *IntOption) Reset() {
+ opt.Value = opt.defaultvalue
+ opt.isset = false
+}
--- /dev/null
+// string.go
+//
+// String Type
+
+package configureit
+
+import (
+ "os"
+ "strings"
+)
+
+var PathListSeparator = os.PathListSeparator
+
+func init() {
+ if PathListSeparator == 0 {
+ // Bloody Plan9.
+ PathListSeparator = '!'
+ }
+}
+
+// PathList is a list of paths. Paths are assuemd to be separated by
+// os.PathListSeparator ('!' if undefined.)
+//
+// White spaces are valid within terms, but leading and trailing whitespace
+// are discarded from the whole input, not from terms!
+type PathListOption struct {
+ defaultvalue []string
+ isset bool
+ Values []string
+}
+
+func NewPathListOption(defaultValue []string) ConfigNode {
+ opt := new(PathListOption)
+
+ opt.defaultvalue = defaultValue
+ opt.Reset()
+
+ return opt
+}
+
+func (opt *PathListOption) String() string {
+ return strings.Join(opt.Values, string(PathListSeparator))
+}
+
+func (opt *PathListOption) Parse(newValue string) os.Error {
+ newValue = strings.TrimSpace(newValue)
+
+ opt.Values = strings.Split(newValue, string(PathListSeparator))
+ opt.isset = true
+
+ return nil
+}
+
+func (opt *PathListOption) IsDefault() bool {
+ return !opt.isset
+}
+
+func (opt *PathListOption) Reset() {
+ opt.Values = opt.defaultvalue
+ opt.isset = false
+}
--- /dev/null
+key_a=Alternate Value
+key_b=27
+user_test=root
+user test 2=1
\ No newline at end of file
--- /dev/null
+// string.go
+//
+// String Type
+
+package configureit
+
+import (
+ "os"
+)
+
+type StringOption struct {
+ defaultvalue string
+ isset bool
+ Value string
+}
+
+func NewStringOption(defaultValue string) ConfigNode {
+ opt := new(StringOption)
+
+ opt.defaultvalue = defaultValue
+ opt.Reset()
+
+ return opt
+}
+
+func (opt *StringOption) String() string {
+ return opt.Value
+}
+
+func (opt *StringOption) Parse(newValue string) os.Error {
+ opt.Value = newValue
+ opt.isset = true
+
+ return nil
+}
+
+func (opt *StringOption) IsDefault() bool {
+ return !opt.isset
+}
+
+func (opt *StringOption) Reset() {
+ opt.Value = opt.defaultvalue
+ opt.isset = false
+}
--- /dev/null
+// user.go
+//
+// User Type
+
+package configureit
+
+import (
+ "os"
+ "os/user"
+ "strings"
+ "strconv"
+)
+
+// User options represent user specifications in a config file.
+//
+// They can be either a literal username, or a number.
+type UserOption struct {
+ defaultvalue string
+ isset bool
+ // literal value
+ Value string
+}
+
+var EmptyUserSet = os.NewError("No value set")
+
+func NewUserOption(defaultValue string) ConfigNode {
+ opt := new(UserOption)
+
+ opt.defaultvalue = defaultValue
+ opt.Reset()
+
+ return opt
+}
+
+func (opt *UserOption) String() string {
+ return opt.Value
+}
+
+func (opt *UserOption) Parse(newValue string) os.Error {
+ nvs := strings.TrimSpace(newValue)
+ if nvs != "" {
+ // validate the input.
+ _, err := strconv.Atoi(nvs)
+ if err != nil {
+ switch err.(type) {
+ case *strconv.NumError:
+ // not a number. do a lookup.
+ _, err = user.Lookup(nvs)
+ if err != nil {
+ return err
+ }
+ default:
+ return err
+ }
+ }
+ }
+ opt.Value = newValue
+ opt.isset = true
+
+ return nil
+}
+
+func (opt *UserOption) IsDefault() bool {
+ return !opt.isset
+}
+
+func (opt *UserOption) Reset() {
+ opt.Value = opt.defaultvalue
+ opt.isset = false
+}
+
+func (opt *UserOption) User() (userinfo *user.User, err os.Error) {
+ nvs := strings.TrimSpace(opt.Value)
+ if nvs == "" {
+ // special case: empty string is the current euid.
+ return nil, EmptyUserSet
+ }
+ // attempt to map this as a number first, in case a numeric UID
+ // was provided.
+ val, err := strconv.Atoi(nvs)
+ if err != nil {
+ switch err.(type) {
+ case *strconv.NumError:
+ // not a number. do a user table lookup.
+ userinfo, err = user.Lookup(nvs)
+ if err != nil {
+ return nil, err
+ }
+ return userinfo, nil
+ default:
+ return nil, err
+ }
+ }
+ userinfo, err = user.LookupId(val)
+ return userinfo, err
+}