From 31cce1a260bff1b540ae080fb5da5d32e0090b5e Mon Sep 17 00:00:00 2001 From: Steven McDonald Date: Mon, 3 Oct 2011 00:30:49 +1100 Subject: [PATCH] initialise --- .gitignore | 7 ++ Makefile | 13 +++ configureit.go | 225 ++++++++++++++++++++++++++++++++++++++++++++ configureit_test.go | 180 +++++++++++++++++++++++++++++++++++ int.go | 51 ++++++++++ pathlist.go | 61 ++++++++++++ sample.conf | 4 + string.go | 44 +++++++++ user.go | 96 +++++++++++++++++++ 9 files changed, 681 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 configureit.go create mode 100644 configureit_test.go create mode 100644 int.go create mode 100644 pathlist.go create mode 100644 sample.conf create mode 100644 string.go create mode 100644 user.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..480c12a --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*~ +\#* +*.[68] +[68].out +_testmain.go +_test/** +_obj/** diff --git a/Makefile b/Makefile new file mode 100644 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 index 0000000..24c1a85 --- /dev/null +++ b/configureit.go @@ -0,0 +1,225 @@ +// configureit.go +// +// configureit: A library for parsing configuration files. +// +// Copyright (C) 2011, Chris Collins + +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 index 0000000..037a6ac --- /dev/null +++ b/configureit_test.go @@ -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 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 index 0000000..57d518c --- /dev/null +++ b/pathlist.go @@ -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 index 0000000..70f2340 --- /dev/null +++ b/sample.conf @@ -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 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 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 +} -- 2.30.2