initialise
authorSteven McDonald <steven@steven-mcdonald.id.au>
Sun, 2 Oct 2011 13:30:49 +0000 (00:30 +1100)
committerSteven McDonald <steven@steven-mcdonald.id.au>
Sun, 2 Oct 2011 13:30:49 +0000 (00:30 +1100)
.gitignore [new file with mode: 0644]
Makefile [new file with mode: 0644]
configureit.go [new file with mode: 0644]
configureit_test.go [new file with mode: 0644]
int.go [new file with mode: 0644]
pathlist.go [new file with mode: 0644]
sample.conf [new file with mode: 0644]
string.go [new file with mode: 0644]
user.go [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..480c12a
--- /dev/null
@@ -0,0 +1,7 @@
+*~
+\#*
+*.[68]
+[68].out
+_testmain.go
+_test/**
+_obj/**
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..54b1373
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,13 @@
+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
diff --git a/configureit.go b/configureit.go
new file mode 100644 (file)
index 0000000..24c1a85
--- /dev/null
@@ -0,0 +1,225 @@
+// 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
diff --git a/configureit_test.go b/configureit_test.go
new file mode 100644 (file)
index 0000000..037a6ac
--- /dev/null
@@ -0,0 +1,180 @@
+
+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?).")
+               }
+       }
+}
+       
diff --git a/int.go b/int.go
new file mode 100644 (file)
index 0000000..1bfd556
--- /dev/null
+++ b/int.go
@@ -0,0 +1,51 @@
+// 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
+}
diff --git a/pathlist.go b/pathlist.go
new file mode 100644 (file)
index 0000000..57d518c
--- /dev/null
@@ -0,0 +1,61 @@
+// 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
+}
diff --git a/sample.conf b/sample.conf
new file mode 100644 (file)
index 0000000..70f2340
--- /dev/null
@@ -0,0 +1,4 @@
+key_a=Alternate Value
+key_b=27
+user_test=root
+user test 2=1
\ No newline at end of file
diff --git a/string.go b/string.go
new file mode 100644 (file)
index 0000000..8e30707
--- /dev/null
+++ b/string.go
@@ -0,0 +1,44 @@
+// 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
+}
diff --git a/user.go b/user.go
new file mode 100644 (file)
index 0000000..81a73d1
--- /dev/null
+++ b/user.go
@@ -0,0 +1,96 @@
+// 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
+}