Support retrieving multiple values for a given keyword

IdentityFile among others supports being provided multiple times and
aggregated across, potentially, multiple files. Support that workflow
by adding GetAll and GetAllStrict alongside the current functions.
diff --git a/README.md b/README.md
index 52cc1ea..d1adfed 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,14 @@
 port := ssh_config.Get("myhost", "Port")
 ```
 
+Certain directives can occur multiple times for a host (such as `IdentityFile`),
+so you should use the `GetAll` or `GetAllStrict` directive to retrieve those
+instead.
+
+```go
+files := ssh_config.GetAll("myhost", "IdentityFile")
+```
+
 You can also load a config file and read values from it.
 
 ```go
diff --git a/config.go b/config.go
index 136f0c3..7c2c8b6 100644
--- a/config.go
+++ b/config.go
@@ -102,6 +102,13 @@
 	return val, nil
 }
 
+func findAll(c *Config, alias, key string) ([]string, error) {
+	if c == nil {
+		return nil, nil
+	}
+	return c.GetAll(alias, key)
+}
+
 // Get finds the first value for key within a declaration that matches the
 // alias. Get returns the empty string if no value was found, or if IgnoreErrors
 // is false and we could not parse the configuration file. Use GetStrict to
@@ -114,19 +121,51 @@
 	return DefaultUserSettings.Get(alias, key)
 }
 
+// GetAll retrieves zero or more directives for key for the given alias. GetAll
+// returns nil if no value was found, or if IgnoreErrors is false and we could
+// not parse the configuration file. Use GetAllStrict to disambiguate the
+// latter cases.
+//
+// In most cases you want to use Get or GetStrict, which returns a single value.
+// However, a subset of ssh configuration values (IdentityFile, for example)
+// allow you to specify multiple directives.
+//
+// The match for key is case insensitive.
+//
+// GetAll is a wrapper around DefaultUserSettings.GetAll.
+func GetAll(alias, key string) []string {
+	return DefaultUserSettings.GetAll(alias, key)
+}
+
 // GetStrict finds the first value for key within a declaration that matches the
 // alias. If key has a default value and no matching configuration is found, the
 // default will be returned. For more information on default values and the way
 // patterns are matched, see the manpage for ssh_config.
 //
-// error will be non-nil if and only if a user's configuration file or the
-// system configuration file could not be parsed, and u.IgnoreErrors is false.
+// The returned error will be non-nil if and only if a user's configuration file
+// or the system configuration file could not be parsed, and u.IgnoreErrors is
+// false.
 //
 // GetStrict is a wrapper around DefaultUserSettings.GetStrict.
 func GetStrict(alias, key string) (string, error) {
 	return DefaultUserSettings.GetStrict(alias, key)
 }
 
+// GetAllStrict retrieves zero or more directives for key for the given alias.
+//
+// In most cases you want to use Get or GetStrict, which returns a single value.
+// However, a subset of ssh configuration values (IdentityFile, for example)
+// allow you to specify multiple directives.
+//
+// The returned error will be non-nil if and only if a user's configuration file
+// or the system configuration file could not be parsed, and u.IgnoreErrors is
+// false.
+//
+// GetAllStrict is a wrapper around DefaultUserSettings.GetAllStrict.
+func GetAllStrict(alias, key string) ([]string, error) {
+	return DefaultUserSettings.GetAllStrict(alias, key)
+}
+
 // Get finds the first value for key within a declaration that matches the
 // alias. Get returns the empty string if no value was found, or if IgnoreErrors
 // is false and we could not parse the configuration file. Use GetStrict to
@@ -141,6 +180,17 @@
 	return val
 }
 
+// GetAll retrieves zero or more directives for key for the given alias. GetAll
+// returns nil if no value was found, or if IgnoreErrors is false and we could
+// not parse the configuration file. Use GetStrict to disambiguate the latter
+// cases.
+//
+// The match for key is case insensitive.
+func (u *UserSettings) GetAll(alias, key string) []string {
+	val, _ := u.GetAllStrict(alias, key)
+	return val
+}
+
 // GetStrict finds the first value for key within a declaration that matches the
 // alias. If key has a default value and no matching configuration is found, the
 // default will be returned. For more information on default values and the way
@@ -149,6 +199,52 @@
 // error will be non-nil if and only if a user's configuration file or the
 // system configuration file could not be parsed, and u.IgnoreErrors is false.
 func (u *UserSettings) GetStrict(alias, key string) (string, error) {
+	u.doLoadConfigs()
+	//lint:ignore S1002 I prefer it this way
+	if u.onceErr != nil && u.IgnoreErrors == false {
+		return "", u.onceErr
+	}
+	val, err := findVal(u.userConfig, alias, key)
+	if err != nil || val != "" {
+		return val, err
+	}
+	val2, err2 := findVal(u.systemConfig, alias, key)
+	if err2 != nil || val2 != "" {
+		return val2, err2
+	}
+	return Default(key), nil
+}
+
+// GetAllStrict retrieves zero or more directives for key for the given alias.
+// If key has a default value and no matching configuration is found, the
+// default will be returned. For more information on default values and the way
+// patterns are matched, see the manpage for ssh_config.
+//
+// The returned error will be non-nil if and only if a user's configuration file
+// or the system configuration file could not be parsed, and u.IgnoreErrors is
+// false.
+func (u *UserSettings) GetAllStrict(alias, key string) ([]string, error) {
+	u.doLoadConfigs()
+	//lint:ignore S1002 I prefer it this way
+	if u.onceErr != nil && u.IgnoreErrors == false {
+		return nil, u.onceErr
+	}
+	val, err := findAll(u.userConfig, alias, key)
+	if err != nil || val != nil {
+		return val, err
+	}
+	val2, err2 := findAll(u.systemConfig, alias, key)
+	if err2 != nil || val2 != nil {
+		return val2, err2
+	}
+	// TODO: IdentityFile has multiple default values that we should return.
+	if def := Default(key); def != "" {
+		return []string{def}, nil
+	}
+	return []string{}, nil
+}
+
+func (u *UserSettings) doLoadConfigs() {
 	u.loadConfigs.Do(func() {
 		// can't parse user file, that's ok.
 		var filename string
@@ -176,19 +272,6 @@
 			return
 		}
 	})
-	//lint:ignore S1002 I prefer it this way
-	if u.onceErr != nil && u.IgnoreErrors == false {
-		return "", u.onceErr
-	}
-	val, err := findVal(u.userConfig, alias, key)
-	if err != nil || val != "" {
-		return val, err
-	}
-	val2, err2 := findVal(u.systemConfig, alias, key)
-	if err2 != nil || val2 != "" {
-		return val2, err2
-	}
-	return Default(key), nil
 }
 
 func parseFile(filename string) (*Config, error) {
@@ -282,6 +365,42 @@
 	return "", nil
 }
 
+// GetAll returns all values in the configuration that match the alias and
+// contains key, or nil if none are present.
+func (c *Config) GetAll(alias, key string) ([]string, error) {
+	lowerKey := strings.ToLower(key)
+	all := []string(nil)
+	for _, host := range c.Hosts {
+		if !host.Matches(alias) {
+			continue
+		}
+		for _, node := range host.Nodes {
+			switch t := node.(type) {
+			case *Empty:
+				continue
+			case *KV:
+				// "keys are case insensitive" per the spec
+				lkey := strings.ToLower(t.Key)
+				if lkey == "match" {
+					panic("can't handle Match directives")
+				}
+				if lkey == lowerKey {
+					all = append(all, t.Value)
+				}
+			case *Include:
+				val, _ := t.GetAll(alias, key)
+				if len(val) > 0 {
+					all = append(all, val...)
+				}
+			default:
+				return nil, fmt.Errorf("unknown Node type %v", t)
+			}
+		}
+	}
+
+	return all, nil
+}
+
 // String returns a string representation of the Config file.
 func (c Config) String() string {
 	return marshal(c).String()
@@ -611,6 +730,31 @@
 	return ""
 }
 
+// GetAll finds all values in the Include statement matching the alias and the
+// given key.
+func (inc *Include) GetAll(alias, key string) ([]string, error) {
+	inc.mu.Lock()
+	defer inc.mu.Unlock()
+	var vals []string
+
+	// TODO: we search files in any order which is not correct
+	for i := range inc.matches {
+		cfg := inc.files[inc.matches[i]]
+		if cfg == nil {
+			panic("nil cfg")
+		}
+		val, err := cfg.GetAll(alias, key)
+		if err == nil && len(val) != 0 {
+			// In theory if SupportsMultiple was false for this key we could
+			// stop looking here. But the caller has asked us to find all
+			// instances of the keyword (and could use Get() if they wanted) so
+			// let's keep looking.
+			vals = append(vals, val...)
+		}
+	}
+	return vals, nil
+}
+
 // String prints out a string representation of this Include directive. Note
 // included Config files are not printed as part of this representation.
 func (inc *Include) String() string {
diff --git a/config_test.go b/config_test.go
index f11fe38..f086600 100644
--- a/config_test.go
+++ b/config_test.go
@@ -67,6 +67,65 @@
 	}
 }
 
+func TestGetAllWithDefault(t *testing.T) {
+	us := &UserSettings{
+		userConfigFinder: testConfigFinder("testdata/config1"),
+	}
+
+	val, err := us.GetAllStrict("wap", "PasswordAuthentication")
+	if err != nil {
+		t.Fatalf("expected nil err, got %v", err)
+	}
+	if len(val) != 1 || val[0] != "yes" {
+		t.Errorf("expected to get PasswordAuthentication yes, got %q", val)
+	}
+}
+
+func TestGetIdentities(t *testing.T) {
+	us := &UserSettings{
+		userConfigFinder: testConfigFinder("testdata/identities"),
+	}
+
+	val, err := us.GetAllStrict("hasidentity", "IdentityFile")
+	if err != nil {
+		t.Errorf("expected nil err, got %v", err)
+	}
+	if len(val) != 1 || val[0] != "file1" {
+		t.Errorf(`expected ["file1"], got %v`, val)
+	}
+
+	val, err = us.GetAllStrict("has2identity", "IdentityFile")
+	if err != nil {
+		t.Errorf("expected nil err, got %v", err)
+	}
+	if len(val) != 2 || val[0] != "f1" || val[1] != "f2" {
+		t.Errorf(`expected [\"f1\", \"f2\"], got %v`, val)
+	}
+
+	val, err = us.GetAllStrict("randomhost", "IdentityFile")
+	if err != nil {
+		t.Errorf("expected nil err, got %v", err)
+	}
+	if len(val) != len(defaultProtocol2Identities) {
+		// TODO: return the right values here.
+		log.Printf("expected defaults, got %v", val)
+	} else {
+		for i, v := range defaultProtocol2Identities {
+			if val[i] != v {
+				t.Errorf("invalid %d in val, expected %s got %s", i, v, val[i])
+			}
+		}
+	}
+
+	val, err = us.GetAllStrict("protocol1", "IdentityFile")
+	if err != nil {
+		t.Errorf("expected nil err, got %v", err)
+	}
+	if len(val) != 1 || val[0] != "~/.ssh/identity" {
+		t.Errorf("expected [\"~/.ssh/identity\"], got %v", val)
+	}
+}
+
 func TestGetInvalidPort(t *testing.T) {
 	us := &UserSettings{
 		userConfigFinder: testConfigFinder("testdata/invalid-port"),
@@ -98,6 +157,20 @@
 	}
 }
 
+func TestGetAllNotFoundNoDefault(t *testing.T) {
+	us := &UserSettings{
+		userConfigFinder: testConfigFinder("testdata/config1"),
+	}
+
+	val, err := us.GetAllStrict("wap", "CanonicalDomains")
+	if err != nil {
+		t.Fatalf("expected nil err, got %v", err)
+	}
+	if len(val) != 0 {
+		t.Errorf("expected to get CanonicalDomains '', got %q", val)
+	}
+}
+
 func TestGetWildcard(t *testing.T) {
 	us := &UserSettings{
 		userConfigFinder: testConfigFinder("testdata/config3"),
diff --git a/testdata/identities b/testdata/identities
new file mode 100644
index 0000000..14971f7
--- /dev/null
+++ b/testdata/identities
@@ -0,0 +1,11 @@
+
+Host hasidentity
+  IdentityFile file1
+
+Host has2identity
+  IdentityFile f1
+  IdentityFile f2
+
+Host protocol1
+  Protocol 1
+
diff --git a/validators.go b/validators.go
index 29fab6a..5977f90 100644
--- a/validators.go
+++ b/validators.go
@@ -160,3 +160,27 @@
 	strings.ToLower("VisualHostKey"):      "no",
 	strings.ToLower("XAuthLocation"):      "/usr/X11R6/bin/xauth",
 }
+
+// these identities are used for SSH protocol 2
+var defaultProtocol2Identities = []string{
+	"~/.ssh/id_dsa",
+	"~/.ssh/id_ecdsa",
+	"~/.ssh/id_ed25519",
+	"~/.ssh/id_rsa",
+}
+
+// these directives support multiple items that can be collected
+// across multiple files
+var pluralDirectives = map[string]bool{
+	"CertificateFile": true,
+	"IdentityFile":    true,
+	"DynamicForward":  true,
+	"RemoteForward":   true,
+	"SendEnv":         true,
+	"SetEnv":          true,
+}
+
+// SupportsMultiple reports whether a directive can be specified multiple times.
+func SupportsMultiple(key string) bool {
+	return pluralDirectives[strings.ToLower(key)]
+}