Imported Upstream version 0.1
[debian/configureit.git] / configureit.go
1 // configureit.go
2 //
3 // configureit:  A library for parsing configuration files.
4 //
5 // Copyright (C) 2011, Chris Collins <chris.collins@anchor.net.au>
6
7 package configureit
8
9 import (
10         "strings"
11         "io"
12         "os"
13         "bufio"
14         "unicode"
15         "fmt"
16 )
17
18 // ParseErrors are returned by ConfigNodes when they encounter a
19 // problem with their input, or by the config reader when it
20 // has problems.
21 type ParseError struct {
22         LineNumber      int
23         InnerError      os.Error
24 }
25
26 var MissingEqualsOperator = os.NewError("No equals (=) sign on non-blank line")
27
28 func (err *ParseError) String() string {
29         return fmt.Sprintf("%s (at line %d)", err.InnerError, err.LineNumber)
30 }
31
32 func NewParseError(lineNumber int, inner os.Error) os.Error {
33         err := new(ParseError)
34         
35         err.LineNumber = lineNumber
36         err.InnerError = inner
37
38         return err
39 }
40
41 // Unknown option errors are thrown when the key name (left-hand side
42 // of a config item) is unknown.
43 type UnknownOptionError struct {
44         LineNumber      int
45         Key             string
46 }
47
48 func (err *UnknownOptionError) String() string {
49         return fmt.Sprintf("Unknown Key \"%s\" at line %d", err.Key, err.LineNumber)
50 }
51
52 func NewUnknownOptionError(lineNumber int, key string) os.Error {
53         err := new(UnknownOptionError)
54         
55         err.LineNumber = lineNumber
56         err.Key = key
57
58         return err
59 }
60
61 // A configuration is made up of many ConfigNodes.
62 //
63 // ConfigNodes are typed, and are handled by their own node
64 // implementations.
65 type ConfigNode interface {
66         // returns the value formatted as a string.  Must be parsable with
67         // Parse() to produce the same value.
68         String()        string
69
70         // parses the string and set the value.  Clears default.  
71         // Returns errors if the results can't be read.
72         Parse(newValue string)  os.Error
73
74         // is the current value the default?
75         IsDefault()     bool
76
77         // reset to the default value.
78         Reset()
79 }
80
81 // This represents a configuration.
82 type Config struct {
83         structure       map[string]ConfigNode
84 }
85
86 // Create a new configuration object.
87 func New() (config *Config) {
88         config = new(Config)
89         config.structure = make(map[string]ConfigNode)
90
91         return config
92 }
93
94 // Add the specified ConfigNode to the configuration
95 func (config *Config) Add(keyname string, newNode ConfigNode) {
96         keyname = strings.ToLower(keyname)
97         config.structure[keyname] = newNode
98 }
99
100 // Reset the entire configuration.
101 func (config *Config) Reset() {
102         for _, v := range config.structure {
103                 v.Reset()
104         }
105 }
106
107 // Get the named node
108 func (config *Config) Get(keyName string) ConfigNode {
109         keyName = strings.ToLower(keyName)
110         citem, found := config.structure[keyName]
111         if found {
112                 return citem
113         }
114         return nil
115 }
116
117 // Save spits out the configuration to the nominated writer.
118 // if emitDefaults is true, values that are set to the default
119 // will be omitted, otherwise they will be omitted.
120 //
121 // When in doubt, you probably want emitDefaults == false.
122 func (config *Config) Write(out io.Writer, emitDefaults bool) {
123         for k,v := range config.structure {
124                 if !v.IsDefault() || emitDefaults {
125                         // non-default value, must write!
126                         line := fmt.Sprintf("%s=%s\n", k, v)
127                         io.WriteString(out, line)
128                 }
129         }
130 }
131
132 // Read the configuration from the specified reader.
133 //
134 // Special behaviour to note:
135 //
136 //   Lines beginning with '#' or ';' are treated as comments.  They are 
137 //   not comments anywhere else on the line unless the config node parser
138 //   handles it itself.
139 //
140 //   Whitespace surrounding the name of a configuration key will be ignored.
141 //
142 //   Configuration key names will be tested case insensitively.
143 //
144 // firstLineNumber specifies the actual first line number in the file (for
145 // partial file reads, or resume from error)
146 func (config *Config) Read(in io.Reader, firstLineNumber int) os.Error {
147         bufin := bufio.NewReader(in)
148
149         // position the line number before the 'first' line.
150         var lineNumber int = (firstLineNumber-1)
151         
152         for {
153                 var bline []byte = nil
154                 var isPrefix bool
155                 var err os.Error
156
157                 // get a whole line of input, and handle buffer exhausation
158                 // correctly.
159                 bline, isPrefix, err = bufin.ReadLine()
160                 if err != nil {
161                         if err == os.EOF {
162                                 break
163                         } else {
164                                 return err
165                         }
166                 }
167                 for isPrefix {
168                         var contline []byte
169                         
170                         contline, isPrefix, err = bufin.ReadLine()
171                         if err != nil {
172                                 return err
173                         }
174                         bline = append(bline, contline...)
175                 }
176                 // advance the line number
177                 lineNumber++
178
179                 // back convert the bytearray to a native string.
180                 line := string(bline)
181                 
182                 // now, start doing unspreakable things to it! (bwahaha)
183
184                 // remove left space
185                 line = strings.TrimLeftFunc(line, unicode.IsSpace)
186
187                 // if empty, skip.
188                 if line == "" {
189                         continue
190                 }
191
192                 // if a comment, skip.
193                 if line[0] == '#' || line[0] == ';' {
194                         continue
195                 }
196
197                 // since it is neither, look for an equals sign.
198                 epos := strings.Index(line, "=")
199                 if epos < 0 {
200                         // no =.  Throw a parse error.
201                         return NewParseError(lineNumber, MissingEqualsOperator)
202                 }
203
204                 // take the two slices.
205                 keyname := line[0:epos]
206                 rawvalue := line[epos+1:len(line)]
207
208                 // clean up the keyname
209                 keyname = strings.TrimRightFunc(keyname,unicode.IsSpace)
210                 keyname = strings.ToLower(keyname)
211
212                 // find the correct key in the config.
213                 cnode := config.Get(keyname)
214                 if nil == cnode {
215                         return NewUnknownOptionError(lineNumber, keyname)
216                 } else {
217                         err := cnode.Parse(rawvalue)
218                         if (err != nil) {
219                                 return NewParseError(lineNumber, err)
220                         }
221                 }
222                 // and we're done!
223         }
224         return nil
225 }