diff --git a/kittens/remote_file/ssh.go b/kittens/remote_file/ssh.go index ef9398d26..47da5b937 100644 --- a/kittens/remote_file/ssh.go +++ b/kittens/remote_file/ssh.go @@ -44,8 +44,11 @@ func parse_conn_data(raw string) (*SSHConnectionData, error) { cmdline = append(cmdline, s) } } + // Python: sk_cmdline[:-2] always removes last 2, or returns empty if fewer items if len(cmdline) >= 2 { cmdline = cmdline[:len(cmdline)-2] + } else { + cmdline = cmdline[:0] } ans.SSHKittenCmdline = cmdline return ans, nil @@ -96,10 +99,28 @@ func hostname_matches(from_hyperlink, actual string) bool { return fl == al } +// pythonSplitExt mirrors Python's os.path.splitext behavior for dotfiles. +// For a file like "/x/.bashrc" (basename ".bashrc"), Python returns ("/x/.bashrc", ""). +// Go's filepath.Ext would return (".bashrc", "/x/"), losing the filename. +// Rule: if basename starts with '.' and has no other dots, treat extension as empty. +func pythonSplitExt(path string) (string, string) { + ext := filepath.Ext(path) + if ext == "" { + return path, "" + } + basename := filepath.Base(path) + // If basename starts with '.' and has exactly one dot (the leading dot), + // then this is a dotfile with no extension. + if strings.HasPrefix(basename, ".") && strings.Count(basename, ".") == 1 { + return path, "" + } + base := strings.TrimSuffix(path, ext) + return base, ext +} + func auto_rename_dest(dest string, exists func(string) bool) string { q := dest - ext := filepath.Ext(dest) - base := strings.TrimSuffix(dest, ext) + base, ext := pythonSplitExt(dest) for c := 1; exists(q); c++ { q = fmt.Sprintf("%s-%d%s", base, c, ext) } diff --git a/kittens/remote_file/ssh_test.go b/kittens/remote_file/ssh_test.go index 9ddefce5b..461792538 100644 --- a/kittens/remote_file/ssh_test.go +++ b/kittens/remote_file/ssh_test.go @@ -75,3 +75,23 @@ func TestAutoRenameDest(t *testing.T) { t.Fatalf("auto_rename_dest: %q", got) } } + +func TestAutoRenameDotfile(t *testing.T) { + taken := map[string]bool{"/x/.bashrc": true} + got := auto_rename_dest("/x/.bashrc", func(p string) bool { return taken[p] }) + if got != "/x/.bashrc-1" { + t.Fatalf("auto_rename_dest dotfile: got %q, want /x/.bashrc-1", got) + } +} + +func TestParseConnDataSSHKittenEdge(t *testing.T) { + // ssh-kitten with exactly 1 cmdline item after sentinel should result in empty SSHKittenCmdline + raw := fmt.Sprintf(`["%s", "onlyitem"]`, is_ssh_kitten_sentinel) + c, err := parse_conn_data(raw) + if err != nil { + t.Fatal(err) + } + if !c.IsSSHKitten || c.Hostname != "onlyitem" || len(c.SSHKittenCmdline) != 0 { + t.Fatalf("ssh kitten edge case (single item): %+v, SSHKittenCmdline=%v", c, c.SSHKittenCmdline) + } +}