diff --git a/Doc/AUTHORS.md b/Doc/AUTHORS.md index fb97cdc..cbc13f3 100644 --- a/Doc/AUTHORS.md +++ b/Doc/AUTHORS.md @@ -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) - diff --git a/Doc/CONTRIBUTORS.md b/Doc/CONTRIBUTORS.md index 14d36da..f00469d 100644 --- a/Doc/CONTRIBUTORS.md +++ b/Doc/CONTRIBUTORS.md @@ -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) - diff --git a/Doc/NEWS.md b/Doc/NEWS.md index 7d59d09..1116f40 100644 --- a/Doc/NEWS.md +++ b/Doc/NEWS.md @@ -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. @@ -49,4 +55,3 @@ comment and separator, and the spaces around separator. after of each new line. + Better documentation. - diff --git a/README.md b/README.md index 8a6f0c6..9cb68aa 100644 --- a/README.md +++ b/README.md @@ -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 ` where `` 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. - diff --git a/all_test.go b/all_test.go index 571f99e..c66c050 100644 --- a/all_test.go +++ b/all_test.go @@ -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, @@ -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) @@ -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) + } +} diff --git a/read.go b/read.go index 595d6d9..99e4490 100644 --- a/read.go +++ b/read.go @@ -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() { @@ -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 diff --git a/testdata/failed_abs_include.cfg b/testdata/failed_abs_include.cfg new file mode 100644 index 0000000..37e7453 --- /dev/null +++ b/testdata/failed_abs_include.cfg @@ -0,0 +1,2 @@ +; Just try to load file with invalid absolute path. +#include /targets/X.cfg diff --git a/testdata/failed_rel_include.cfg b/testdata/failed_rel_include.cfg new file mode 100644 index 0000000..6851a97 --- /dev/null +++ b/testdata/failed_rel_include.cfg @@ -0,0 +1,5 @@ +[Some] +value1=1 +value2=TWO + +#include this_file_does_not_exists.cfg diff --git a/testdata/target.cfg b/testdata/target.cfg index 1ecf025..c83360e 100644 --- a/testdata/target.cfg +++ b/testdata/target.cfg @@ -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 - diff --git a/testdata/targets/X.cfg b/testdata/targets/X.cfg new file mode 100644 index 0000000..e3fc676 --- /dev/null +++ b/testdata/targets/X.cfg @@ -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 diff --git a/testdata/targets/Y.cfg b/testdata/targets/Y.cfg new file mode 100644 index 0000000..c3117c1 --- /dev/null +++ b/testdata/targets/Y.cfg @@ -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