Treat circular references and malformed expressions as errors.
Add Set() method for setting values.
Renamed GetDefault() to GetString().
Added GetFloat, GetInt, GetUint and GetBool convenience methods.
diff --git a/doc.go b/doc.go
index a11d46b..c140a35 100644
--- a/doc.go
+++ b/doc.go
@@ -2,14 +2,50 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// goproperties provides functions for reading
-// ISO-8859-1 (Java) and UTF-8 encoded .properties files and has
-// support for Spring-like property expansion.
+// goproperties provides functions for reading and writing
+// ISO-8859-1 and UTF-8 encoded .properties files and has
+// support for recursive property expansion.
//
+// Java properties files are ISO-8859-1 encoded and use Unicode
+// literals for characters outside the ISO character set. Unicode
+// literals can be used in UTF-8 encoded properties files but
+// aren't necessary.
+//
+// All of the different key/value delimiters " :=" are supported
+// as well as the comment characters '!' and '#' and multi-line
+// values.
+//
+// ! this is a comment
+// # and so is this
+//
+// # the following expressions are equal
+// key value
+// key=value
+// key:value
// key = value
-// # key2 = value
+// key : value
+// key = val\
+// ue
+//
+// Property expansion is recursive and circular references
+// and malformed expressions are not allowed and cause an
+// error.
+//
+// # standard property
+// key = value
+//
+// # property expansion: key2 = value
// key2 = ${key}
//
+// # recursive property expansion: key3 = value
+// key3 = ${key2}
+//
+// # circular reference (error)
+// key = ${key}
+//
+// # malformed expression (error)
+// key = ${ke
+//
// The default property expansion format is ${key} but can be
// changed by setting different pre- and postfix values on the
// Properties object.
@@ -18,20 +54,7 @@
// p.Prefix = "#["
// p.Postfix = "]#"
//
-// Property expansion is recursive and circular references are not allowed.
-// If a circular reference is detected an error is logged and the
-// unexpanded value is returned.
-//
-// # Circular reference
-// key = ${key}
-//
-// # Malformed expression
-// key = ${ke
-//
-// When writing properties to a writer currently only ISO-8859-1 encoding
-// is supported.
-//
-// See one of the following links for a description of the properties
+// The following documents provide a description of the properties
// file format.
//
// http://en.wikipedia.org/wiki/.properties
diff --git a/example_test.go b/example_test.go
index 18081aa..14811d5 100644
--- a/example_test.go
+++ b/example_test.go
@@ -31,9 +31,9 @@
// UTF-8 value with unicode character ⌘ and umlaut ä
}
-func Example_Properties_GetDefault() {
+func Example_Properties_GetString() {
p, _ := Load([]byte("key=value"), ISO_8859_1)
- v := p.GetDefault("another key", "default value")
+ v := p.GetString("another key", "default value")
fmt.Println(v)
// Output:
// default value
@@ -57,7 +57,7 @@
}
// Get a key with a default value
- v := p.GetDefault("does not exist", "some value")
+ v := p.GetString("does not exist", "some value")
fmt.Println(v)
// Dump the expanded key/value pairs of the Properties
diff --git a/lex.go b/lex.go
index 562b85b..b1e71bf 100644
--- a/lex.go
+++ b/lex.go
@@ -322,7 +322,7 @@
return l.scanUnicodeLiteral()
case isEOF(r):
- return fmt.Errorf("premature EOF")
+ return fmt.Errorf("Premature EOF")
// silently drop the escape character and append the rune as is
default:
@@ -339,7 +339,7 @@
for i := 0; i < 4; i++ {
d[i] = l.next()
if d[i] == eof || !strings.ContainsRune("0123456789abcdefABCDEF", d[i]) {
- return fmt.Errorf("invalid unicode literal")
+ return fmt.Errorf("Invalid unicode literal")
}
}
diff --git a/load.go b/load.go
index b77da41..a0fad7f 100644
--- a/load.go
+++ b/load.go
@@ -50,7 +50,12 @@
)
func loadBuf(buf []byte, enc Encoding) (*Properties, error) {
- return parse(convert(buf, enc))
+ p, err := parse(convert(buf, enc))
+ if err != nil {
+ return nil, err
+ }
+
+ return p, p.check()
}
func loadFiles(filenames []string, enc Encoding, ignoreMissing bool) (*Properties, error) {
@@ -96,6 +101,6 @@
}
return string(runes)
default:
- panic(fmt.Sprintf("unsupported encoding %v", enc))
+ panic(fmt.Sprintf("Unsupported encoding %v", enc))
}
}
diff --git a/properties.go b/properties.go
index 4b3b5d1..f87af49 100644
--- a/properties.go
+++ b/properties.go
@@ -7,7 +7,7 @@
import (
"fmt"
"io"
- "log"
+ "strconv"
"strings"
"unicode/utf8"
)
@@ -39,22 +39,75 @@
expanded, err := p.expand(v)
- // if there is an error then this is a format exception which we just log
- // and return the input unchanged.
+ // we guarantee that the expanded value is free of
+ // circular references and malformed expressions
+ // so we panic if we still get an error here.
if err != nil {
- log.Printf("%s in %q", err, key+" = "+v)
- return v, true
+ panic(fmt.Errorf("%s in %q", err, key+" = "+v))
}
return expanded, true
}
-// GetDefault returns the expanded value for the given key if exists or the default value otherwise.
-func (p *Properties) GetDefault(key, defaultValue string) (value string) {
+// GetBool checks if the expanded value is one of '1', 'yes',
+// 'true' or 'on' if the key exists. The comparison is case-insensitive.
+// If the key does not exist the default value is returned.
+func (p *Properties) GetBool(key string, def bool) bool {
+ if v, ok := p.Get(key); ok {
+ v = strings.ToLower(v)
+ return v == "1" || v == "true" || v == "yes" || v == "on"
+ }
+ return def
+}
+
+// GetFloat parses the expanded value as a float64 if the key exists.
+// If key does not exist or the value cannot be parsed the default
+// value is returned.
+func (p *Properties) GetFloat(key string, def float64) float64 {
+ if v, ok := p.Get(key); ok {
+ n, err := strconv.ParseFloat(v, 64)
+ if err != nil {
+ return def
+ }
+ return n
+ }
+ return def
+}
+
+// GetInt parses the expanded value as an int if the key exists.
+// If key does not exist or the value cannot be parsed the default
+// value is returned.
+func (p *Properties) GetInt(key string, def int64) int64 {
+ if v, ok := p.Get(key); ok {
+ n, err := strconv.ParseInt(v, 10, 64)
+ if err != nil {
+ return def
+ }
+ return n
+ }
+ return def
+}
+
+// GetUint parses the expanded value as an uint64 if the key exists.
+// If key does not exist or the value cannot be parsed the default
+// value is returned.
+func (p *Properties) GetUint(key string, def uint64) uint64 {
+ if v, ok := p.Get(key); ok {
+ n, err := strconv.ParseUint(v, 10, 64)
+ if err != nil {
+ return def
+ }
+ return n
+ }
+ return def
+}
+
+// GetString returns the expanded value for the given key if exists or the default value otherwise.
+func (p *Properties) GetString(key, def string) string {
if v, ok := p.Get(key); ok {
return v
}
- return defaultValue
+ return def
}
// Len returns the number of keys.
@@ -62,6 +115,23 @@
return len(p.m)
}
+// Set sets the property key to the corresponding value.
+// If a value for key existed before then ok is true and prev
+// contains the previous value. If the value contains a
+// circular reference or a malformed expression then
+// an error is returned.
+func (p *Properties) Set(key, value string) (prev string, ok bool, err error) {
+ // TODO(frank): Check for invalid unicode literals since this is currently done in the lexer.
+ _, err = p.expand(value)
+ if err != nil {
+ return "", false, err
+ }
+
+ v, ok := p.Get(key)
+ p.m[key] = value
+ return v, ok, nil
+}
+
// Dump returns a string of all unexpanded 'key = value' pairs.
func (p *Properties) Dump() string {
var s string
@@ -81,11 +151,11 @@
return s
}
-// Write writes all unexpanded 'key = value' pairs as ISO-8859-1 to the given writer.
-func (p *Properties) Write(w io.Writer) (int, error) {
+// Write writes all unexpanded 'key = value' pairs to the given writer.
+func (p *Properties) Write(w io.Writer, enc Encoding) (int, error) {
total := 0
for key, value := range p.m {
- s := fmt.Sprintf("%s = %s\n", encode(key, " :"), encode(value, ""))
+ s := fmt.Sprintf("%s = %s\n", encode(key, " :", enc), encode(value, "", enc))
n, err := w.Write([]byte(s))
if err != nil {
return total, err
@@ -95,6 +165,19 @@
return total, nil
}
+// ----------------------------------------------------------------------------
+
+// check expands all values and returns an error if a circular reference or
+// a malformed expression was found.
+func (p *Properties) check() error {
+ for _, value := range p.m {
+ if _, err := p.expand(value); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
// expand recursively expands expressions of '(prefix)key(postfix)' to their corresponding values.
// The function keeps track of the keys that were already expanded and stops if it
// detects a circular reference or a malformed expression.
@@ -136,7 +219,28 @@
}
// encode encodes a UTF-8 string to ISO-8859-1 and escapes some characters.
-func encode(s string, special string) string {
+func encode(s string, special string, enc Encoding) string {
+ switch enc {
+ case UTF8:
+ return encodeUtf8(s, special)
+ case ISO_8859_1:
+ return encodeIso(s, special)
+ default:
+ panic(fmt.Sprintf("Unsupported encoding %v", enc))
+ }
+}
+
+func encodeUtf8(s string, special string) string {
+ v := ""
+ for pos := 0; pos < len(s); {
+ r, w := utf8.DecodeRuneInString(s[pos:])
+ pos += w
+ v += escape(r, special)
+ }
+ return v
+}
+
+func encodeIso(s string, special string) string {
var r rune
var w int
var v string
diff --git a/properties_test.go b/properties_test.go
index 0522007..36950e3 100644
--- a/properties_test.go
+++ b/properties_test.go
@@ -83,33 +83,41 @@
// expansion tests
{"key=value\nkey2=${key}", "key", "value", "key2", "value"},
{"key=value\nkey2=${key}\nkey3=${key2}", "key", "value", "key2", "value", "key3", "value"},
-
- // circular references
- {"key=${key}", "key", "${key}"},
- {"key1=${key2}\nkey2=${key1}", "key1", "${key2}", "key2", "${key1}"},
-
- // malformed expressions
- {"key=${ke", "key", "${ke"},
- {"key=valu${ke", "key", "valu${ke"},
}
// define error test cases in the form of
// {"input", "expected error message"}
var errorTests = [][]string{
- {"key\\u1 = value", "invalid unicode literal"},
- {"key\\u12 = value", "invalid unicode literal"},
- {"key\\u123 = value", "invalid unicode literal"},
- {"key\\u123g = value", "invalid unicode literal"},
- {"key\\u123", "invalid unicode literal"},
+ // unicode literals
+ {"key\\u1 = value", "Invalid unicode literal"},
+ {"key\\u12 = value", "Invalid unicode literal"},
+ {"key\\u123 = value", "Invalid unicode literal"},
+ {"key\\u123g = value", "Invalid unicode literal"},
+ {"key\\u123", "Invalid unicode literal"},
+
+ // circular references
+ {"key=${key}", "Circular reference"},
+ {"key1=${key2}\nkey2=${key1}", "Circular reference"},
+
+ // malformed expressions
+ {"key=${ke", "Malformed expression"},
+ {"key=valu${ke", "Malformed expression"},
}
// define write encoding test cases in the form of
-// {"input", "expected output after write"}
+// {"input", "expected output after write", ["UTF-8", "ISO-8859-1"]}
var writeTests = [][]string{
- {"key = value", "key = value\n"},
- {"key = value \\\n continued", "key = value continued\n"},
- {"key⌘ = value", "key\\u2318 = value\n"},
- {"ke\\ \\:y = value", "ke\\ \\:y = value\n"},
+ // ISO-8859-1 tests
+ {"key = value", "key = value\n", "ISO-8859-1"},
+ {"key = value \\\n continued", "key = value continued\n", "ISO-8859-1"},
+ {"key⌘ = value", "key\\u2318 = value\n", "ISO-8859-1"},
+ {"ke\\ \\:y = value", "ke\\ \\:y = value\n", "ISO-8859-1"},
+
+ // UTF-8 tests
+ {"key = value", "key = value\n", "UTF-8"},
+ {"key = value \\\n continued", "key = value continued\n", "UTF-8"},
+ {"key⌘ = value⌘", "key⌘ = value⌘\n", "UTF-8"},
+ {"ke\\ \\:y = value", "ke\\ \\:y = value\n", "UTF-8"},
}
// Benchmarks the decoder by creating a property file with 1000 key/value pairs.
@@ -149,11 +157,17 @@
// Test write encoding.
func (l *TestSuite) TestWrite(c *C) {
for _, test := range writeTests {
- input, output := test[0], test[1]
+ input, output, enc := test[0], test[1], test[2]
p, err := parse(input)
buf := new(bytes.Buffer)
- n, err := p.Write(buf)
+ var n int
+ switch enc {
+ case "UTF-8":
+ n, err = p.Write(buf, UTF8)
+ case "ISO-8859-1":
+ n, err = p.Write(buf, ISO_8859_1)
+ }
c.Assert(err, IsNil)
s := string(buf.Bytes())
c.Assert(n, Equals, len(output), Commentf("input=%q expected=%q obtained=%q", input, output, s))