Yet Another Config File Reader for Go (Golang)

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.

Advertisements

About Jim Lawless

I've been programming computers for about 36 years ... 30 of that professionally. I've been a teacher, I've worked as a consultant, and have written articles here and there for publications like Dr. Dobbs Journal, The C/C++ Users Journal, Nuts and Volts, and others.
This entry was posted in Programming and tagged . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s