package infra import ( "bufio" "strconv" "strings" ) // SSHConfigParse parsea el contenido de un archivo ~/.ssh/config y retorna // una lista de SSHConfigEntry. Ignora comentarios y lineas vacias. // Los bloques Host con wildcards (* o ?) se ignoran. func SSHConfigParse(content string) []SSHConfigEntry { var entries []SSHConfigEntry var current *SSHConfigEntry scanner := bufio.NewScanner(strings.NewReader(content)) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } key, value := splitDirective(line) if key == "" { continue } if strings.EqualFold(key, "Host") { if current != nil && current.Alias != "" { entries = append(entries, *current) } if strings.ContainsAny(value, "*?") { current = nil continue } current = &SSHConfigEntry{ Alias: value, Options: make(map[string]string), } continue } if current == nil { continue } switch strings.ToLower(key) { case "hostname": current.HostName = value case "user": current.User = value case "port": if p, err := strconv.Atoi(value); err == nil { current.Port = p } case "identityfile": current.IdentityFile = value default: current.Options[key] = value } } if current != nil && current.Alias != "" { entries = append(entries, *current) } return entries } // splitDirective separa una linea "Key Value" o "Key=Value" en clave y valor. func splitDirective(line string) (string, string) { // Intentar separar por = if idx := strings.IndexByte(line, '='); idx >= 0 { return strings.TrimSpace(line[:idx]), strings.TrimSpace(line[idx+1:]) } // Separar por primer espacio/tab fields := strings.SplitN(line, " ", 2) if len(fields) < 2 { fields = strings.SplitN(line, "\t", 2) } if len(fields) < 2 { return "", "" } return strings.TrimSpace(fields[0]), strings.TrimSpace(fields[1]) }