Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
unixdj committed Dec 27, 2011
0 parents commit 3c8a654
Show file tree
Hide file tree
Showing 7 changed files with 378 additions and 0 deletions.
14 changes: 14 additions & 0 deletions LICENSE
@@ -0,0 +1,14 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004

Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>

Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.

DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

0. You just DO WHAT THE FUCK YOU WANT TO.

6 changes: 6 additions & 0 deletions Makefile
@@ -0,0 +1,6 @@
include $(GOROOT)/src/Make.inc

TARG=github.gom/unixdj/conf
GOFILES=conf.go

include $(GOROOT)/src/Make.pkg
5 changes: 5 additions & 0 deletions README
@@ -0,0 +1,5 @@
Trivial configuration file parser for Go. Documentation in source files.

Copyright 2011 Vadim Vygonets. Licensed under WTFPL 2.0.

I wrote this because other Go configuration file parser packages are too big.
293 changes: 293 additions & 0 deletions conf.go
@@ -0,0 +1,293 @@
// Copyright 2001 Vadim Vygonets. No rights reserved.
// Use of this source code is governed by the WTFPL 2.0 license
// that can be found in the LICENSE file.

/*
Package conf parses simple configuration files.
Configuration file syntax (see Parse() for semantics):
The file is composed of lines of UTF-8 text, each no longer than 4KB.
Comments start with '#' and continue to end of line.
Whitespace (Unicode character class Z) between tokens is ignored.
Configuration settings look like this:
ident = value
Identifiers start with an ASCII letter, dash ('-') or underscore ('_'),
and continue with zero or more ASCII letters, ASCII digits, dashes or
underscores. That is, they match /[-_a-zA-Z][-_a-zA-Z0-9]/.
Values may be plain or quoted. Plain values may have any character in
them besides space (Unicode character class Z), control characters
(Unicode character class C), or any of '"', '#', `'`, '=', `\`.
Quoted values are enclosed in double quotes (like "this") and obey Go
quoted string rules. They may not include Unicode control characters.
Any character except `"` and `\` stands for itself. Backslash escapes
\a, \b, \f, \n, \r, \v, \", \\, \337, \xDF, \u1A2F and \U00104567 are
accepted. Quoted values, unlike plain ones, can be empty ("").
The rule about control characters means that tabs inside quoted strings
must be replaced with "\t" (or "\U00000009" or whatever).
Example:
ipv6-addr = [::1]:23 # Look ma, no quotes!
file = /etc/passwd # Comments after settings are OK.
-- = "hello, world\n" # Variables can have strange names.
More formally:
file:
lines
lines:
|
line lines
line:
optional-assignment optional-comment '\n'
optional-assignment:
|
assignment
assignment:
ident '=' value
value:
plain-value |
quoted-value
optional-comment:
|
comment
Tokens:
ident: <ident-alpha> <ident-alnum>*
<ident-alpha>: ascii-alpha | '-' | '_'
<ident-alnum>: <ident-alpha> | ascii-digit
plain-value: <plain-val-char>+
<plain-val-char>: anything except space, control, '"', '#', `'`, '=', `\`
quoted-value: '"' <quoted-val-char>* '"'
<quoted-val-char>: <self-char> | `\` <quoted-char>
<self-char>: anything except control, `"`, `\`
<quoted-char>: 'a' | 'b' | 'f' | 'n' | 'r' | 'v' | `\` | '"' |
<octal-byte-value> | <hex-byte-value> | <unicode-value>
<octal-byte-value>: octal-digit{3}
<hex-byte-value>: 'x' hex-digit{2}
<unicode-value>: 'u' hex-digit{4} | 'U' hex-digit{8}
comment: '#' <any-char>*
ascii-alpha: [a-zA-Z]
ascii-digit: [0-9]
control: Unicode character class C (includes 00-1F and 80-9F)
space: Unicode character class Z
*/
package conf

import (
"bufio"
"errors"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"unicode"
)

// Value is the interface to the value pointed to by Var.
// Built-in Values also implement String() for compatibility with flag.Value.
// A usage example is in example/example.go.
type Value interface {
// Set receives a string value after it's been unquoted.
// The error it returns, if not nil, gets wrapped in ParseError.
Set(string) error
}

// StringValue represents a configuration variable's string value.
type StringValue string

func (v *StringValue) Set(s string) error {
*v = StringValue(s)
return nil
}

func (v *StringValue) String() string { return string(*v) }

// Uint64Value represents a configuration variable's uint64 value.
// Numeric values can be given as decimal, octal or hexadecimal,
// in the usual C/Go manner (255 == 0377 == 0xff).
type Uint64Value uint64

func (v *Uint64Value) Set(s string) error {
u, err := strconv.ParseUint(s, 0, 64)
if err != nil {
// strip fluff from strconf.ParseUint
return err.(*strconv.NumError).Err
}
*v = Uint64Value(u)
return nil
}

func (v *Uint64Value) String() string { return fmt.Sprintf("%d", *v) }

// Var describes a configuration variable and has pointers to corresponding
// (Go) variables. Slice of Var is used for calling Parse().
type Var struct {
Name string // name of configuration variable
Val Value // Value to set
Required bool // variable is required to be set in conf file
set bool // has been set
}

type parser struct {
r *bufio.Reader
file string
line int
ident string
value string
vars []Var
}

const (
outOfRange = "value out of range"
syntaxError = "syntax error"
)

// ParseError represents the error.
type ParseError struct {
File string // filename or "stdin"
Line int // line number or 0
Ident string // identifier or ""
Value string // value as appears in input, possibly quoted; or ""
Err error // error
}

// Error prints ParseError as follows:
// File:[Line:][ Ident:] Err
// Value never gets printed.
func (p *ParseError) Error() string {
var line, ident string
if p.Line != 0 {
line = fmt.Sprintf("%d:", p.Line)
}
if p.Ident != "" {
ident = fmt.Sprintf(" %s:", p.Ident)
}
return fmt.Sprintf("%s:%s%s %s\n", p.File, line, ident, p.Err)
}

// newError creates ParseError from s
func (p *parser) newError(s string) *ParseError {
return &ParseError{p.file, p.line, p.ident, p.value, errors.New(s)}
}

// Regexps for tokens
var (
identRE = regexp.MustCompile(`^[-_a-zA-Z][-_a-zA-Z0-9]*`)
plainRE = regexp.MustCompile(`^[^\pZ\pC"#'=\\]+`)
quotedRE = regexp.MustCompile(`^"(?:[^\pC"\\]|\\[^\pC])*"`)
)

func eatSpace(s string) string {
return strings.TrimLeftFunc(s, unicode.IsSpace)
}

func (p *parser) setValue(value string) error {
for i := range p.vars {
v := &p.vars[i]
if p.ident == v.Name {
if v.set {
return p.newError("already defined")
}
v.set = true
if err := v.Val.Set(value); err != nil {
return &ParseError{p.file, p.line, p.ident,
p.value, err}
}
return nil
}
}
return p.newError("unknown variable")
}

func (p *parser) parseLine(line string) error {
line = eatSpace(line)
if line == "" || line[0] == '#' {
return nil
}
p.ident = identRE.FindString(line)
line = eatSpace(line[len(p.ident):])
if p.ident == "" || line == "" || line[0] != '=' {
return p.newError(syntaxError)
}
line = eatSpace(line[1:])
p.value = plainRE.FindString(line)
unquoted := p.value
if p.value == "" {
p.value = quotedRE.FindString(line)
var err error
unquoted, err = strconv.Unquote(p.value)
if err != nil {
return p.newError(syntaxError)
}
}
line = eatSpace(line[len(p.value):])
if len(line) != 0 && line[0] != '#' {
return p.newError(syntaxError)
}
return p.setValue(unquoted)
}

// Parse parses the configuration file from r according the description
// in vars and sets the variables pointed to to the values in the file.
// The filename is used in error messages; if empty, it's set to "stdin".
// It returns nil on success, ParseError on parsing error and something
// from the depths of io on actual real error.
//
// Parsing stops on the first error encountered. Setting an unknown
// variable, setting a variable more than once or omitting a Var whose
// Required == true are errors.
//
// When parsing, the value gets unquoted if needed and the Var
// corresponding to the identifier is found. Then the Set() method
// is called to set the Var. If you need syntax validation, you
// should create your own Value type and return an error from Set()
// on invalid input.
//
// The parsing sequence implies that even when a number is desired,
// the quoted string "\x32\u0033" is the same as unquoted 23.
func Parse(r io.Reader, filename string, vars []Var) error {
p := &parser{file: filename, vars: vars}
if p.file == "" {
p.file = "stdin"
}
if t, ok := r.(*bufio.Reader); ok {
p.r = t
} else {
p.r = bufio.NewReader(r)
}
for {
p.line++
p.ident, p.value = "", ""
buf, ispref, err := p.r.ReadLine()
if err == io.EOF {
break
} else if err != nil {
return err
} else if ispref {
return p.newError("line too long")
}
if err = p.parseLine(string(buf)); err != nil {
return err
}
}
for _, v := range p.vars {
if v.Required && !v.set {
p.ident = v.Name
p.line, p.value = 0, ""
return p.newError("required but not set")
}
}
return nil
}
6 changes: 6 additions & 0 deletions example/Makefile
@@ -0,0 +1,6 @@
include $(GOROOT)/src/Make.inc

TARG=example
GOFILES=example.go

include $(GOROOT)/src/Make.cmd
5 changes: 5 additions & 0 deletions example/example.conf
@@ -0,0 +1,5 @@
# Example configuration file

string = http://[::1]:6060/
number = "2\U00000033"
key = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
49 changes: 49 additions & 0 deletions example/example.go
@@ -0,0 +1,49 @@
package main

import (
"errors"
"fmt"
"github.com/unixdj/conf"
"os"
"regexp"
)

var (
sval = "default value"
nval uint64
netKey []byte
netKeyRE = regexp.MustCompile(`^[0-9a-fA-F]{64}$`)
)

type netKeyValue []byte

func (key *netKeyValue) Set(s string) error {
if !netKeyRE.MatchString(s) {
return errors.New("invalid key (must be 64 hexadecimal digits)")
}
fmt.Sscanf(s, "%x", key) // will succeed
return nil
}

//func (key *netKeyValue) String() string { return fmt.Sprintf("%x", *key) }

func readConf(conffile string) error {
f, err := os.Open(conffile)
if err != nil {
return err
}
defer f.Close()
return conf.Parse(f, conffile, []conf.Var{
{Name: "string", Val: (*conf.StringValue)(&sval)},
{Name: "number", Val: (*conf.Uint64Value)(&nval)},
{Name: "key", Val: (*netKeyValue)(&netKey), Required: true},
})
}

func main() {
if err := readConf("example.conf"); err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Printf("string: %s\nnumber: %d\nkey: %x\n", sval, nval, netKey)
}

0 comments on commit 3c8a654

Please sign in to comment.