Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include directives #18

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Doc/AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ explanation.*

* * *

[Andrew Bashkatov](https://github.com/workanator)
[Jonas mg](https://github.com/kless)
[Miguel Branco](https://github.com/msbranco)
[Rob Figueiredo](https://github.com/robfig)
[Tom Bruggeman](https://github.com/tmbrggmn)

2 changes: 1 addition & 1 deletion Doc/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ because the organization holds the copyright.*

### Other authors

[Andrew Bashkatov](https://github.com/workanator)
[Jonas mg](https://github.com/kless)
[Tom Bruggeman](https://github.com/tmbrggmn)

7 changes: 6 additions & 1 deletion Doc/NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@

* * *

### 2017-03-17 v0.10.0

+ Support for #include directive.

+ #include and #require supports globs.

### 2011-??-?? v0.9.6

+ Changed to line comments.
Expand Down Expand Up @@ -49,4 +55,3 @@ comment and separator, and the spaces around separator.
after of each new line.

+ Better documentation.

26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,34 @@ This results in the file:

Note that sections, options and values are all case-sensitive.

## Configuration loader directives

The loader which is responsible for loading configuration files supports
some directives which can be handy in some situations, .e.g in case
multi-file configuration is used.

Directives are started with hash `#` and followed with the name of the
directive and zero, one or many arguments. For example,
`#include extra/user.cfg`.

### Including files

The loader can be instructed to load other configuration file(s) with
directive `#include`. The directive requires loading files to exist what
means if the loader fails loading one of including files it will fail
loading the whole configuration.

The syntax of the directive is `#include <path>` where `<path>` is the relative
or absolute path to the file which should be loaded,
e.g. `#include db.cfg`, `#include /etc/my_tool/extra.cfg`. The path
can contain globs (or wildcards), e.g. `#include user/*.cfg`.

Please notice that all relative paths are relative to the main file path,
the path you passed into `config.Read()` or `config.ReadDefault()`.

## License

The source files are distributed under the [Mozilla Public License, version 2.0](http://mozilla.org/MPL/2.0/),
unless otherwise noted.
Please read the [FAQ](http://www.mozilla.org/MPL/2.0/FAQ.html)
if you have further questions regarding the license.

34 changes: 29 additions & 5 deletions all_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import (
)

const (
tmpFilename = "testdata/__test.go"
sourceFilename = "testdata/source.cfg"
targetFilename = "testdata/target.cfg"
tmpFilename = "testdata/__test.go"
sourceFilename = "testdata/source.cfg"
targetFilename = "testdata/target.cfg"
failedAbsIncludeFilename = "testdata/failed_abs_include.cfg"
failedRelIncludeFilename = "testdata/failed_rel_include.cfg"
)

func testGet(t *testing.T, c *Config, section string, option string,
Expand Down Expand Up @@ -364,12 +366,12 @@ func TestSectionOptions(t *testing.T) {
func TestMerge(t *testing.T) {
target, error := ReadDefault(targetFilename)
if error != nil {
t.Fatalf("Unable to read target config file '%s'", targetFilename)
t.Fatalf("Unable to read target config file '%s' because %s", targetFilename, error)
}

source, error := ReadDefault(sourceFilename)
if error != nil {
t.Fatalf("Unable to read source config file '%s'", sourceFilename)
t.Fatalf("Unable to read source config file '%s' because %s", sourceFilename, error)
}

target.Merge(source)
Expand Down Expand Up @@ -398,3 +400,25 @@ func TestMerge(t *testing.T) {
t.Errorf("Expected '[X] x.four' to be 'x4' but instead it was '%s'", result)
}
}

// TestFailedAbsInclude tests loading file with invalid #include with
// absolute path
func TestFailedAbsInclude(t *testing.T) {
_, error := Read(failedAbsIncludeFilename, DEFAULT_COMMENT, DEFAULT_SEPARATOR, false, false)
if error != nil {
t.Logf("Unable to read config file '%s' because %s", failedAbsIncludeFilename, error)
} else {
t.Errorf("Load of config file '%s' must fail", failedAbsIncludeFilename)
}
}

// TestFailedRelInclude tests loading file with invalid #include with
// relative path
func TestFailedRelInclude(t *testing.T) {
_, error := ReadDefault(failedRelIncludeFilename)
if error != nil {
t.Logf("Unable to read config file '%s' because %s", failedRelIncludeFilename, error)
} else {
t.Errorf("Load of config file '%s' must fail", failedRelIncludeFilename)
}
}
143 changes: 132 additions & 11 deletions read.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,44 +18,156 @@ import (
"bufio"
"errors"
"os"
"path/filepath"
"regexp"
"strings"
"unicode"
)

// _read is the base to read a file and get the configuration representation.
var (
// The regexp test if the path contains globs * and/or ?
reGlob = regexp.MustCompile(`[\*\?]`)
// The regexp is for matching include file directive
reIncludeFile = regexp.MustCompile(`^#include\s+(.+?)\s*$`)
)

// configFile identifies a file which should be read.
type configFile struct {
Path string
Read bool
}

// fileList is the list of files to read.
type fileList []*configFile

// pushFile converts the path into the absolute path and pushes the file
// into the list if it does not contain the same absolute path already.
// All relative paths are relative to the main file which is the first
// file in the list.
func (list *fileList) pushFile(path string) error {
var (
absPath string
err error
)

// Convert the path into the absolute path
if !filepath.IsAbs(path) {
// Make the path relative to the main file
var relPath string
if len(*list) > 0 {
// Join the relative path with the main file path
relPath = filepath.Join(filepath.Dir((*list)[0].Path), path)
} else {
relPath = path
}

if absPath, err = filepath.Abs(relPath); err != nil {
return err
}
} else {
absPath = path
}

// Make the list of file candidates to include
var candidates []string
if reGlob.MatchString(absPath) {
candidates, err = filepath.Glob(absPath)
if err != nil {
return err
}
} else {
candidates = []string{absPath}
}

for _, candidate := range candidates {
// Test the file with the absolute path exists in the list
for _, file := range *list {
if file.Path == candidate {
return nil
}
}

// Push the new file to the list
*list = append(*list, &configFile{
Path: candidate,
Read: false,
})
}

return nil
}

// _read reads file list
func _read(c *Config, list *fileList) (*Config, error) {
// Pass through the list untill all files are read
for {
hasUnread := false

// Go through the list and read files
for _, file := range *list {
if !file.Read {
if err := _readFile(file.Path, c, list); err != nil {
return nil, err
}

file.Read = true
hasUnread = true
}
}

// Exit the loop because all files are read
if !hasUnread {
break
}
}

return c, nil
}

// _readFile is the base to read a file and get the configuration representation.
// That representation can be queried with GetString, etc.
func _read(fname string, c *Config) (*Config, error) {
func _readFile(fname string, c *Config, list *fileList) error {
file, err := os.Open(fname)
if err != nil {
return nil, err
return err
}

if err = c.read(bufio.NewReader(file)); err != nil {
return nil, err
// Defer closing the file so we can be sure the underlying file handle
// will be closed in any case.
defer file.Close()

if err = c.read(bufio.NewReader(file), list); err != nil {
return err
}

if err = file.Close(); err != nil {
return nil, err
return err
}

return c, nil
return nil
}

// Read reads a configuration file and returns its representation.
// All arguments, except `fname`, are related to `New()`
func Read(fname string, comment, separator string, preSpace, postSpace bool) (*Config, error) {
return _read(fname, New(comment, separator, preSpace, postSpace))
list := &fileList{}
list.pushFile(fname)

return _read(New(comment, separator, preSpace, postSpace), list)
}

// ReadDefault reads a configuration file and returns its representation.
// It uses values by default.
func ReadDefault(fname string) (*Config, error) {
return _read(fname, NewDefault())
list := &fileList{}
list.pushFile(fname)

return _read(NewDefault(), list)
}

// * * *

func (c *Config) read(buf *bufio.Reader) (err error) {
func (c *Config) read(buf *bufio.Reader, list *fileList) (err error) {
var section, option string
var scanner = bufio.NewScanner(buf)
for scanner.Scan() {
Expand All @@ -64,9 +176,18 @@ func (c *Config) read(buf *bufio.Reader) (err error) {
// Switch written for readability (not performance)
switch {
// Empty line and comments
case len(l) == 0, l[0] == '#', l[0] == ';':
case len(l) == 0, l[0] == ';':
continue

// Comments starting with ;
case l[0] == '#':
// Test for possible directives
if matches := reIncludeFile.FindStringSubmatch(l); matches != nil {
list.pushFile(matches[1])
} else {
continue
}

// New section. The [ must be at the start of the line
case l[0] == '[' && l[len(l)-1] == ']':
option = "" // reset multi-line value
Expand Down
2 changes: 2 additions & 0 deletions testdata/failed_abs_include.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
; Just try to load file with invalid absolute path.
#include /targets/X.cfg
5 changes: 5 additions & 0 deletions testdata/failed_rel_include.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[Some]
value1=1
value2=TWO

#include this_file_does_not_exists.cfg
17 changes: 4 additions & 13 deletions testdata/target.cfg
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
; Include the file target_Y.cfg to included and the file on load will
; inform the loader to load all configurations files in targets subdirectory.
#include targets/*.cfg

one=1
two=2
three=3
five=5

two_+_three=%(two)s + %(three)s

[X]
x.one=x1
x.two=x2
x.three=x3
x.four=x4

[Y]
y.one=y1
y.two=y2
y.three=y3
y.four=y4

9 changes: 9 additions & 0 deletions testdata/targets/X.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
; This is a circular reference.
; Remember relative paths are relative to the main file.
#include targets/Y.cfg

[X]
x.one=x1
x.two=x2
x.three=x3
x.four=x4
11 changes: 11 additions & 0 deletions testdata/targets/Y.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
; Remember relative paths are relative to the main file.
#include targets/X.cfg

; Try to require self
#include targets/Y.cfg

[Y]
y.one=y1
y.two=y2
y.three=y3
y.four=y4