Files
ssh-netbox-wrapper/internal/hook/aliases.go
T
2026-10-27 19:19:42 +01:00

146 lines
4.5 KiB
Go

package hook
import (
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/cache"
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/shortcuts"
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/storage"
)
// AliasEntry holds a single shell alias mapping.
type AliasEntry struct {
Name string // short alias name (e.g. "web01")
Host string // canonical NetBox hostname (e.g. "web01.example.com")
}
// AliasesPath returns the path for the bash/zsh aliases file.
func AliasesPath() string {
cacheDir := os.Getenv("XDG_CACHE_HOME")
if cacheDir == "" {
var err error
cacheDir, err = os.UserCacheDir()
if err != nil {
cacheDir = filepath.Join(os.Getenv("HOME"), ".cache")
}
}
return filepath.Join(cacheDir, "netssh", "aliases.sh")
}
// FishAliasesPath returns the path for the fish conf.d aliases file.
// Fish auto-sources every file in conf.d, so no explicit source line is needed.
func FishAliasesPath() string {
return filepath.Join(FishConfigDir(), "conf.d", "netssh.fish")
}
// WriteAliasFiles writes alias definitions to disk for all supported shells.
//
// The bash/zsh file (~/.cache/netssh/aliases.sh) is always written.
// The fish file (~/.config/fish/conf.d/netssh.fish) is written only when
// ~/.config/fish/ already exists (i.e. fish is configured on this system).
func WriteAliasFiles(entries []AliasEntry) error {
if err := writeShAliasFile(entries); err != nil {
return err
}
writeFishAliasFile(entries) // best-effort — never blocks the caller on failure
alias := ""
for _, entry := range entries {
if entry.Host == "" {
alias = entry.Name
break
}
}
return SyncCommandCompletion(alias)
}
func writeShAliasFile(entries []AliasEntry) error {
p := AliasesPath()
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
return fmt.Errorf("creating aliases dir: %w", err)
}
var b strings.Builder
b.WriteString("# netssh aliases — generated automatically, do not edit\n")
b.WriteString("# Regenerated on every shell start and after 'netssh cache refresh'.\n")
for _, e := range entries {
b.WriteString(e.Definition("bash") + "\n")
}
return storage.WriteFile(p, []byte(b.String()), 0o600)
}
func writeFishAliasFile(entries []AliasEntry) {
// Only write if fish is configured on this system.
if _, err := os.Stat(FishConfigDir()); err != nil {
return
}
p := FishAliasesPath()
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
return
}
var b strings.Builder
b.WriteString("# netssh aliases — generated automatically, do not edit\n")
b.WriteString("# Regenerated on every shell start and after 'netssh cache refresh'.\n")
for _, e := range entries {
b.WriteString(e.Definition("fish") + "\n")
}
storage.WriteFile(p, []byte(b.String()), 0o600) //nolint:errcheck
}
// Empty Host represents an alias for the netssh command itself.
func (e AliasEntry) Definition(shell string) string {
command := "netssh"
if e.Host != "" {
host := e.Host
if !regexp.MustCompile(`^[a-zA-Z0-9_.:-]+$`).MatchString(host) {
host = shellQuote(host, shell)
}
command += " " + host
}
if shell == "fish" {
return fmt.Sprintf("alias %s %s", e.Name, shellQuote(command, shell))
}
return fmt.Sprintf("alias %s=%s", e.Name, shellQuote(command, shell))
}
func shellQuote(value, shell string) string {
if shell == "fish" {
value = strings.ReplaceAll(value, `\`, `\\`)
return "'" + strings.ReplaceAll(value, "'", `\'`) + "'"
}
return "'" + strings.ReplaceAll(value, "'", `'\''`) + "'"
}
// BuildAliasEntries gives the configured command alias priority over host aliases.
func BuildAliasEntries(entries []cache.Entry, cfg config.Config) []AliasEntry {
sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
seen := map[string]bool{"netssh": true}
var out []AliasEntry
if cfg.Shell.Alias != "" && config.ValidateCommandAlias(cfg.Shell.Alias) == nil {
seen[cfg.Shell.Alias] = true
out = append(out, AliasEntry{Name: cfg.Shell.Alias})
}
for _, e := range entries {
name := shortcuts.AliasName(e.Name, cfg.Shortcuts)
if seen[name] || name == "" || !regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_-]*$`).MatchString(name) {
continue
}
seen[name] = true
out = append(out, AliasEntry{Name: name, Host: e.Name})
}
return out
}
// FishConfigDir follows fish's XDG layout on both Linux and macOS.
func FishConfigDir() string {
dir := os.Getenv("XDG_CONFIG_HOME")
if dir == "" {
dir = filepath.Join(os.Getenv("HOME"), ".config")
}
return filepath.Join(dir, "fish")
}