Skip to content

Commit

Permalink
Merge branch 'comment_branch'
Browse files Browse the repository at this point in the history
  • Loading branch information
magiconair committed Nov 18, 2014
2 parents 316179a + c1cfcf9 commit 94befa3
Show file tree
Hide file tree
Showing 6 changed files with 452 additions and 171 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ environment variables like in `${USER}`.
Filenames can also contain environment variables like in
`/home/${USER}/myapp.properties`.

Comments and the order of keys are preserved. Comments can be modified
and can be written to the output.

The properties library supports both ISO-8859-1 and UTF-8 encoded data.

Starting from version 1.3.0 the behavior of the MustXXX() functions is
Expand Down Expand Up @@ -41,9 +44,23 @@ Installation and Upgrade
$ go get -u github.com/magiconair/properties
```

For testing and debugging you need the [go-check](https://github.com/go-check/check) library

```
$ go get -u gopkg.in/check.v1
```

History
-------

v1.5.0, 18 Nov 2014
-------------------
* Added support for single and multi-line comments (reading, writing and updating)
* The order of keys is now preserved
* Calling Set() with an empty key now silently ignores the call and does not create a new entry
* Added a MustSet() method
* Migrated test library from launchpad.net/gocheck to gopkg.in/check.v1

v1.4.2, 15 Nov 2014
-------------------
* Issue #2: Fixed goroutine leak in parser which created two lexers but cleaned up only one
Expand Down
11 changes: 8 additions & 3 deletions lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ type itemType int
const (
itemError itemType = iota // error occurred; value is text of error
itemEOF
itemKey // a key
itemValue // a value
itemKey // a key
itemValue // a value
itemComment // a comment
)

// defines a constant for EOF
Expand Down Expand Up @@ -207,15 +208,19 @@ func lexBeforeKey(l *lexer) stateFn {

// lexComment scans a comment line. The comment character has already been scanned.
func lexComment(l *lexer) stateFn {
l.acceptRun(whitespace)
l.ignore()
for {
switch r := l.next(); {
case isEOF(r):
l.ignore()
l.emit(itemEOF)
return nil
case isEOL(r):
l.ignore()
l.emit(itemComment)
return lexBeforeKey
default:
l.appendRune(r)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"os"
"strings"

. "launchpad.net/gocheck"
. "gopkg.in/check.v1"
)

type LoadSuite struct {
Expand Down
42 changes: 31 additions & 11 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,39 @@ func parse(input string) (properties *Properties, err error) {
defer p.recover(&err)

properties = NewProperties()
key := ""
comments := []string{}

for {
token := p.expectOneOf(itemKey, itemEOF)
if token.typ == itemEOF {
break
token := p.expectOneOf(itemComment, itemKey, itemEOF)
switch token.typ {
case itemEOF:
goto done
case itemComment:
comments = append(comments, token.val)
continue
case itemKey:
key = token.val
if _, ok := properties.m[key]; !ok {
properties.k = append(properties.k, key)
}
}
key := token.val

token = p.expectOneOf(itemValue, itemEOF)
if token.typ == itemEOF {
if len(comments) > 0 {
properties.c[key] = comments
comments = []string{}
}
switch token.typ {
case itemEOF:
properties.m[key] = ""
break
goto done
case itemValue:
properties.m[key] = token.val
}
properties.m[key] = token.val
}

done:
return properties, nil
}

Expand All @@ -50,12 +67,15 @@ func (p *parser) expect(expected itemType) (token item) {
return token
}

func (p *parser) expectOneOf(expected1, expected2 itemType) (token item) {
func (p *parser) expectOneOf(expected ...itemType) (token item) {
token = p.lex.nextItem()
if token.typ != expected1 && token.typ != expected2 {
p.unexpected(token)
for _, v := range expected {
if token.typ == v {
return token
}
}
return token
p.unexpected(token)
panic("unexpected token")
}

func (p *parser) unexpected(token item) {
Expand Down
158 changes: 141 additions & 17 deletions properties.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,14 @@ type Properties struct {
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
Expand All @@ -54,7 +61,9 @@ func NewProperties() *Properties {
return &Properties{
Prefix: "${",
Postfix: "}",
m: make(map[string]string),
m: map[string]string{},
c: map[string][]string{},
k: []string{},
}
}

Expand Down Expand Up @@ -90,6 +99,53 @@ func (p *Properties) MustGet(key string) 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 {
return ""
}
return comments[len(comments)-1]
}

// ----------------------------------------------------------------------------

// GetComments returns all comments that appeared before the given key or nil.
func (p *Properties) GetComments(key string) []string {
if comments, ok := p.c[key]; ok {
return comments
}
return nil
}

// ----------------------------------------------------------------------------

// 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.
Expand Down Expand Up @@ -360,11 +416,11 @@ func (p *Properties) Len() int {
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
}
Expand All @@ -374,39 +430,107 @@ func (p *Properties) Keys() []string {
// contains the previous value. If the value contains a
// circular reference or a malformed expression then
// an error is returned.
// An empty key is silently ignored.
func (p *Properties) Set(key, value string) (prev string, ok bool, err error) {
if key == "" {
return "", false, nil
}

// to check for a circular reference we temporarily need
// to set the new value. If there is an error then revert
// to the previous state. Only if all tests are successful
// then we add the key to the p.k list.
prev, ok = p.Get(key)
p.m[key] = value

// now check for a circular reference
_, err = p.expand(value)
if err != nil {

// revert to the previous state
if ok {
p.m[key] = prev
} else {
delete(p.m, key)
}

return "", false, err
}

v, ok := p.Get(key)
p.m[key] = value
return v, ok, nil
if !ok {
p.k = append(p.k, key)
}

return prev, ok, nil
}

// MustSet 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. An empty key is silently ignored.
func (p *Properties) MustSet(key, value string) (prev string, ok bool) {
prev, ok, err := p.Set(key, value)
if err != nil {
ErrorHandler(err)
}
return prev, ok
}

// 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)
}
return s
}

// 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))
// 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
}
}
}

x, err = fmt.Fprintf(w, "%s = %s\n", encode(key, " :", enc), encode(value, "", enc))
if err != nil {
return total, err
return
}
total += n
n += x
}
return total, nil
return
}

// ----------------------------------------------------------------------------
Expand Down
Loading

0 comments on commit 94befa3

Please sign in to comment.