feat: add Go SSH config management functions and type
7 funciones Go del dominio infra para gestion programatica de ~/.ssh/config: ssh_config_parse (parser de bloques Host/Match), ssh_config_read (lectura del archivo), ssh_config_find (busqueda por host), ssh_config_add_entry y ssh_config_remove_entry (CRUD), ssh_config_render (serializacion a texto), ssh_config_write (escritura atomica). Incluye tipo SshConfigEntry (product type) y tests unitarios del parser.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
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])
|
||||
}
|
||||
Reference in New Issue
Block a user