feat(shortcuts): regex rewrite rules for host shortcuts
shortcuts.rewrites applies regex replacements after the domain suffix is stripped, e.g. ^([a-z]+)[0-9]+- -> $1 turns fsn1-bx1.hoster.de into fsnbx1. The shortcut works for connecting (cached hosts), shell aliases, and Tab completion, where it is offered when the full name does not match the typed text. Rules are editable in netssh configure, and netssh alias --test HOSTNAME shows a name's shortcut and the cached hosts it matches. The shortcut lookup only ran when domains or strip_hyphens were set; it now also runs for rewrite-only configurations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d51c040d9f
commit
878701aba7
12 files changed
+408
-21
No files matched your search
@@ -57,8 +57,34 @@ type CompletionConfig struct {
|
||||
}
|
||||
|
||||
type ShortcutsConfig struct {
|
||||
Domains []string `mapstructure:"domains"`
|
||||
StripHyphens bool `mapstructure:"strip_hyphens"`
|
||||
Domains []string `mapstructure:"domains"`
|
||||
StripHyphens bool `mapstructure:"strip_hyphens"`
|
||||
Rewrites []ShortcutRewrite `mapstructure:"rewrites"`
|
||||
}
|
||||
|
||||
// ShortcutRewrite replaces every match of Pattern (Go regexp syntax) with
|
||||
// Replace, which may reference groups as $1. Applied after domain stripping.
|
||||
type ShortcutRewrite struct {
|
||||
Pattern string `mapstructure:"pattern"`
|
||||
Replace string `mapstructure:"replace"`
|
||||
}
|
||||
|
||||
// Enabled reports whether any shortcut normalization is configured.
|
||||
func (s ShortcutsConfig) Enabled() bool {
|
||||
return len(s.Domains) > 0 || s.StripHyphens || len(s.Rewrites) > 0
|
||||
}
|
||||
|
||||
// Validate rejects rewrite rules that cannot be compiled.
|
||||
func (s ShortcutsConfig) Validate() error {
|
||||
for i, rewrite := range s.Rewrites {
|
||||
if rewrite.Pattern == "" {
|
||||
return fmt.Errorf("shortcuts.rewrites[%d]: pattern is required", i)
|
||||
}
|
||||
if _, err := regexp.Compile(rewrite.Pattern); err != nil {
|
||||
return fmt.Errorf("shortcuts.rewrites[%d]: invalid pattern %q: %v", i, rewrite.Pattern, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Path returns the canonical config file path.
|
||||
@@ -128,6 +154,9 @@ func Load() (*Config, error) {
|
||||
if err := cfg.ValidateSSH(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cfg.Shortcuts.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range cfg.NetBoxes {
|
||||
source := &cfg.NetBoxes[i]
|
||||
source.Name = strings.ToLower(source.Name)
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -177,3 +178,21 @@ func TestCommandAliasEnvironmentAndValidation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_ShortcutRewrites(t *testing.T) {
|
||||
dir := writeConfig(t, "shortcuts:\n rewrites:\n - pattern: '^([a-z]+)[0-9]+-'\n replace: '$1'\n")
|
||||
cfg := loadFromDir(t, dir)
|
||||
want := []ShortcutRewrite{{Pattern: `^([a-z]+)[0-9]+-`, Replace: "$1"}}
|
||||
if len(cfg.Shortcuts.Rewrites) != 1 || cfg.Shortcuts.Rewrites[0] != want[0] || !cfg.Shortcuts.Enabled() {
|
||||
t.Fatalf("rewrites: %+v", cfg.Shortcuts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_InvalidShortcutRewrite(t *testing.T) {
|
||||
for _, rule := range []string{"pattern: '[a-'", "replace: x"} {
|
||||
t.Setenv("XDG_CONFIG_HOME", writeConfig(t, "shortcuts:\n rewrites:\n - "+rule+"\n"))
|
||||
if _, err := Load(); err == nil || !strings.Contains(err.Error(), "shortcuts.rewrites[0]") {
|
||||
t.Errorf("%s: got %v", rule, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user