I’ve been working up to some larger projects as I learn Go. One of my current projects now needs the ability to parse a configuration file.
I’ve looked at a few of the existing options. There are parsers for Windows-style INI files, YAML files, JSON files, and one for Java-like Properties files.
I’ve always preferred the simplicity of reading a configuration file into a map or associative-array construct, so I thought I’d write my own.
The project will be maintained at this Github repository:
https://github.com/jimlawless/cfg
My basic requirements were:
- A “#” character at the beginning of a line denotes a comment.
- A blank line should be ignored.
- All key/value pairs should start with a non-blank identifier name at the beginning of a line.
- An equals-sign ( “=” ) should separate the key from the value with no whitespace on either the left or right side.
- While the configuration file reader will read files that look like Java Properties files, escape-sequences in the value portion of the line will not be evaluated.
The file test.cfg will be used as a sample config file. Its contents are as follows:
# this is a comment # blank line above fred=flintstone of The Flintstones barney=barney rubble # biff= Onesie=Twosie
Note that the file must end with a newline or an error condition will occur.
The config file reader library I’ve created is simply called “cfg”. The sample program to use the library to load a config file into a map is as follows:
// Copyright 2013 - by Jim Lawless
// License: MIT / X11
// See: http://www.mailsend-online.com/license2013.php
package main
import (
"cfg"
"fmt"
"log"
)
func main() {
mymap := make(map[string]string)
err := cfg.Load("test.cfg", mymap)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%v\n", mymap)
}
The output of the above fmt.Printf() call is:
map[fred:flintstone of The Flintstones barney:barney rubble biff: onesie:Twosie]
Note that blank values are supported on the right side of the equals sign.
The cfg library exposes one function: Load(). Load() accepts a filename and a map. The file represented by the filename is loaded into a string using the following code:
func Load(filename string, dest map[string]string) error {
fi, err := os.Stat(filename)
if err != nil {
return err
}
f, err := os.Open(filename)
if err != nil {
return err
}
buff := make([]byte, fi.Size())
f.Read(buff)
f.Close()
str := string(buff)
...
I use a regular-expression to then parse the string into its significant tokens instead of dealing with the file on a line-by-line basis.
The regular-expression is :
var re *regexp.Regexp
var pat = "[#].*\\n|\\s+\\n|\\S+[=]|.*\n"
func init() {
re, _ = regexp.Compile(pat)
}
The regular-expression first checks for a comment-line. Then it checks for a blank line. Then, it looks for a string of non-blank characters followed by the equals-sign. Then it looks for any other characters up to and including the newline character.
A check is made before any regex parsing is performed to ensure that the data ends with a newline. If not, the code adds a newline. ( This check was added by a contributor whose name I’ve since lost when shuffling repos. Sorry! )
if !strings.HasSuffix(str, "\n") {
str += "\n";
}
The logic in the portion of the code that parses the config data discards comment-lines and blank-lines:
if strings.HasPrefix(s2[i], "#") {
i++
...
} else if strings.Index(" \t\r\n", s2[i][0:1]) > -1 {
i++
If the string token ends with an equals sign, we have a “key”. We remove the “=” and then parse for the remainder of the line removing the newline ( and a carriage-return, if present, for files edited on Windows systems ).
Please note that I force all keys to lower-case with string.ToLower() to avoid any potential issues with a user mis-keying a key based on case. The value portion of the line is kept intact.
} else if strings.HasSuffix(s2[i], "=") {
key := strings.ToLower(s2[i])[0 : len(s2[i])-1]
i++
if strings.HasSuffix(s2[i], "\n") {
val := s2[i][0 : len(s2[i])-1]
if strings.HasSuffix(val, "\r") {
val = val[0 : len(val)-1]
}
i++
dest[key] = val
}
…and if we encounter a token that doesn’t have an equals-sign or doesn’t conform to the simple grammar that we’re parsing, we return an error:
return errors.New("Unable to process line in cfg file containing " + s2[i])
The initial cfg library code is as follows:
// cfg - Yet another config file reader
// License: MIT / X11
// Copyright (c) 2013 by James K. Lawless
// jimbo@radiks.net http://www.radiks.net/~jimbo
// http://www.mailsend-online.com
//
// 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.
package cfg
import (
"errors"
"os"
"regexp"
"strings"
)
var re *regexp.Regexp
var pat = "[#].*\\n|\\s+\\n|\\S+[=]|.*\n"
func init() {
re, _ = regexp.Compile(pat)
}
// Load adds or updates entries in an existing map with string keys
// and string values using a configuration file.
//
// The filename paramter indicates the configuration file to load ...
// the dest parameter is the map that will be updated.
//
// The configuration file entries should be constructed in key=value
// syntax. A # symbol at the beginning of a line indicates a comment.
// Blank lines are ignored.
func Load(filename string, dest map[string]string) error {
fi, err := os.Stat(filename)
if err != nil {
return err
}
f, err := os.Open(filename)
if err != nil {
return err
}
buff := make([]byte, fi.Size())
f.Read(buff)
f.Close()
str := string(buff)
if !strings.HasSuffix(str, "\n") {
return errors.New("Config file does not end with a newline character.")
}
s2 := re.FindAllString(str, -1)
for i := 0; i < len(s2); { if strings.HasPrefix(s2[i], "#") { i++ } else if strings.HasSuffix(s2[i], "=") { key := strings.ToLower(s2[i])[0 : len(s2[i])-1] i++ if strings.HasSuffix(s2[i], "\n") { val := s2[i][0 : len(s2[i])-1] if strings.HasSuffix(val, "\r") { val = val[0 : len(val)-1] } i++ dest[key] = val } } else if strings.Index(" \t\r\n", s2[i][0:1]) > -1 {
i++
} else {
return errors.New("Unable to process line in cfg file containing " + s2[i])
}
}
return nil
}
I intend to use this library in an upcoming project.
