Updated README.
Fixed FF whitespace escaping.
Renamed Decode* to Load*.
diff --git a/README.md b/README.md
index b551d65..c0015cf 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,20 @@
 Overview
 ========
 
-goproperties is a Go library for parsing Java property files.
+goproperties is a Go library for reading and writing Java property files.
+
+It supports reading properties from multiple files and Spring style property
+expansion of expressions of '${key}' to their corresponding value.
 
 The current version supports reading both ISO-8859-1 and UTF-8 encoded data.
 
-A future version will also support Spring Framework style property expansion like
+Install
+-------
 
-	key = value
-	key2 = ${key}
-
-History
-=======
-
-v0.9 - Initial release
+	$ go get github.com/magiconair/goproperties
 
 Usage
-=====
+-----
 
 	buf, err := ioutil.ReadFile(filename)
 	if err != nil {
@@ -29,10 +27,36 @@
 	}
 
 	value, ok := p.Get("key")
+	if ok {
+		fmt.Println(value)
+	}
 
-Import
-======
+or
 
-	go get github.com/magiconair/goproperties
+	p, err := goproperties.DecodeString("key = value")
+	if err != nil {
+		// handle error
+	}
 
+	value, ok := p.Get("key")
+	if ok {
+		fmt.Println(value)
+	}
 
+History
+-------
+
+v0.9, 17 Dec 2013 - Initial release
+
+License
+-------
+
+2 clause BSD license. See LICENSE file for details.
+
+Parts of the lexer are taken from the template/text/parser package
+For these parts the following applies:
+
+Copyright 2011 The Go Authors. All rights reserved.
+Use of this source code is governed by a BSD-style
+license that can be found in the LICENSE file of the go 1.2
+distribution.
diff --git a/decoder.go b/decoder.go
deleted file mode 100644
index 9a8b90d..0000000
--- a/decoder.go
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2013 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 goproperties
-
-import (
-	"fmt"
-)
-
-type encoding uint
-
-const (
-	enc_utf8 encoding = 1 << iota
-	enc_iso_8859_1
-)
-
-// Decodes an ISO-8859-1 encoded buffer into a Properties struct.
-func Decode(buf []byte) (Properties, error) {
-	return decodeWithEncoding(buf, enc_iso_8859_1)
-}
-
-// Decodes an UTF-8 string into a Properties struct.
-func DecodeFromString(input string) (Properties, error) {
-	return decodeWithEncoding([]byte(input), enc_utf8)
-}
-
-// Decodes either an ISO-8859-1 or an UTF-8 encoded string into a Properties struct.
-func decodeWithEncoding(buf []byte, enc encoding) (Properties, error) {
-	return newParser().Parse(convert(buf, enc))
-}
-
-// The Java properties spec says that .properties files must be ISO-8859-1
-// encoded. Since the first 256 unicode code points cover ISO-8859-1 we
-// can convert each byte straight into a rune and use the resulting string
-// as UTF-8 input for the parser.
-func convert(buf []byte, enc encoding) string {
-	switch enc {
-	case enc_utf8:
-		return string(buf)
-	case enc_iso_8859_1:
-		runes := make([]rune, len(buf))
-		for i, b := range buf {
-			runes[i] = rune(b)
-		}
-		return string(runes)
-	default:
-		panic(fmt.Sprintf("unsupported encoding %v", enc))
-	}
-}
diff --git a/doc.go b/doc.go
index 17992fd..f3ea1fe 100644
--- a/doc.go
+++ b/doc.go
@@ -2,66 +2,19 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// Package goproperties reads Java properties files.
+// goproperties provides functions for reading and writing
+// Java properties files and has support for Spring like
+// property expansion.
 //
-// Java properties files contain key/value pairs in one of the following form:
+// By default, if a value contains a reference '${key}' then
+// getting the value will recursively expand the key to its value.
+// The format is configurable and circular references are not allowed.
 //
-//   key value
-//   key = value
-//   key : value
+// See one of the following links for a description of the properties
+// file format.
 //
-// The value is optional and ends with EOF or a new line which can either be '\n', '\r' or "\r\n".
-// Therefore, the following expression is legal and results in a key with an empty value:
+// http://en.wikipedia.org/wiki/.properties
 //
-//   key
-//
-// Whitespace before the key and around the delimiter is ignored. Whitespace at the end of the value is part of the value.
-// Besides the space ' ' (U+0020) character the TAB (U+0009) and FF (U+000C) characters are also treated as whitespace.
-// Therefore, the following expressions are equal:
-//
-//   key=value
-//      key=value
-//   key= value
-//   key =value
-//   key = value
-//   key   =   value
-//   key\f=\fvalue
-//   key\t=\tvalue
-//
-// Blank lines and comment lines starting with '#' or '!' and are ignored until the end of the line.
-//
-//   # the next line is empty and will be ignored
-//
-//   ! this is a comment
-//   key = value
-//
-// If the delimiter characters '=' and ':' appear in either key or value then
-// they must be escaped with a backslash. Because of this the backslash must
-// also be escaped. The characters '\n', '\r' or '\t' can be part of both key
-// or value and must be escaped. For all other characters the backslash is
-// silently dropped.
-//
-//   # key:1 = value=2
-//   key\:1 = value\=2
-//
-//   # key = value	with	tabs
-//   key = value\twith\ttabs
-//
-//   # key = value with silently dropped backslash
-//   key = v\alu\e with silently dropped backslash
-//
-// Values can span multiple lines by using a backslash before the newline character.
-// All subsequent whitespace on the following line is ignored. Comment lines cannot be
-// extended like this.
-//
-//   # key = value continued
-//   key = value \
-//         continued
-//
-// Java properties files are ISO-8559-1 encoded and can have Unicode literals for
-// characters outside the character set. Unicode literals are specified as \uXXXX.
-//
-//   # key = value with €
-//   key = value with \u20AC
+// http://docs.oracle.com/javase/7/docs/api/java/util/Properties.html#load%28java.io.Reader%29
 //
 package goproperties
diff --git a/example_test.go b/example_test.go
index 1c128cc..d8ee275 100644
--- a/example_test.go
+++ b/example_test.go
@@ -6,13 +6,14 @@
 
 import (
 	"fmt"
+	"log"
 )
 
-func ExampleDecode() {
+func ExampleLoad() {
 	buf := []byte("key = ISO-8859-1 value with unicode literal \\u2318 and umlaut ")
 	buf = append(buf, 0xE4) // 0xE4 == ä
-	p, _ := Decode(buf)
-	v, ok := p["key"]
+	p, _ := Load(buf)
+	v, ok := p.Get("key")
 	fmt.Println(ok)
 	fmt.Println(v)
 	// Output:
@@ -20,12 +21,61 @@
 	// ISO-8859-1 value with unicode literal ⌘ and umlaut ä
 }
 
-func ExampleDecodeFromString() {
-	p, _ := DecodeFromString("key = UTF-8 value with unicode character ⌘ and umlaut ä")
-	v, ok := p["key"]
+func ExampleLoadString() {
+	p, _ := LoadString("key = UTF-8 value with unicode character ⌘ and umlaut ä")
+	v, ok := p.Get("key")
 	fmt.Println(ok)
 	fmt.Println(v)
 	// Output:
 	// true
 	// UTF-8 value with unicode character ⌘ and umlaut ä
 }
+
+func Example_Properties_GetDefault() {
+	p, _ := LoadString("key=value")
+	v := p.GetDefault("another key", "default value")
+	fmt.Println(v)
+	// Output:
+	// default value
+}
+
+func Example() {
+	// Decode some key/value pairs with expressions
+	p, err := LoadString("key=value\nkey2=${key}")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Get a valid key
+	if v, ok := p.Get("key"); ok {
+		fmt.Println(v)
+	}
+
+	// Get an invalid key
+	if _, ok := p.Get("does not exist"); !ok {
+		fmt.Println("invalid key")
+	}
+
+	// Get a key with a default value
+	v := p.GetDefault("does not exist", "some value")
+	fmt.Println(v)
+
+	// Dump the expanded key/value pairs of the Properties
+	fmt.Println("Expanded key/value pairs")
+	fmt.Println(p)
+
+	// Dump the raw key/value pairs.
+	fmt.Println("Raw key/value pairs")
+	fmt.Println(p.Dump())
+	// Output:
+	// value
+	// invalid key
+	// some value
+	// Expanded key/value pairs
+	// key = value
+	// key2 = value
+	//
+	// Raw key/value pairs
+	// key = value
+	// key2 = ${key}
+}
diff --git a/lex.go b/lex.go
index 80506bb..562b85b 100644
--- a/lex.go
+++ b/lex.go
@@ -48,8 +48,10 @@
 	itemValue // a value
 )
 
+// defines a constant for EOF
 const eof = -1
 
+// permitted whitespace characters space, FF and TAB
 const whitespace = " \f\t"
 
 // stateFn represents the state of the scanner as a function that returns the next state.
@@ -322,8 +324,11 @@
 	case isEOF(r):
 		return fmt.Errorf("premature EOF")
 
+	// silently drop the escape character and append the rune as is
 	default:
-		return fmt.Errorf("invalid escape sequence %s", string(r))
+		l.appendRune(r)
+		return nil
+		// return fmt.Errorf("invalid escape sequence %s", string(r))
 	}
 }
 
@@ -351,6 +356,8 @@
 // decodeEscapedCharacter returns the unescaped rune. We expect to be after the escape character.
 func decodeEscapedCharacter(r rune) rune {
 	switch r {
+	case 'f':
+		return '\f'
 	case 'n':
 		return '\n'
 	case 'r':
@@ -397,7 +404,7 @@
 // isEscapedCharacter reports whether we are at one of the characters that need escaping.
 // The escape character has already been consumed.
 func isEscapedCharacter(r rune) bool {
-	return strings.ContainsRune(" :=nrt", r)
+	return strings.ContainsRune(" :=fnrt", r)
 }
 
 // isWhitespace reports whether the rune is a whitespace character.
diff --git a/load.go b/load.go
new file mode 100644
index 0000000..ecb9439
--- /dev/null
+++ b/load.go
@@ -0,0 +1,100 @@
+// Copyright 2013 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 goproperties
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+)
+
+// Load reads an ISO-8859-1 encoded buffer into a Properties struct.
+func Load(buf []byte) (*Properties, error) {
+	return loadBuf(buf, enc_iso_8859_1)
+}
+
+// LoadString reads an UTF-8 string into a Properties struct.
+func LoadString(input string) (*Properties, error) {
+	return loadBuf([]byte(input), enc_utf8)
+}
+
+// LoadFile reads a file into a Properties struct.
+func LoadFile(filename string) (*Properties, error) {
+	return loadFiles([]string{filename}, false)
+}
+
+// LoadFiles reads multiple file in the given order into
+// a Properties struct. If 'ignoreMissing' is 'true' then
+// non-existent files will not be reported as error.
+func LoadFiles(filenames []string, ignoreMissing bool) (*Properties, error) {
+	return loadFiles(filenames, ignoreMissing)
+}
+
+// MustLoadFile reads a file into a Properties struct and panics on error.
+func MustLoadFile(filename string) *Properties {
+	return MustLoadFiles([]string{filename}, false)
+}
+
+// MustLoadFiles reads multiple file in the given order into
+// a Properties struct and panics on error.
+// If 'ignoreMissing' is 'true' then non-existent files will not be reported as error.
+func MustLoadFiles(filenames []string, ignoreMissing bool) *Properties {
+	p, err := loadFiles(filenames, ignoreMissing)
+	if err != nil {
+		panic(err)
+	}
+	return p
+}
+
+type encoding uint
+
+const (
+	enc_utf8 encoding = 1 << iota
+	enc_iso_8859_1
+)
+
+// Loads either an ISO-8859-1 or an UTF-8 encoded string into a Properties struct.
+func loadBuf(buf []byte, enc encoding) (*Properties, error) {
+	return parse(convert(buf, enc))
+}
+
+func loadFiles(filenames []string, ignoreMissing bool) (*Properties, error) {
+	buff := make([]byte, 0, 4096)
+
+	for _, filename := range filenames {
+		buf, err := ioutil.ReadFile(filename)
+		if err != nil {
+			if ignoreMissing && os.IsNotExist(err) {
+				// TODO(frank): should we log that we are skipping the file?
+				continue
+			}
+			return nil, err
+		}
+
+		// concatenate the buffers and add a new line in case
+		// the previous file didn't end with a new line
+		buff = append(append(buff, buf...), '\n')
+	}
+
+	return loadBuf(buff, enc_iso_8859_1)
+}
+
+// Interprets a byte buffer either as ISO-8859-1 or UTF-8 encoded string.
+// For ISO-8859-1 we can convert each byte straight into a rune since the
+// first 256 unicode code points cover ISO-8859-1.
+func convert(buf []byte, enc encoding) string {
+	switch enc {
+	case enc_utf8:
+		return string(buf)
+	case enc_iso_8859_1:
+		runes := make([]rune, len(buf))
+		for i, b := range buf {
+			runes[i] = rune(b)
+		}
+		return string(runes)
+	default:
+		panic(fmt.Sprintf("unsupported encoding %v", enc))
+	}
+}
diff --git a/load_test.go b/load_test.go
new file mode 100644
index 0000000..42fe1d7
--- /dev/null
+++ b/load_test.go
@@ -0,0 +1,121 @@
+// Copyright 2013 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 goproperties
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+
+	. "launchpad.net/gocheck"
+)
+
+type LoadSuite struct {
+	tempFiles []string
+}
+
+var (
+	_ = Suite(&LoadSuite{})
+)
+
+// ----------------------------------------------------------------------------
+
+func (s *LoadSuite) TestLoadFailsWithNotExistingFile(c *C) {
+	_, err := LoadFile("doesnotexist.properties")
+	c.Assert(err, NotNil)
+	c.Assert(err, ErrorMatches, "open.*no such file or directory")
+}
+
+// ----------------------------------------------------------------------------
+
+func (s *LoadSuite) TestLoadFilesFailsOnNotExistingFile(c *C) {
+	_, err := LoadFiles([]string{"doesnotexist.properties"}, false)
+	c.Assert(err, NotNil)
+	c.Assert(err, ErrorMatches, "open.*no such file or directory")
+}
+
+// ----------------------------------------------------------------------------
+
+func (s *LoadSuite) TestLoadFilesDoesNotFailOnNotExistingFileAndIgnoreMissing(c *C) {
+	p, err := LoadFiles([]string{"doesnotexist.properties"}, true)
+	c.Assert(err, IsNil)
+	c.Assert(p.Len(), Equals, 0)
+}
+
+// ----------------------------------------------------------------------------
+
+func (s *LoadSuite) TestLoad(c *C) {
+	filename := s.makeFile(c, "key=value")
+	p := MustLoadFile(filename)
+
+	c.Assert(p.Len(), Equals, 1)
+	assertKeyValues(c, "", p, "key", "value")
+}
+
+// ----------------------------------------------------------------------------
+
+func (s *LoadSuite) TestLoadFiles(c *C) {
+	filename := s.makeFile(c, "key=value")
+	filename2 := s.makeFile(c, "key2=value2")
+	p := MustLoadFiles([]string{filename, filename2}, false)
+	assertKeyValues(c, "", p, "key", "value", "key2", "value2")
+}
+
+// ----------------------------------------------------------------------------
+
+func (s *LoadSuite) TestLoadFilesAndIgnoreMissing(c *C) {
+	filename := s.makeFile(c, "key=value")
+	filename2 := s.makeFile(c, "key2=value2")
+	p := MustLoadFiles([]string{filename, filename + "foo", filename2, filename2 + "foo"}, true)
+	assertKeyValues(c, "", p, "key", "value", "key2", "value2")
+}
+
+// ----------------------------------------------------------------------------
+
+func (s *LoadSuite) SetUpSuite(c *C) {
+	s.tempFiles = make([]string, 0)
+}
+
+// ----------------------------------------------------------------------------
+
+func (s *LoadSuite) TearDownSuite(c *C) {
+	for _, path := range s.tempFiles {
+		err := os.Remove(path)
+		if err != nil {
+			fmt.Printf("os.Remove: %v", err)
+		}
+	}
+}
+
+// ----------------------------------------------------------------------------
+
+func (s *LoadSuite) makeFile(c *C, data string) string {
+	f, err := ioutil.TempFile("", "properties")
+	if err != nil {
+		fmt.Printf("ioutil.TempFile: %v", err)
+		c.FailNow()
+	}
+
+	// remember the temp file so that we can remove it later
+	s.tempFiles = append(s.tempFiles, f.Name())
+
+	n, err := fmt.Fprint(f, data)
+	if err != nil {
+		fmt.Printf("fmt.Fprintln: %v", err)
+		c.FailNow()
+	}
+	if n != len(data) {
+		fmt.Printf("Data size mismatch. expected=%d wrote=%d\n", len(data), n)
+		c.FailNow()
+	}
+
+	err = f.Close()
+	if err != nil {
+		fmt.Printf("f.Close: %v", err)
+		c.FailNow()
+	}
+
+	return f.Name()
+}
diff --git a/parser.go b/parser.go
index 849449b..8cb73d8 100644
--- a/parser.go
+++ b/parser.go
@@ -13,14 +13,12 @@
 	lex *lexer
 }
 
-func newParser() *parser {
-	return &parser{}
-}
-
-func (p *parser) Parse(input string) (props Properties, err error) {
+func parse(input string) (properties *Properties, err error) {
+	p := &parser{lex: lex(input)}
 	defer p.recover(&err)
+
 	p.lex = lex(input)
-	props = make(map[string]string)
+	properties = NewProperties()
 
 	for {
 		token := p.expectOneOf(itemKey, itemEOF)
@@ -31,13 +29,13 @@
 
 		token = p.expectOneOf(itemValue, itemEOF)
 		if token.typ == itemEOF {
-			props[key] = ""
+			properties.m[key] = ""
 			break
 		}
-		props[key] = token.val
+		properties.m[key] = token.val
 	}
 
-	return props, nil
+	return properties, nil
 }
 
 func (p *parser) errorf(format string, args ...interface{}) {
diff --git a/properties.go b/properties.go
index 0f965a9..87bf5cf 100644
--- a/properties.go
+++ b/properties.go
@@ -4,4 +4,166 @@
 
 package goproperties
 
-type Properties map[string]string
+import (
+	"fmt"
+	"io"
+	"log"
+	"strings"
+	"unicode/utf8"
+)
+
+type Properties struct {
+	// Pre-/Postfix for property expansion.
+	Prefix  string
+	Postfix string
+
+	m map[string]string
+}
+
+// NewProperties creates a new Properties struct with the default
+// configuration for "${key}" expressions.
+func NewProperties() *Properties {
+	return &Properties{
+		Prefix:  "${",
+		Postfix: "}",
+		m:       make(map[string]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 !ok {
+		return "", false
+	}
+
+	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.
+	if err != nil {
+		log.Printf("%s in %q", err, key+" = "+v)
+		return v, true
+	}
+
+	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) {
+	if v, ok := p.Get(key); ok {
+		return v
+	}
+	return defaultValue
+}
+
+// Len returns the number of keys.
+func (p *Properties) Len() int {
+	return len(p.m)
+}
+
+// Dump returns a string of all unexpanded 'key = value' pairs.
+func (p *Properties) Dump() string {
+	var s string
+	for key, value := range p.m {
+		s = fmt.Sprintf("%s%s = %s\n", s, key, value)
+	}
+	return s
+}
+
+// String returns a string of all expanded 'key = value' pairs.
+func (p *Properties) String() string {
+	var s string
+	for key, _ := range p.m {
+		value, _ := p.Get(key)
+		s = fmt.Sprintf("%s%s = %s\n", s, key, value)
+	}
+	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) {
+	total := 0
+	for key, value := range p.m {
+		s := fmt.Sprintf("%s = %s\n", encode(key, " :"), encode(value, ""))
+		n, err := w.Write([]byte(s))
+		if err != nil {
+			return total, err
+		}
+		total += n
+	}
+	return total, 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.
+func (p *Properties) expand(input string) (string, error) {
+	// no pre/postfix -> nothing to expand
+	if p.Prefix == "" && p.Postfix == "" {
+		return input, nil
+	}
+
+	return p.doExpand(input, make(map[string]bool))
+}
+
+func (p *Properties) doExpand(s string, keys map[string]bool) (string, error) {
+	a := strings.Index(s, p.Prefix)
+	if a == -1 {
+		return s, nil
+	}
+
+	b := strings.Index(s[a:], p.Postfix)
+	if b == -1 {
+		return "", fmt.Errorf("Malformed expression")
+	}
+
+	key := s[a+len(p.Prefix) : b-len(p.Postfix)+1]
+
+	if _, ok := keys[key]; ok {
+		return "", fmt.Errorf("Circular reference")
+	}
+
+	val, ok := p.m[key]
+	if !ok {
+		val = ""
+	}
+
+	// remember that we've seen the key
+	keys[key] = true
+
+	return p.doExpand(s[:a]+val+s[b+1:], keys)
+}
+
+// encode encodes a UTF-8 string to ISO-8859-1 and escapes some characters.
+func encode(s string, escape 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 -> encode special chars only
+			switch r {
+			case '\f':
+				v += "\\f"
+			case '\n':
+				v += "\\n"
+			case '\r':
+				v += "\\r"
+			case '\t':
+				v += "\\t"
+			default:
+				if strings.ContainsRune(escape, r) {
+					v += "\\"
+				}
+				v += string(r)
+			}
+		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
+}
diff --git a/decoder_test.go b/properties_test.go
similarity index 63%
rename from decoder_test.go
rename to properties_test.go
index 701ef2e..91b1a7e 100644
--- a/decoder_test.go
+++ b/properties_test.go
@@ -5,13 +5,14 @@
 package goproperties
 
 import (
+	"bytes"
 	"flag"
 	"fmt"
 	"os"
 	"strings"
 	"testing"
 
-	. "github.scm.corp.ebay.com/ecg-marktplaats/cas-go/third_party/launchpad.net/gocheck"
+	. "launchpad.net/gocheck"
 )
 
 func Test(t *testing.T) { TestingT(t) }
@@ -42,9 +43,27 @@
 	{"\rkey=value\r", "key", "value"},
 	{"\r\nkey=value\r\n", "key", "value"},
 
-	// escaped chars
-	{"k\\ e\\:y\\= = value", "k e:y=", "value"},                // escaped chars in key
-	{"key = v\\ a\\:lu\\=e\\n\\r\\t", "key", "v a:lu=e\n\r\t"}, // escaped chars in value
+	// escaped chars in key
+	{"k\\ ey = value", "k ey", "value"},
+	{"k\\:ey = value", "k:ey", "value"},
+	{"k\\=ey = value", "k=ey", "value"},
+	{"k\\fey = value", "k\fey", "value"},
+	{"k\\ney = value", "k\ney", "value"},
+	{"k\\rey = value", "k\rey", "value"},
+	{"k\\tey = value", "k\tey", "value"},
+
+	// escaped chars in value
+	{"key = v\\ alue", "key", "v alue"},
+	{"key = v\\:alue", "key", "v:alue"},
+	{"key = v\\=alue", "key", "v=alue"},
+	{"key = v\\falue", "key", "v\falue"},
+	{"key = v\\nalue", "key", "v\nalue"},
+	{"key = v\\ralue", "key", "v\ralue"},
+	{"key = v\\talue", "key", "v\talue"},
+
+	// silently dropped escape character
+	{"k\\zey = value", "kzey", "value"},
+	{"key = v\\zalue", "key", "vzalue"},
 
 	// unicode literals
 	{"key\\u2318 = value", "key⌘", "value"},
@@ -60,6 +79,18 @@
 
 	// comments
 	{"# this is a comment\n! and so is this\nkey1=value1\nkey#2=value#2\n\nkey!3=value!3\n# and another one\n! and the final one", "key1", "value1", "key#2", "value#2", "key!3", "value!3"},
+
+	// 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
@@ -72,6 +103,15 @@
 	{"key\\u123", "invalid unicode literal"},
 }
 
+// define write encoding test cases in the form of
+// {"input", "expected output after write"}
+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"},
+}
+
 // Benchmarks the decoder by creating a property file with 1000 key/value pairs.
 func BenchmarkDecoder(b *testing.B) {
 	input := ""
@@ -80,7 +120,7 @@
 	}
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
-		Decode([]byte(input))
+		Load([]byte(input))
 	}
 }
 
@@ -106,6 +146,21 @@
 	}
 }
 
+// Test write encoding.
+func (l *TestSuite) TestWrite(c *C) {
+	for _, test := range writeTests {
+		input, output := test[0], test[1]
+		p, err := parse(input)
+
+		buf := new(bytes.Buffer)
+		n, err := p.Write(buf)
+		c.Assert(err, IsNil)
+		s := string(buf.Bytes())
+		c.Assert(n, Equals, len(output), Commentf("input=%q expected=%q obtained=%q", input, output, s))
+		c.Assert(s, Equals, output, Commentf("input=%q expected=%q obtained=%q", input, output, s))
+	}
+}
+
 // tests all combinations of delimiters, leading and/or trailing whitespace and newlines.
 func testAllCombinations(c *C, key, value string) {
 	whitespace := []string{"", " ", "\f", "\t"}
@@ -133,28 +188,34 @@
 func testKeyValue(c *C, input string, keyvalues ...string) {
 	printf("%q\n", input)
 
-	p, err := Decode([]byte(input))
+	p, err := Load([]byte(input))
 	c.Assert(err, IsNil)
-	c.Assert(p, NotNil)
-	c.Assert(len(p), Equals, len(keyvalues)/2, Commentf("Odd number of key/value pairs."))
-
-	for i := 0; i < len(keyvalues)/2; i += 2 {
-		key, value := keyvalues[i], keyvalues[i+1]
-		v, ok := p[key]
-		c.Assert(ok, Equals, true, Commentf("No key %q for input %q", key, input))
-		c.Assert(v, Equals, value, Commentf("Value %q does not match input %q", value, input))
-	}
+	assertKeyValues(c, input, p, keyvalues...)
 }
 
 // tests whether some input produces a given error message.
 func testError(c *C, input, msg string) {
 	printf("%q\n", input)
 
-	_, err := Decode([]byte(input))
+	_, err := Load([]byte(input))
 	c.Assert(err, NotNil)
 	c.Assert(strings.Contains(err.Error(), msg), Equals, true, Commentf("Expected %q got %q", msg, err.Error()))
 }
 
+// tests whether key/value pairs exist for a given input.
+// keyvalues is expected to be an even number of strings of "key", "value", ...
+func assertKeyValues(c *C, input string, p *Properties, keyvalues ...string) {
+	c.Assert(p, NotNil)
+	c.Assert(2*p.Len(), Equals, len(keyvalues), Commentf("Odd number of key/value pairs."))
+
+	for i := 0; i < len(keyvalues); i += 2 {
+		key, value := keyvalues[i], keyvalues[i+1]
+		v, ok := p.Get(key)
+		c.Assert(ok, Equals, true, Commentf("No key %q found (input=%q)", key, input))
+		c.Assert(v, Equals, value, Commentf("Value %q does not match %q (input=%q)", v, value, input))
+	}
+}
+
 // prints to stderr if the -verbose flag was given.
 func printf(format string, args ...interface{}) {
 	if *verbose {