Add property support for pointers to bools and strings

The only append semantics for bool that result in a no-op when the zero
value is appended is to OR the two values together, but that is rarely
the desired semantics.  Add support for *bool and *string as property
types, where appending a nil pointer is a no-op.  For *bool, appending a
non-nil pointer replaces the destination with the value.  For *string,
appending a non-nil pointer appends the value.

This also provides a more reliable replacement for
ModuleContext.ContainsProperty, as the  build logic can tell that the
property was set, even if it was set by a  mutator and not by the
blueprints file, by testing against nil.

[]string already provides these semantics for lists.

Setting a *bool or *string property from a blueprints file is the same
syntax as setting a bool or a string property.
diff --git a/proptools/clone.go b/proptools/clone.go
index d08a9db..7496584 100644
--- a/proptools/clone.go
+++ b/proptools/clone.go
@@ -91,27 +91,34 @@
 			}
 			fallthrough
 		case reflect.Ptr:
-			if srcFieldValue.Type().Elem().Kind() != reflect.Struct {
-				panic(fmt.Errorf("can't clone field %q: points to a non-struct",
-					field.Name))
-			}
-
 			if srcFieldValue.IsNil() {
 				dstFieldValue.Set(srcFieldValue)
 				break
 			}
 
-			if !dstFieldValue.IsNil() {
-				// Re-use the existing allocation.
-				CopyProperties(dstFieldValue.Elem(), srcFieldValue.Elem())
-				break
-			} else {
-				newValue := CloneProperties(srcFieldValue.Elem())
-				if dstFieldInterfaceValue.IsValid() {
-					dstFieldInterfaceValue.Set(newValue)
+			srcFieldValue := srcFieldValue.Elem()
+
+			switch srcFieldValue.Kind() {
+			case reflect.Struct:
+				if !dstFieldValue.IsNil() {
+					// Re-use the existing allocation.
+					CopyProperties(dstFieldValue.Elem(), srcFieldValue)
+					break
 				} else {
-					dstFieldValue.Set(newValue)
+					newValue := CloneProperties(srcFieldValue)
+					if dstFieldInterfaceValue.IsValid() {
+						dstFieldInterfaceValue.Set(newValue)
+					} else {
+						dstFieldValue.Set(newValue)
+					}
 				}
+			case reflect.Bool, reflect.String:
+				newValue := reflect.New(srcFieldValue.Type())
+				newValue.Elem().Set(srcFieldValue)
+				dstFieldValue.Set(newValue)
+			default:
+				panic(fmt.Errorf("can't clone field %q: points to a %s",
+					field.Name, srcFieldValue.Kind()))
 			}
 		default:
 			panic(fmt.Errorf("unexpected kind for property struct field %q: %s",
@@ -153,18 +160,19 @@
 			}
 			fallthrough
 		case reflect.Ptr:
-			// We leave the pointer intact and zero out the struct that's
-			// pointed to.
-			if fieldValue.Type().Elem().Kind() != reflect.Struct {
-				panic(fmt.Errorf("can't zero field %q: points to a non-struct",
-					field.Name))
+			switch fieldValue.Type().Elem().Kind() {
+			case reflect.Struct:
+				if fieldValue.IsNil() {
+					break
+				}
+				ZeroProperties(fieldValue.Elem())
+			case reflect.Bool, reflect.String:
+				fieldValue.Set(reflect.Zero(fieldValue.Type()))
+			default:
+				panic(fmt.Errorf("can't zero field %q: points to a %s",
+					field.Name, fieldValue.Elem().Kind()))
 			}
 
-			if fieldValue.IsNil() {
-				break
-			}
-
-			ZeroProperties(fieldValue.Elem())
 		default:
 			panic(fmt.Errorf("unexpected kind for property struct field %q: %s",
 				field.Name, fieldValue.Kind()))
@@ -217,21 +225,24 @@
 			dstFieldValue = newValue
 			fallthrough
 		case reflect.Ptr:
-			if srcFieldValue.Type().Elem().Kind() != reflect.Struct {
-				panic(fmt.Errorf("can't clone field %q: points to a non-struct",
-					field.Name))
+			switch srcFieldValue.Type().Elem().Kind() {
+			case reflect.Struct:
+				if srcFieldValue.IsNil() {
+					break
+				}
+				newValue := CloneEmptyProperties(srcFieldValue.Elem())
+				if dstFieldInterfaceValue.IsValid() {
+					dstFieldInterfaceValue.Set(newValue)
+				} else {
+					dstFieldValue.Set(newValue)
+				}
+			case reflect.Bool, reflect.String:
+				// Nothing
+			default:
+				panic(fmt.Errorf("can't clone empty field %q: points to a %s",
+					field.Name, srcFieldValue.Elem().Kind()))
 			}
 
-			if srcFieldValue.IsNil() {
-				break
-			}
-
-			newValue := CloneEmptyProperties(srcFieldValue.Elem())
-			if dstFieldInterfaceValue.IsValid() {
-				dstFieldInterfaceValue.Set(newValue)
-			} else {
-				dstFieldValue.Set(newValue)
-			}
 		default:
 			panic(fmt.Errorf("unexpected kind for property struct field %q: %s",
 				field.Name, srcFieldValue.Kind()))
diff --git a/proptools/clone_test.go b/proptools/clone_test.go
index b082543..f7460cf 100644
--- a/proptools/clone_test.go
+++ b/proptools/clone_test.go
@@ -71,6 +71,25 @@
 		out: &struct{ S []string }{},
 	},
 	{
+		// Clone pointer to bool
+		in: &struct{ B1, B2 *bool }{
+			B1: BoolPtr(true),
+			B2: BoolPtr(false),
+		},
+		out: &struct{ B1, B2 *bool }{
+			B1: BoolPtr(true),
+			B2: BoolPtr(false),
+		},
+	},
+	{
+		// Clone pointer to string
+		in: &struct{ S *string }{
+			S: StringPtr("string1"),
+		},
+		out: &struct{ S *string }{
+			S: StringPtr("string1"),
+		},
+	},
 	{
 		// Clone struct
 		in: &struct{ S struct{ S string } }{
@@ -201,6 +220,20 @@
 		out: &struct{ S []string }{},
 	},
 	{
+		// Clone pointer to bool
+		in: &struct{ B1, B2 *bool }{
+			B1: BoolPtr(true),
+			B2: BoolPtr(false),
+		},
+		out: &struct{ B1, B2 *bool }{},
+	},
+	{
+		// Clone pointer to string
+		in: &struct{ S *string }{
+			S: StringPtr("string1"),
+		},
+		out: &struct{ S *string }{},
+	},
 	{
 		// Clone struct
 		in: &struct{ S struct{ S string } }{
diff --git a/proptools/extend.go b/proptools/extend.go
index 241643b..d356419 100644
--- a/proptools/extend.go
+++ b/proptools/extend.go
@@ -29,8 +29,9 @@
 // An error returned by AppendProperties that applies to a specific property will be an
 // *ExtendPropertyError, and can have the property name and error extracted from it.
 //
-// The append operation is defined as appending string and slices of strings normally, OR-ing
-// bool values, and recursing into embedded structs, pointers to structs, and interfaces containing
+// The append operation is defined as appending strings, pointers to strings, and slices of
+// strings normally, OR-ing bool values, replacing non-nil pointers to booleans, and recursing into
+// embedded structs, pointers to structs, and interfaces containing
 // pointers to structs.  Appending the zero value of a property will always be a no-op.
 func AppendProperties(dst interface{}, src interface{}, filter ExtendPropertyFilterFunc) error {
 	return extendProperties(dst, src, filter, false)
@@ -46,8 +47,9 @@
 // An error returned by PrependProperties that applies to a specific property will be an
 // *ExtendPropertyError, and can have the property name and error extracted from it.
 //
-// The prepend operation is defined as prepending string and slices of strings normally, OR-ing
-// bool values, and recursing into embedded structs, pointers to structs, and interfaces containing
+// The prepend operation is defined as prepending strings, pointers to strings, and slices of
+// strings normally, OR-ing bool values, replacing non-nil pointers to booleans, and recursing into
+// embedded structs, pointers to structs, and interfaces containing
 // pointers to structs.  Prepending the zero value of a property will always be a no-op.
 func PrependProperties(dst interface{}, src interface{}, filter ExtendPropertyFilterFunc) error {
 	return extendProperties(dst, src, filter, true)
@@ -65,8 +67,9 @@
 // An error returned by AppendMatchingProperties that applies to a specific property will be an
 // *ExtendPropertyError, and can have the property name and error extracted from it.
 //
-// The append operation is defined as appending string and slices of strings normally, OR-ing
-// bool values, and recursing into embedded structs, pointers to structs, and interfaces containing
+// The append operation is defined as appending strings, pointers to strings, and slices of
+// strings normally, OR-ing bool values, replacing non-nil pointers to booleans, and recursing into
+// embedded structs, pointers to structs, and interfaces containing
 // pointers to structs.  Appending the zero value of a property will always be a no-op.
 func AppendMatchingProperties(dst []interface{}, src interface{},
 	filter ExtendPropertyFilterFunc) error {
@@ -85,8 +88,9 @@
 // An error returned by PrependProperties that applies to a specific property will be an
 // *ExtendPropertyError, and can have the property name and error extracted from it.
 //
-// The prepend operation is defined as prepending string and slices of strings normally, OR-ing
-// bool values, and recursing into embedded structs, pointers to structs, and interfaces containing
+// The prepend operation is defined as prepending strings, pointers to strings, and slices of
+// strings normally, OR-ing bool values, replacing non-nil pointers to booleans, and recursing into
+// embedded structs, pointers to structs, and interfaces containing
 // pointers to structs.  Prepending the zero value of a property will always be a no-op.
 func PrependMatchingProperties(dst []interface{}, src interface{},
 	filter ExtendPropertyFilterFunc) error {
@@ -213,6 +217,18 @@
 
 				fallthrough
 			case reflect.Ptr:
+				ptrKind := srcFieldValue.Type().Elem().Kind()
+				if ptrKind == reflect.Bool || ptrKind == reflect.String {
+					if srcFieldValue.Type() != dstFieldValue.Type() {
+						return extendPropertyErrorf(propertyName, "mismatched pointer types %s and %s",
+							dstFieldValue.Type(), srcFieldValue.Type())
+					}
+					break
+				} else if ptrKind != reflect.Struct {
+					return extendPropertyErrorf(propertyName, "pointer is a %s", ptrKind)
+				}
+
+				// Pointer to a struct
 				if dstFieldValue.IsNil() != srcFieldValue.IsNil() {
 					return extendPropertyErrorf(propertyName, "nilitude mismatch")
 				}
@@ -223,10 +239,6 @@
 				dstFieldValue = dstFieldValue.Elem()
 				srcFieldValue = srcFieldValue.Elem()
 
-				if srcFieldValue.Kind() != reflect.Struct || dstFieldValue.Kind() != reflect.Struct {
-					return extendPropertyErrorf(propertyName, "pointer not to a struct")
-				}
-
 				fallthrough
 			case reflect.Struct:
 				if sameTypes && dstFieldValue.Type() != srcFieldValue.Type() {
@@ -293,6 +305,34 @@
 					newSlice = reflect.AppendSlice(newSlice, srcFieldValue)
 				}
 				dstFieldValue.Set(newSlice)
+			case reflect.Ptr:
+				if srcFieldValue.IsNil() {
+					break
+				}
+
+				switch ptrKind := srcFieldValue.Type().Elem().Kind(); ptrKind {
+				case reflect.Bool:
+					if prepend {
+						if dstFieldValue.IsNil() {
+							dstFieldValue.Set(reflect.ValueOf(BoolPtr(srcFieldValue.Elem().Bool())))
+						}
+					} else {
+						// For append, replace the original value.
+						dstFieldValue.Set(reflect.ValueOf(BoolPtr(srcFieldValue.Elem().Bool())))
+					}
+				case reflect.String:
+					dstStr := ""
+					if !dstFieldValue.IsNil() {
+						dstStr = dstFieldValue.Elem().String()
+					}
+					if prepend {
+						dstFieldValue.Set(reflect.ValueOf(StringPtr(srcFieldValue.Elem().String() + dstStr)))
+					} else {
+						dstFieldValue.Set(reflect.ValueOf(StringPtr(dstStr + srcFieldValue.Elem().String())))
+					}
+				default:
+					panic(fmt.Errorf("unexpected pointer kind %s", ptrKind))
+				}
 			}
 		}
 		if !found {
diff --git a/proptools/extend_test.go b/proptools/extend_test.go
index a2482cc..35a1816 100644
--- a/proptools/extend_test.go
+++ b/proptools/extend_test.go
@@ -101,6 +101,114 @@
 		prepend: true,
 	},
 	{
+		// Append pointer to bool
+		in1: &struct{ B1, B2, B3, B4, B5, B6, B7, B8, B9 *bool }{
+			B1: BoolPtr(true),
+			B2: BoolPtr(false),
+			B3: nil,
+			B4: BoolPtr(true),
+			B5: BoolPtr(false),
+			B6: nil,
+			B7: BoolPtr(true),
+			B8: BoolPtr(false),
+			B9: nil,
+		},
+		in2: &struct{ B1, B2, B3, B4, B5, B6, B7, B8, B9 *bool }{
+			B1: nil,
+			B2: nil,
+			B3: nil,
+			B4: BoolPtr(true),
+			B5: BoolPtr(true),
+			B6: BoolPtr(true),
+			B7: BoolPtr(false),
+			B8: BoolPtr(false),
+			B9: BoolPtr(false),
+		},
+		out: &struct{ B1, B2, B3, B4, B5, B6, B7, B8, B9 *bool }{
+			B1: BoolPtr(true),
+			B2: BoolPtr(false),
+			B3: nil,
+			B4: BoolPtr(true),
+			B5: BoolPtr(true),
+			B6: BoolPtr(true),
+			B7: BoolPtr(false),
+			B8: BoolPtr(false),
+			B9: BoolPtr(false),
+		},
+	},
+	{
+		// Prepend pointer to bool
+		in1: &struct{ B1, B2, B3, B4, B5, B6, B7, B8, B9 *bool }{
+			B1: BoolPtr(true),
+			B2: BoolPtr(false),
+			B3: nil,
+			B4: BoolPtr(true),
+			B5: BoolPtr(false),
+			B6: nil,
+			B7: BoolPtr(true),
+			B8: BoolPtr(false),
+			B9: nil,
+		},
+		in2: &struct{ B1, B2, B3, B4, B5, B6, B7, B8, B9 *bool }{
+			B1: nil,
+			B2: nil,
+			B3: nil,
+			B4: BoolPtr(true),
+			B5: BoolPtr(true),
+			B6: BoolPtr(true),
+			B7: BoolPtr(false),
+			B8: BoolPtr(false),
+			B9: BoolPtr(false),
+		},
+		out: &struct{ B1, B2, B3, B4, B5, B6, B7, B8, B9 *bool }{
+			B1: BoolPtr(true),
+			B2: BoolPtr(false),
+			B3: nil,
+			B4: BoolPtr(true),
+			B5: BoolPtr(false),
+			B6: BoolPtr(true),
+			B7: BoolPtr(true),
+			B8: BoolPtr(false),
+			B9: BoolPtr(false),
+		},
+		prepend: true,
+	},
+	{
+		// Append pointer to strings
+		in1: &struct{ S1, S2, S3, S4 *string }{
+			S1: StringPtr("string1"),
+			S2: StringPtr("string2"),
+		},
+		in2: &struct{ S1, S2, S3, S4 *string }{
+			S1: StringPtr("string3"),
+			S3: StringPtr("string4"),
+		},
+		out: &struct{ S1, S2, S3, S4 *string }{
+			S1: StringPtr("string1string3"),
+			S2: StringPtr("string2"),
+			S3: StringPtr("string4"),
+			S4: nil,
+		},
+	},
+	{
+		// Prepend pointer to strings
+		in1: &struct{ S1, S2, S3, S4 *string }{
+			S1: StringPtr("string1"),
+			S2: StringPtr("string2"),
+		},
+		in2: &struct{ S1, S2, S3, S4 *string }{
+			S1: StringPtr("string3"),
+			S3: StringPtr("string4"),
+		},
+		out: &struct{ S1, S2, S3, S4 *string }{
+			S1: StringPtr("string3string1"),
+			S2: StringPtr("string2"),
+			S3: StringPtr("string4"),
+			S4: nil,
+		},
+		prepend: true,
+	},
+	{
 		// Append slice
 		in1: &struct{ S []string }{
 			S: []string{"string1"},
@@ -439,7 +547,7 @@
 		out: &struct{ S *[]string }{
 			S: &[]string{"string1"},
 		},
-		err: extendPropertyErrorf("s", "pointer not to a struct"),
+		err: extendPropertyErrorf("s", "pointer is a slice"),
 	},
 	{
 		// Error in nested struct
diff --git a/proptools/proptools.go b/proptools/proptools.go
index 79c4f6d..690d384 100644
--- a/proptools/proptools.go
+++ b/proptools/proptools.go
@@ -49,3 +49,31 @@
 
 	return false
 }
+
+// BoolPtr returns a pointer to a new bool containing the given value.
+func BoolPtr(b bool) *bool {
+	return &b
+}
+
+// StringPtr returns a pointer to a new string containing the given value.
+func StringPtr(s string) *string {
+	return &s
+}
+
+// Bool takes a pointer to a bool and returns true iff the pointer is non-nil and points to a true
+// value.
+func Bool(b *bool) bool {
+	if b != nil {
+		return *b
+	}
+	return false
+}
+
+// String takes a pointer to a string and returns the value of the string if the pointer is non-nil,
+// or an empty string.
+func String(s *string) string {
+	if s != nil {
+		return *s
+	}
+	return ""
+}
diff --git a/unpack.go b/unpack.go
index 445a8c6..64c65e4 100644
--- a/unpack.go
+++ b/unpack.go
@@ -161,15 +161,18 @@
 			}
 			fallthrough
 		case reflect.Ptr:
-			if fieldValue.IsNil() {
-				panic(fmt.Errorf("field %s contains a nil pointer",
-					field.Name))
-			}
-			fieldValue = fieldValue.Elem()
-			elemType := fieldValue.Type()
-			if elemType.Kind() != reflect.Struct {
-				panic(fmt.Errorf("field %s contains a non-struct pointer",
-					field.Name))
+			switch ptrKind := fieldValue.Type().Elem().Kind(); ptrKind {
+			case reflect.Struct:
+				if fieldValue.IsNil() {
+					panic(fmt.Errorf("field %s contains a nil pointer",
+						field.Name))
+				}
+				fieldValue = fieldValue.Elem()
+			case reflect.Bool, reflect.String:
+				// Nothing
+			default:
+				panic(fmt.Errorf("field %s contains a pointer to %s",
+					field.Name, ptrKind))
 			}
 
 		case reflect.Int, reflect.Uint:
@@ -225,9 +228,19 @@
 			newErrs = unpackString(fieldValue, packedProperty.property)
 		case reflect.Slice:
 			newErrs = unpackSlice(fieldValue, packedProperty.property)
-		case reflect.Ptr, reflect.Interface:
-			fieldValue = fieldValue.Elem()
-			fallthrough
+		case reflect.Ptr:
+			switch ptrKind := fieldValue.Type().Elem().Kind(); ptrKind {
+			case reflect.Bool:
+				newValue := reflect.New(fieldValue.Type().Elem())
+				newErrs = unpackBool(newValue.Elem(), packedProperty.property)
+				fieldValue.Set(newValue)
+			case reflect.String:
+				newValue := reflect.New(fieldValue.Type().Elem())
+				newErrs = unpackString(newValue.Elem(), packedProperty.property)
+				fieldValue.Set(newValue)
+			default:
+				panic(fmt.Errorf("unexpected pointer kind %s", ptrKind))
+			}
 		case reflect.Struct:
 			localFilterKey, localFilterValue := filterKey, filterValue
 			if k, v, err := HasFilter(field.Tag); err != nil {
@@ -248,6 +261,8 @@
 			}
 			newErrs = unpackStruct(propertyName+".", fieldValue,
 				packedProperty.property, propertyMap, localFilterKey, localFilterValue)
+		default:
+			panic(fmt.Errorf("unexpected kind %s", kind))
 		}
 		errs = append(errs, newErrs...)
 		if len(errs) >= maxErrors {
diff --git a/unpack_test.go b/unpack_test.go
index 0b31efc..77a57ec 100644
--- a/unpack_test.go
+++ b/unpack_test.go
@@ -33,6 +33,24 @@
 	{`
 		m {
 			name: "abc",
+			blank: "",
+		}
+		`,
+		struct {
+			Name  *string
+			Blank *string
+			Unset *string
+		}{
+			Name:  proptools.StringPtr("abc"),
+			Blank: proptools.StringPtr(""),
+			Unset: nil,
+		},
+		nil,
+	},
+
+	{`
+		m {
+			name: "abc",
 		}
 		`,
 		struct {
@@ -58,6 +76,24 @@
 
 	{`
 		m {
+			isGood: true,
+			isBad: false,
+		}
+		`,
+		struct {
+			IsGood *bool
+			IsBad  *bool
+			IsUgly *bool
+		}{
+			IsGood: proptools.BoolPtr(true),
+			IsBad:  proptools.BoolPtr(false),
+			IsUgly: nil,
+		},
+		nil,
+	},
+
+	{`
+		m {
 			stuff: ["asdf", "jkl;", "qwert",
 				"uiop", "bnm,"],
 			empty: []