config: Implement percent TOKEN substitution
Some checks failed
Test / lint (push) Has been cancelled
Test / test (1.17.x) (push) Has been cancelled
Test / test (1.18.x) (push) Has been cancelled
Test / test (1.19.x) (push) Has been cancelled
Test / test (1.20.x) (push) Has been cancelled
Test / test (1.21.x) (push) Has been cancelled
Test / test (1.22.x) (push) Has been cancelled
Test / test (1.23.x) (push) Has been cancelled
Test / test (1.24.x) (push) Has been cancelled

This commit is contained in:
T. R. Bernstein
2025-08-04 20:51:42 +02:00
parent a07d8f5c66
commit 511052e2fd
2 changed files with 132 additions and 1 deletions

View File

@@ -370,6 +370,102 @@ type Config struct {
position Position
}
// %% A literal `%'.
// %C Shorthand for %l%h%p%r.
// %d Local user's home directory.
// %h The remote hostname.
// %i The local user ID.
// %L The local hostname.
// %l The local hostname, including the domain name.
// %n The original remote hostname, as given on the command line.
// %p The remote port.
// %r The remote username.
// %u The local username.
func (host *Host) percent(alias, val string) string {
var (
b bytes.Buffer
sawPercent bool
)
for _, c := range val {
if sawPercent {
sawPercent = false
switch c {
case 'd':
b.WriteString(homedir())
case 'h':
b.WriteString(host.tryKV(alias, "HostName"))
case 'i':
b.WriteString(fmt.Sprintf("%d", os.Getuid()))
case 'L':
if h, err := os.Hostname(); err != nil {
b.WriteString(fmt.Sprintf("%%!L(%v)", err))
} else {
b.WriteString(h)
}
case 'n':
b.WriteString(alias)
case 'p':
b.WriteString(host.tryKV(alias, "Port"))
case 'r':
b.WriteString(host.tryKV(alias, "User"))
case 'u':
b.WriteString(os.Getenv("USER"))
case '%':
b.WriteString("%")
default:
// In the event of a bad format char, fmt returns
// the mangled string and no error.
// It may be best to follow that practice, as
// it gives you a much better idea where things
// went wrong.
b.WriteString(`%!` + string(c))
}
continue
}
if c != '%' {
b.WriteByte(byte(c))
continue
}
sawPercent = true
}
if sawPercent {
b.WriteString("%!(NOVERB)")
}
return b.String()
}
func (host *Host) findKV(alias, key string) (string, error) {
lowerKey := strings.ToLower(key)
for _, node := range host.Nodes {
switch t := node.(type) {
case *Empty:
continue
case *KV:
// "keys are case insensitive" per the spec
lkey := strings.ToLower(t.Key)
if lkey == "match" {
panic("can't handle Match directives")
}
if lkey == lowerKey {
return t.Value, nil
}
case *Include:
val := t.Get(alias, key)
if val != "" {
return val, nil
}
default:
return "", fmt.Errorf("unknown Node type %v", t)
}
}
return "", fmt.Errorf("%v has no key %v", alias, key)
}
func (host *Host) tryKV(alias, key string) string {
v, _ := host.findKV(alias, key)
return v
}
// Get finds the first value in the configuration that matches the alias and
// contains key. Get returns the empty string if no value was found, or if the
// Config contains an invalid conditional Include value.
@@ -831,7 +927,7 @@ func init() {
func newConfig() *Config {
return &Config{
Hosts: []*Host{
&Host{
{
implicit: true,
Patterns: []*Pattern{matchAll},
Nodes: make([]Node, 0),

View File

@@ -482,6 +482,41 @@ func TestNoTrailingNewline(t *testing.T) {
}
}
func TestPercent(t *testing.T) {
b := bytes.NewBufferString(`Host wap
HostName wap.example.org
Port 22
User root
KexAlgorithms diffie-hellman-group1-sha1
`)
cfg, err := Decode(b)
if err != nil {
t.Fatal(err)
}
host := cfg.Hosts[1]
t.Logf("cfg is %v, %d hosts, Hosts %v, host %v", cfg, len(cfg.Hosts), cfg.Hosts, host)
home := os.Getenv("HOME")
user := os.Getenv("USER")
for _, tt := range []struct {
in string
out string
}{
{"hi", "hi"},
{"%dhi", home + "hi"},
{"%uhi", user + "hi"},
{"%h.%n.%p.%r.%u", "wap.example.org.wap.22.root." + user},
{"%Z", "%!Z"},
{"%", "%!(NOVERB)"},
{"%d%", home + "%!(NOVERB)"},
} {
o := host.percent("wap", tt.in)
if o != tt.out {
t.Errorf("%q: got %q, want %q", tt.in, o, tt.out)
}
}
}
func TestCustomFinder(t *testing.T) {
us := &UserSettings{}
us.ConfigFinder(func() string {