Added support for setting and clearing comments. Return keys in the order they are in the original input. Add support for writing properties files with comments.
diff --git a/parser.go b/parser.go index a7028ca..297326c 100644 --- a/parser.go +++ b/parser.go
@@ -32,6 +32,9 @@ continue case itemKey: key = token.val + if _, ok := properties.m[key]; !ok { + properties.k = append(properties.k, key) + } } token = p.expectOneOf(itemValue, itemEOF)
diff --git a/properties.go b/properties.go index 898320e..ccf91fa 100644 --- a/properties.go +++ b/properties.go
@@ -45,8 +45,14 @@ Prefix string Postfix string + // Stores the key/value pairs m map[string]string + + // Stores the comments per key. c map[string][]string + + // Stores the keys in order of appearance. + k []string } // NewProperties creates a new Properties struct with the default @@ -55,8 +61,9 @@ return &Properties{ Prefix: "${", Postfix: "}", - m: make(map[string]string), - c: make(map[string][]string), + m: map[string]string{}, + c: map[string][]string{}, + k: []string{}, } } @@ -92,8 +99,14 @@ // ---------------------------------------------------------------------------- -// GetComment returns the last comment before the given key or an -// empty string. +// ClearComments removes the comments for all keys. +func (p *Properties) ClearComments() { + p.c = map[string][]string{} +} + +// ---------------------------------------------------------------------------- + +// GetComment returns the last comment before the given key or an empty string. func (p *Properties) GetComment(key string) string { comments, ok := p.c[key] if !ok || len(comments) == 0 { @@ -114,6 +127,25 @@ // ---------------------------------------------------------------------------- +// SetComment sets the comment for the key. +func (p *Properties) SetComment(key, comment string) { + p.c[key] = []string{comment} +} + +// ---------------------------------------------------------------------------- + +// SetComments sets the comments for the key. If the comments are nil then +// all comments for this key are deleted. +func (p *Properties) SetComments(key string, comments []string) { + if comments == nil { + delete(p.c, key) + return + } + p.c[key] = comments +} + +// ---------------------------------------------------------------------------- + // 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. @@ -384,11 +416,11 @@ return len(p.m) } -// Keys returns all keys. +// Keys returns all keys in the same order as in the input. func (p *Properties) Keys() []string { - keys := make([]string, 0, len(p.m)) - for k, _ := range p.m { - keys = append(keys, k) + keys := make([]string, len(p.k)) + for i, k := range p.k { + keys[i] = k } return keys } @@ -412,7 +444,7 @@ // String returns a string of all expanded 'key = value' pairs. func (p *Properties) String() string { var s string - for key, _ := range p.m { + for _, key := range p.k { value, _ := p.Get(key) s = fmt.Sprintf("%s%s = %s\n", s, key, value) } @@ -420,17 +452,51 @@ } // 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, " :", enc), encode(value, "", enc)) - n, err := w.Write([]byte(s)) - if err != nil { - return total, err +// Write returns the number of bytes written and any write error encountered. +func (p *Properties) Write(w io.Writer, enc Encoding) (n int, err error) { + return p.WriteComment(w, "", enc) +} + +// WriteComment writes all unexpanced 'key = value' pairs to the given writer. +// If prefix is not empty then comments are written with a blank line and the +// given prefix. The prefix should be either "#" or "!" to be compatible with +// the properties file format. Otherwise, the properties parser will not be +// able to read the file back in. It returns the number of bytes written and +// any write error encountered. +func (p *Properties) WriteComment(w io.Writer, prefix string, enc Encoding) (n int, err error) { + var x int + + for _, key := range p.k { + value := p.m[key] + + if prefix != "" { + if comments, ok := p.c[key]; ok { + // add a blank line between entries but not at the top + if len(comments) > 0 && n > 0 { + x, err = fmt.Fprintln(w) + if err != nil { + return + } + n += x + } + + for _, c := range comments { + x, err = fmt.Fprintf(w, "%s %s\n", prefix, encode(c, "", enc)) + if err != nil { + return + } + n += x + } + } } - total += n + + x, err = fmt.Fprintf(w, "%s = %s\n", encode(key, " :", enc), encode(value, "", enc)) + if err != nil { + return + } + n += x } - return total, nil + return } // ----------------------------------------------------------------------------
diff --git a/properties_test.go b/properties_test.go index f0c6e49..a1b02bf 100644 --- a/properties_test.go +++ b/properties_test.go
@@ -109,6 +109,8 @@ {"key=${USER}\nUSER=value", "key", "value", "USER", "value"}, } +// ---------------------------------------------------------------------------- + var commentTests = []struct { input, key, value string comments []string @@ -128,6 +130,8 @@ {"! comment1\n\n! comment2\n\nkey=value", "key", "value", []string{"comment1", "comment2"}}, } +// ---------------------------------------------------------------------------- + var errorTests = []struct { input, msg string }{ @@ -147,6 +151,8 @@ {"key=valu${ke", "Malformed expression"}, } +// ---------------------------------------------------------------------------- + var writeTests = []struct { input, output, encoding string }{ @@ -165,6 +171,28 @@ // ---------------------------------------------------------------------------- +var writeCommentTests = []struct { + input, output, encoding string +}{ + // ISO-8859-1 tests + {"key = value", "key = value\n", "ISO-8859-1"}, + {"# comment\nkey = value", "# comment\nkey = value\n", "ISO-8859-1"}, + {"\n# comment\nkey = value", "# comment\nkey = value\n", "ISO-8859-1"}, + {"# comment\n\nkey = value", "# comment\nkey = value\n", "ISO-8859-1"}, + {"# comment1\n# comment2\nkey = value", "# comment1\n# comment2\nkey = value\n", "ISO-8859-1"}, + {"#comment1\nkey1 = value1\n#comment2\nkey2 = value2", "# comment1\nkey1 = value1\n\n# comment2\nkey2 = value2\n", "ISO-8859-1"}, + + // UTF-8 tests + {"key = value", "key = value\n", "UTF-8"}, + {"# comment⌘\nkey = value⌘", "# comment⌘\nkey = value⌘\n", "UTF-8"}, + {"\n# comment⌘\nkey = value⌘", "# comment⌘\nkey = value⌘\n", "UTF-8"}, + {"# comment⌘\n\nkey = value⌘", "# comment⌘\nkey = value⌘\n", "UTF-8"}, + {"# comment1⌘\n# comment2⌘\nkey = value⌘", "# comment1⌘\n# comment2⌘\nkey = value⌘\n", "UTF-8"}, + {"#comment1⌘\nkey1 = value1⌘\n#comment2⌘\nkey2 = value2⌘", "# comment1⌘\nkey1 = value1⌘\n\n# comment2⌘\nkey2 = value2⌘\n", "UTF-8"}, +} + +// ---------------------------------------------------------------------------- + var boolTests = []struct { input, key string def, value bool @@ -299,6 +327,7 @@ {"", []string{}}, {"key = abc", []string{"key"}}, {"key = abc\nkey2=def", []string{"key", "key2"}}, + {"key2 = abc\nkey=def", []string{"key2", "key"}}, {"key = abc\nkey=def", []string{"key"}}, } @@ -530,6 +559,26 @@ } else { c.Assert(p.GetComment(test.key), Equals, "") } + + // test setting comments + if len(test.comments) > 0 { + // set single comment + p.ClearComments() + c.Assert(len(p.c), Equals, 0) + p.SetComment(test.key, test.comments[0]) + c.Assert(p.GetComment(test.key), Equals, test.comments[0]) + + // set multiple comments + p.ClearComments() + c.Assert(len(p.c), Equals, 0) + p.SetComments(test.key, test.comments) + c.Assert(p.GetComments(test.key), DeepEquals, test.comments) + + // clear comments for a key + p.SetComments(test.key, nil) + c.Assert(p.GetComment(test.key), Equals, "") + c.Assert(p.GetComments(test.key), IsNil) + } } } @@ -577,12 +626,10 @@ c.Assert(err, IsNil) c.Assert(p.Len(), Equals, len(test.keys)) c.Assert(len(p.Keys()), Equals, len(test.keys)) - for _, key := range test.keys { - _, ok := p.Get(key) - c.Assert(ok, Equals, true) - } + c.Assert(p.Keys(), DeepEquals, test.keys) } } + func (l *TestSuite) TestWrite(c *C) { for _, test := range writeTests { p, err := parse(test.input) @@ -602,6 +649,25 @@ } } +func (l *TestSuite) TestWriteComment(c *C) { + for _, test := range writeCommentTests { + p, err := parse(test.input) + + buf := new(bytes.Buffer) + var n int + switch test.encoding { + case "UTF-8": + n, err = p.WriteComment(buf, "#", UTF8) + case "ISO-8859-1": + n, err = p.WriteComment(buf, "#", ISO_8859_1) + } + c.Assert(err, IsNil) + s := string(buf.Bytes()) + c.Assert(n, Equals, len(test.output), Commentf("input=%q expected=%q obtained=%q", test.input, test.output, s)) + c.Assert(s, Equals, test.output, Commentf("input=%q expected=%q obtained=%q", test.input, test.output, s)) + } +} + func (l *TestSuite) TestCustomExpansionExpression(c *C) { testKeyValuePrePostfix(c, "*[", "]*", "key=value\nkey2=*[key]*", "key", "value", "key2", "value") }