// Copyright 2013-2014 Frank Schroeder. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package properties // BUG(frank): Set() does not check for invalid unicode literals since this is currently handled by the lexer. // BUG(frank): Write() does not allow to configure the newline character. Therefore, on Windows LF is used. import ( "fmt" "io" "log" "os" "regexp" "strconv" "strings" "time" "unicode/utf8" ) // ErrorHandlerFunc defines the type of function which handles failures // of the MustXXX() functions. An error handler function must exit // the application after handling the error. type ErrorHandlerFunc func(error) // ErrorHandler is the function which handles failures of the MustXXX() // functions. The default is LogFatalHandler. var ErrorHandler = LogFatalHandler // LogFatalHandler handles the error by logging a fatal error and exiting. func LogFatalHandler(err error) { log.Fatal(err) } // PanicHandler handles the error by panicking. func PanicHandler(err error) { panic(err) } // ----------------------------------------------------------------------------- // A Properties contains the key/value pairs from the properties input. // All values are stored in unexpanded form and are expanded at runtime type Properties struct { // Pre-/Postfix for property expansion. Prefix string Postfix string // DisableExpansion controls the expansion of properties on Get() // and the check for circular references on Set(). When set to // true Properties behaves like a simple key/value store and does // not check for circular references on Get() or on Set(). DisableExpansion bool // 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 // configuration for "${key}" expressions. func NewProperties() *Properties { return &Properties{ Prefix: "${", Postfix: "}", m: map[string]string{}, c: map[string][]string{}, k: []string{}, } } // Get returns the expanded value for the given key if exists. // Otherwise, ok is false. func (p *Properties) Get(key string) (value string, ok bool) { v, ok := p.m[key] if p.DisableExpansion { return v, ok } if !ok { return "", false } expanded, err := p.expand(v) // 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 { ErrorHandler(fmt.Errorf("%s in %q", err, key+" = "+v)) } return expanded, true } // MustGet returns the expanded value for the given key if exists. // Otherwise, it panics. func (p *Properties) MustGet(key string) string { if v, ok := p.Get(key); ok { return v } ErrorHandler(invalidKeyError(key)) panic("ErrorHandler should exit") } // ---------------------------------------------------------------------------- // 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. func (p *Properties) GetBool(key string, def bool) bool { v, err := p.getBool(key) if err != nil { return def } return v } // MustGetBool 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 function panics. func (p *Properties) MustGetBool(key string) bool { v, err := p.getBool(key) if err != nil { ErrorHandler(err) } return v } func (p *Properties) getBool(key string) (value bool, err error) { if v, ok := p.Get(key); ok { return boolVal(v), nil } return false, invalidKeyError(key) } func boolVal(v string) bool { v = strings.ToLower(v) return v == "1" || v == "true" || v == "yes" || v == "on" } // ---------------------------------------------------------------------------- // GetDuration parses the expanded value as an time.Duration (in ns) if the // key exists. If key does not exist or the value cannot be parsed the default // value is returned. In almost all cases you want to use GetParsedDuration(). func (p *Properties) GetDuration(key string, def time.Duration) time.Duration { v, err := p.getInt64(key) if err != nil { return def } return time.Duration(v) } // MustGetDuration parses the expanded value as an time.Duration (in ns) if // the key exists. If key does not exist or the value cannot be parsed the // function panics. In almost all cases you want to use MustGetParsedDuration(). func (p *Properties) MustGetDuration(key string) time.Duration { v, err := p.getInt64(key) if err != nil { ErrorHandler(err) } return time.Duration(v) } // ---------------------------------------------------------------------------- // GetParsedDuration parses the expanded value with time.ParseDuration() if the key exists. // If key does not exist or the value cannot be parsed the default // value is returned. func (p *Properties) GetParsedDuration(key string, def time.Duration) time.Duration { s, ok := p.Get(key) if !ok { return def } v, err := time.ParseDuration(s) if err != nil { return def } return v } // MustGetParsedDuration parses the expanded value with time.ParseDuration() if the key exists. // If key does not exist or the value cannot be parsed the function panics. func (p *Properties) MustGetParsedDuration(key string) time.Duration { s, ok := p.Get(key) if !ok { ErrorHandler(invalidKeyError(key)) } v, err := time.ParseDuration(s) if err != nil { ErrorHandler(err) } return v } // ---------------------------------------------------------------------------- // GetFloat64 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) GetFloat64(key string, def float64) float64 { v, err := p.getFloat64(key) if err != nil { return def } return v } // MustGetFloat64 parses the expanded value as a float64 if the key exists. // If key does not exist or the value cannot be parsed the function panics. func (p *Properties) MustGetFloat64(key string) float64 { v, err := p.getFloat64(key) if err != nil { ErrorHandler(err) } return v } func (p *Properties) getFloat64(key string) (value float64, err error) { if v, ok := p.Get(key); ok { value, err = strconv.ParseFloat(v, 64) if err != nil { return 0, err } return value, nil } return 0, invalidKeyError(key) } // ---------------------------------------------------------------------------- // 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. If the value does not fit into an int the // function panics with an out of range error. func (p *Properties) GetInt(key string, def int) int { v, err := p.getInt64(key) if err != nil { return def } return intRangeCheck(key, v) } // MustGetInt parses the expanded value as an int if the key exists. // If key does not exist or the value cannot be parsed the function panics. // If the value does not fit into an int the function panics with // an out of range error. func (p *Properties) MustGetInt(key string) int { v, err := p.getInt64(key) if err != nil { ErrorHandler(err) } return intRangeCheck(key, v) } // ---------------------------------------------------------------------------- // GetInt64 parses the expanded value as an int64 if the key exists. // If key does not exist or the value cannot be parsed the default // value is returned. func (p *Properties) GetInt64(key string, def int64) int64 { v, err := p.getInt64(key) if err != nil { return def } return v } // MustGetInt64 parses the expanded value as an int if the key exists. // If key does not exist or the value cannot be parsed the function panics. func (p *Properties) MustGetInt64(key string) int64 { v, err := p.getInt64(key) if err != nil { ErrorHandler(err) } return v } func (p *Properties) getInt64(key string) (value int64, err error) { if v, ok := p.Get(key); ok { value, err = strconv.ParseInt(v, 10, 64) if err != nil { return 0, err } return value, nil } return 0, invalidKeyError(key) } // ---------------------------------------------------------------------------- // GetUint parses the expanded value as an uint if the key exists. // If key does not exist or the value cannot be parsed the default // value is returned. If the value does not fit into an int the // function panics with an out of range error. func (p *Properties) GetUint(key string, def uint) uint { v, err := p.getUint64(key) if err != nil { return def } return uintRangeCheck(key, v) } // MustGetUint parses the expanded value as an int if the key exists. // If key does not exist or the value cannot be parsed the function panics. // If the value does not fit into an int the function panics with // an out of range error. func (p *Properties) MustGetUint(key string) uint { v, err := p.getUint64(key) if err != nil { ErrorHandler(err) } return uintRangeCheck(key, v) } // ---------------------------------------------------------------------------- // GetUint64 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) GetUint64(key string, def uint64) uint64 { v, err := p.getUint64(key) if err != nil { return def } return v } // MustGetUint64 parses the expanded value as an int if the key exists. // If key does not exist or the value cannot be parsed the function panics. func (p *Properties) MustGetUint64(key string) uint64 { v, err := p.getUint64(key) if err != nil { ErrorHandler(err) } return v } func (p *Properties) getUint64(key string) (value uint64, err error) { if v, ok := p.Get(key); ok { value, err = strconv.ParseUint(v, 10, 64) if err != nil { return 0, err } return value, nil } return 0, invalidKeyError(key) } // ---------------------------------------------------------------------------- // 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 def } // MustGetString returns the expanded value for the given key if exists or // panics otherwise. func (p *Properties) MustGetString(key string) string { if v, ok := p.Get(key); ok { return v } ErrorHandler(invalidKeyError(key)) panic("ErrorHandler should exit") } // ---------------------------------------------------------------------------- // Filter returns a new properties object which contains all properties // for which the key matches the pattern. func (p *Properties) Filter(pattern string) (*Properties, error) { re, err := regexp.Compile(pattern) if err != nil { return nil, err } return p.FilterRegexp(re), nil } // FilterRegexp returns a new properties object which contains all properties // for which the key matches the regular expression. func (p *Properties) FilterRegexp(re *regexp.Regexp) *Properties { pp := NewProperties() for _, k := range p.k { if re.MatchString(k) { pp.Set(k, p.m[k]) } } return pp } // FilterPrefix returns a new properties object with a subset of all keys // with the given prefix. func (p *Properties) FilterPrefix(prefix string) *Properties { pp := NewProperties() for _, k := range p.k { if strings.HasPrefix(k, prefix) { pp.Set(k, p.m[k]) } } return pp } // FilterStripPrefix returns a new properties object with a subset of all keys // with the given prefix and the prefix removed from the keys. func (p *Properties) FilterStripPrefix(prefix string) *Properties { pp := NewProperties() n := len(prefix) for _, k := range p.k { if len(k) > len(prefix) && strings.HasPrefix(k, prefix) { pp.Set(k[n:], p.m[k]) } } return pp } // Len returns the number of keys. func (p *Properties) Len() int { return len(p.m) } // Keys returns all keys in the same order as in the input. func (p *Properties) Keys() []string { keys := make([]string, len(p.k)) for i, k := range p.k { keys[i] = k } return keys } // 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. // An empty key is silently ignored. func (p *Properties) Set(key, value string) (prev string, ok bool, err error) { if key == "" { return "", false, nil } // if expansion is disabled we allow circular references if p.DisableExpansion { prev, ok = p.Get(key) p.m[key] = value return prev, ok, 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 } 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.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. // 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 { // don't print comments if they are all empty allEmpty := true for _, c := range comments { if c != "" { allEmpty = false break } } if !allEmpty { // 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 } n += x } return } // ---------------------------------------------------------------------------- // Delete removes the key and its comments. func (p *Properties) Delete(key string) { delete(p.m, key) delete(p.c, key) newKeys := []string{} for _, k := range p.k { if k != key { newKeys = append(newKeys, key) } } p.k = newKeys } // ---------------------------------------------------------------------------- // 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 } func (p *Properties) expand(input string) (string, error) { // no pre/postfix -> nothing to expand if p.Prefix == "" && p.Postfix == "" { return input, nil } return expand(input, make(map[string]bool), p.Prefix, p.Postfix, p.m) } // 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 of the form '(prefix)key'. func expand(s string, keys map[string]bool, prefix, postfix string, values map[string]string) (string, error) { start := strings.Index(s, prefix) if start == -1 { return s, nil } keyStart := start + len(prefix) keyLen := strings.Index(s[keyStart:], postfix) if keyLen == -1 { return "", fmt.Errorf("malformed expression") } end := keyStart + keyLen + len(postfix) - 1 key := s[keyStart : keyStart+keyLen] // fmt.Printf("s:%q pp:%q start:%d end:%d keyStart:%d keyLen:%d key:%q\n", s, prefix + "..." + postfix, start, end, keyStart, keyLen, key) if _, ok := keys[key]; ok { return "", fmt.Errorf("circular reference") } val, ok := values[key] if !ok { val = os.Getenv(key) } // remember that we've seen the key keys[key] = true return expand(s[:start]+val+s[end+1:], keys, prefix, postfix, values) } // encode encodes a UTF-8 string to ISO-8859-1 and escapes some characters. 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 for pos := 0; pos < len(s); { switch r, w = utf8.DecodeRuneInString(s[pos:]); { case r < 1<<8: // single byte rune -> escape special chars only v += escape(r, special) case r < 1<<16: // two byte rune -> unicode literal v += fmt.Sprintf("\\u%04x", r) default: // more than two bytes per rune -> can't encode v += "?" } pos += w } return v } func escape(r rune, special string) string { switch r { case '\f': return "\\f" case '\n': return "\\n" case '\r': return "\\r" case '\t': return "\\t" default: if strings.ContainsRune(special, r) { return "\\" + string(r) } return string(r) } } func invalidKeyError(key string) error { return fmt.Errorf("unknown property: %s", key) }