Skip to content

Commit 94befa3

Browse files
committed
Merge branch 'comment_branch'
2 parents 316179a + c1cfcf9 commit 94befa3

File tree

6 files changed

+452
-171
lines changed

6 files changed

+452
-171
lines changed

README.md

+17
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ environment variables like in `${USER}`.
1010
Filenames can also contain environment variables like in
1111
`/home/${USER}/myapp.properties`.
1212

13+
Comments and the order of keys are preserved. Comments can be modified
14+
and can be written to the output.
15+
1316
The properties library supports both ISO-8859-1 and UTF-8 encoded data.
1417

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

47+
For testing and debugging you need the [go-check](https://github.com/go-check/check) library
48+
49+
```
50+
$ go get -u gopkg.in/check.v1
51+
```
52+
4453
History
4554
-------
4655

56+
v1.5.0, 18 Nov 2014
57+
-------------------
58+
* Added support for single and multi-line comments (reading, writing and updating)
59+
* The order of keys is now preserved
60+
* Calling Set() with an empty key now silently ignores the call and does not create a new entry
61+
* Added a MustSet() method
62+
* Migrated test library from launchpad.net/gocheck to gopkg.in/check.v1
63+
4764
v1.4.2, 15 Nov 2014
4865
-------------------
4966
* Issue #2: Fixed goroutine leak in parser which created two lexers but cleaned up only one

lex.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ type itemType int
4444
const (
4545
itemError itemType = iota // error occurred; value is text of error
4646
itemEOF
47-
itemKey // a key
48-
itemValue // a value
47+
itemKey // a key
48+
itemValue // a value
49+
itemComment // a comment
4950
)
5051

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

208209
// lexComment scans a comment line. The comment character has already been scanned.
209210
func lexComment(l *lexer) stateFn {
211+
l.acceptRun(whitespace)
212+
l.ignore()
210213
for {
211214
switch r := l.next(); {
212215
case isEOF(r):
213216
l.ignore()
214217
l.emit(itemEOF)
215218
return nil
216219
case isEOL(r):
217-
l.ignore()
220+
l.emit(itemComment)
218221
return lexBeforeKey
222+
default:
223+
l.appendRune(r)
219224
}
220225
}
221226
}

load_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"os"
1111
"strings"
1212

13-
. "launchpad.net/gocheck"
13+
. "gopkg.in/check.v1"
1414
)
1515

1616
type LoadSuite struct {

parser.go

+31-11
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,39 @@ func parse(input string) (properties *Properties, err error) {
1818
defer p.recover(&err)
1919

2020
properties = NewProperties()
21+
key := ""
22+
comments := []string{}
2123

2224
for {
23-
token := p.expectOneOf(itemKey, itemEOF)
24-
if token.typ == itemEOF {
25-
break
25+
token := p.expectOneOf(itemComment, itemKey, itemEOF)
26+
switch token.typ {
27+
case itemEOF:
28+
goto done
29+
case itemComment:
30+
comments = append(comments, token.val)
31+
continue
32+
case itemKey:
33+
key = token.val
34+
if _, ok := properties.m[key]; !ok {
35+
properties.k = append(properties.k, key)
36+
}
2637
}
27-
key := token.val
2838

2939
token = p.expectOneOf(itemValue, itemEOF)
30-
if token.typ == itemEOF {
40+
if len(comments) > 0 {
41+
properties.c[key] = comments
42+
comments = []string{}
43+
}
44+
switch token.typ {
45+
case itemEOF:
3146
properties.m[key] = ""
32-
break
47+
goto done
48+
case itemValue:
49+
properties.m[key] = token.val
3350
}
34-
properties.m[key] = token.val
3551
}
3652

53+
done:
3754
return properties, nil
3855
}
3956

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

53-
func (p *parser) expectOneOf(expected1, expected2 itemType) (token item) {
70+
func (p *parser) expectOneOf(expected ...itemType) (token item) {
5471
token = p.lex.nextItem()
55-
if token.typ != expected1 && token.typ != expected2 {
56-
p.unexpected(token)
72+
for _, v := range expected {
73+
if token.typ == v {
74+
return token
75+
}
5776
}
58-
return token
77+
p.unexpected(token)
78+
panic("unexpected token")
5979
}
6080

6181
func (p *parser) unexpected(token item) {

properties.go

+141-17
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,14 @@ type Properties struct {
4545
Prefix string
4646
Postfix string
4747

48+
// Stores the key/value pairs
4849
m map[string]string
50+
51+
// Stores the comments per key.
52+
c map[string][]string
53+
54+
// Stores the keys in order of appearance.
55+
k []string
4956
}
5057

5158
// NewProperties creates a new Properties struct with the default
@@ -54,7 +61,9 @@ func NewProperties() *Properties {
5461
return &Properties{
5562
Prefix: "${",
5663
Postfix: "}",
57-
m: make(map[string]string),
64+
m: map[string]string{},
65+
c: map[string][]string{},
66+
k: []string{},
5867
}
5968
}
6069

@@ -90,6 +99,53 @@ func (p *Properties) MustGet(key string) string {
9099

91100
// ----------------------------------------------------------------------------
92101

102+
// ClearComments removes the comments for all keys.
103+
func (p *Properties) ClearComments() {
104+
p.c = map[string][]string{}
105+
}
106+
107+
// ----------------------------------------------------------------------------
108+
109+
// GetComment returns the last comment before the given key or an empty string.
110+
func (p *Properties) GetComment(key string) string {
111+
comments, ok := p.c[key]
112+
if !ok || len(comments) == 0 {
113+
return ""
114+
}
115+
return comments[len(comments)-1]
116+
}
117+
118+
// ----------------------------------------------------------------------------
119+
120+
// GetComments returns all comments that appeared before the given key or nil.
121+
func (p *Properties) GetComments(key string) []string {
122+
if comments, ok := p.c[key]; ok {
123+
return comments
124+
}
125+
return nil
126+
}
127+
128+
// ----------------------------------------------------------------------------
129+
130+
// SetComment sets the comment for the key.
131+
func (p *Properties) SetComment(key, comment string) {
132+
p.c[key] = []string{comment}
133+
}
134+
135+
// ----------------------------------------------------------------------------
136+
137+
// SetComments sets the comments for the key. If the comments are nil then
138+
// all comments for this key are deleted.
139+
func (p *Properties) SetComments(key string, comments []string) {
140+
if comments == nil {
141+
delete(p.c, key)
142+
return
143+
}
144+
p.c[key] = comments
145+
}
146+
147+
// ----------------------------------------------------------------------------
148+
93149
// GetBool checks if the expanded value is one of '1', 'yes',
94150
// 'true' or 'on' if the key exists. The comparison is case-insensitive.
95151
// If the key does not exist the default value is returned.
@@ -360,11 +416,11 @@ func (p *Properties) Len() int {
360416
return len(p.m)
361417
}
362418

363-
// Keys returns all keys.
419+
// Keys returns all keys in the same order as in the input.
364420
func (p *Properties) Keys() []string {
365-
keys := make([]string, 0, len(p.m))
366-
for k, _ := range p.m {
367-
keys = append(keys, k)
421+
keys := make([]string, len(p.k))
422+
for i, k := range p.k {
423+
keys[i] = k
368424
}
369425
return keys
370426
}
@@ -374,39 +430,107 @@ func (p *Properties) Keys() []string {
374430
// contains the previous value. If the value contains a
375431
// circular reference or a malformed expression then
376432
// an error is returned.
433+
// An empty key is silently ignored.
377434
func (p *Properties) Set(key, value string) (prev string, ok bool, err error) {
435+
if key == "" {
436+
return "", false, nil
437+
}
438+
439+
// to check for a circular reference we temporarily need
440+
// to set the new value. If there is an error then revert
441+
// to the previous state. Only if all tests are successful
442+
// then we add the key to the p.k list.
443+
prev, ok = p.Get(key)
444+
p.m[key] = value
445+
446+
// now check for a circular reference
378447
_, err = p.expand(value)
379448
if err != nil {
449+
450+
// revert to the previous state
451+
if ok {
452+
p.m[key] = prev
453+
} else {
454+
delete(p.m, key)
455+
}
456+
380457
return "", false, err
381458
}
382459

383-
v, ok := p.Get(key)
384-
p.m[key] = value
385-
return v, ok, nil
460+
if !ok {
461+
p.k = append(p.k, key)
462+
}
463+
464+
return prev, ok, nil
465+
}
466+
467+
// MustSet sets the property key to the corresponding value.
468+
// If a value for key existed before then ok is true and prev
469+
// contains the previous value. An empty key is silently ignored.
470+
func (p *Properties) MustSet(key, value string) (prev string, ok bool) {
471+
prev, ok, err := p.Set(key, value)
472+
if err != nil {
473+
ErrorHandler(err)
474+
}
475+
return prev, ok
386476
}
387477

388478
// String returns a string of all expanded 'key = value' pairs.
389479
func (p *Properties) String() string {
390480
var s string
391-
for key, _ := range p.m {
481+
for _, key := range p.k {
392482
value, _ := p.Get(key)
393483
s = fmt.Sprintf("%s%s = %s\n", s, key, value)
394484
}
395485
return s
396486
}
397487

398488
// Write writes all unexpanded 'key = value' pairs to the given writer.
399-
func (p *Properties) Write(w io.Writer, enc Encoding) (int, error) {
400-
total := 0
401-
for key, value := range p.m {
402-
s := fmt.Sprintf("%s = %s\n", encode(key, " :", enc), encode(value, "", enc))
403-
n, err := w.Write([]byte(s))
489+
// Write returns the number of bytes written and any write error encountered.
490+
func (p *Properties) Write(w io.Writer, enc Encoding) (n int, err error) {
491+
return p.WriteComment(w, "", enc)
492+
}
493+
494+
// WriteComment writes all unexpanced 'key = value' pairs to the given writer.
495+
// If prefix is not empty then comments are written with a blank line and the
496+
// given prefix. The prefix should be either "# " or "! " to be compatible with
497+
// the properties file format. Otherwise, the properties parser will not be
498+
// able to read the file back in. It returns the number of bytes written and
499+
// any write error encountered.
500+
func (p *Properties) WriteComment(w io.Writer, prefix string, enc Encoding) (n int, err error) {
501+
var x int
502+
503+
for _, key := range p.k {
504+
value := p.m[key]
505+
506+
if prefix != "" {
507+
if comments, ok := p.c[key]; ok {
508+
// add a blank line between entries but not at the top
509+
if len(comments) > 0 && n > 0 {
510+
x, err = fmt.Fprintln(w)
511+
if err != nil {
512+
return
513+
}
514+
n += x
515+
}
516+
517+
for _, c := range comments {
518+
x, err = fmt.Fprintf(w, "%s%s\n", prefix, encode(c, "", enc))
519+
if err != nil {
520+
return
521+
}
522+
n += x
523+
}
524+
}
525+
}
526+
527+
x, err = fmt.Fprintf(w, "%s = %s\n", encode(key, " :", enc), encode(value, "", enc))
404528
if err != nil {
405-
return total, err
529+
return
406530
}
407-
total += n
531+
n += x
408532
}
409-
return total, nil
533+
return
410534
}
411535

412536
// ----------------------------------------------------------------------------

0 commit comments

Comments
 (0)