146 lines
4.5 KiB
Go
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")
|
|
}
|