diff --git a/docs/changelog.rst b/docs/changelog.rst index cfefc9f2c..e84d1b28a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -62,6 +62,9 @@ Detailed list of changes - macOS: Fix the maximized window not taking up full space when the title bar is hidden or when :opt:`resize_in_steps` is configured (:iss:`6021`) +- ssh kitten: Change the syntax of glob patterns slightly to match common usage + elsewhere. Now the syntax is the same a "extendedglob" in most shells. + 0.27.1 [2023-02-07] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kittens/ssh/copy.py b/kittens/ssh/copy.py index f2ae3b63e..87f45e389 100644 --- a/kittens/ssh/copy.py +++ b/kittens/ssh/copy.py @@ -38,10 +38,12 @@ type=list A glob pattern. Files with names matching this pattern are excluded from being transferred. Useful when adding directories. Can be specified multiple times, if any of the patterns match the file will be -excluded. To exclude a directory use a pattern like :code:`**/directory_name/**`. -Based on standard wildcards with the addition that ``/**/`` matches any number of directories -and patterns starting with a single :code:`*` (as opposed to two asterisks) match any filename prefix. -See the :link:`detailed syntax `. +excluded. If the pattern includes a :code:`/` then it will match against the full +path, not just the filename. In such patterns you can use :code:`/**/` to match zero +or more directories. For example, to exclude a directory and everything under it use +:code:`**/directory_name`. +See the :link:`detailed syntax ` for +how wildcards match. --symlink-strategy @@ -52,7 +54,8 @@ the symlink, re-creating it on the remote machine. Setting this to :code:`resolv will cause the symlink to be followed and its target used as the file/directory to copy. The value of :code:`keep-path` is the same as :code:`resolve` except that the remote file path is derived from the symlink's path instead of the path of the symlink's target. -Note that this option does not apply to symlinks encountered while recursively copying directories. +Note that this option does not apply to symlinks encountered while recursively copying directories, +those are always preserved. ''' diff --git a/kitty_tests/ssh.py b/kitty_tests/ssh.py index 4bccb6b14..b03668d1e 100644 --- a/kitty_tests/ssh.py +++ b/kitty_tests/ssh.py @@ -75,6 +75,8 @@ print(' '.join(map(str, buf)))'''), lines=13, cols=77) os.makedirs(f'{local_home}/d1/d2/d3') touch('d1/d2/x') touch('d1/d2/w.exclude') + os.mkdir(f'{local_home}/d1/r') + touch('d1/r/noooo') os.symlink('d2/x', f'{local_home}/d1/y') os.symlink('simple-file', f'{local_home}/s1') os.symlink('simple-file', f'{local_home}/s2') @@ -85,7 +87,7 @@ copy s1 copy --symlink-strategy=keep-path s2 copy --dest=a/sfa simple-file copy --glob g.* -copy --exclude **/w.* d1 +copy --exclude **/w.* --exclude **/r d1 ''' self.check_bootstrap( sh, remote_home, test_script='env; exit 0', SHELL_INTEGRATION_VALUE='', conf=conf, home=local_home, diff --git a/tools/cmd/ssh/config.go b/tools/cmd/ssh/config.go index a84e0977d..a863a9cee 100644 --- a/tools/cmd/ssh/config.go +++ b/tools/cmd/ssh/config.go @@ -9,6 +9,7 @@ import ( "fmt" "io/fs" "os" + "path" "path/filepath" "strings" "time" @@ -230,7 +231,7 @@ type file_unique_id struct { } func excluded(pattern, path string) bool { - if strings.HasPrefix(pattern, "*") && !strings.HasPrefix(pattern, "**") { + if !strings.ContainsRune(pattern, '/') { path = filepath.Base(path) } if matched, err := doublestar.PathMatch(pattern, path); matched && err == nil { @@ -239,13 +240,13 @@ func excluded(pattern, path string) bool { return false } -func get_file_data(callback func(h *tar.Header, data []byte) error, seen map[file_unique_id]string, local_path, arcname string, exclude_patterns []string, recurse bool) error { +func get_file_data(callback func(h *tar.Header, data []byte) error, seen map[file_unique_id]string, local_path, arcname string, exclude_patterns []string) error { s, err := os.Lstat(local_path) if err != nil { return err } u, ok := s.Sys().(unix.Stat_t) - cb := func(h *tar.Header, data []byte) error { + cb := func(h *tar.Header, data []byte, arcname string) error { h.Name = arcname if h.Typeflag == tar.TypeDir { h.Name = strings.TrimRight(h.Name, "/") + "/" @@ -270,41 +271,57 @@ func get_file_data(callback func(h *tar.Header, data []byte) error, seen map[fil err = cb(&tar.Header{ Typeflag: tar.TypeSymlink, Linkname: target, - }, nil) + }, nil, arcname) if err != nil { return err } case fs.ModeDir: - err = cb(&tar.Header{Typeflag: tar.TypeDir}, nil) - if err != nil { - return err + local_path = filepath.Clean(local_path) + type entry struct { + path, arcname string } - if recurse { - local_path = filepath.Clean(local_path) - return filepath.WalkDir(local_path, func(path string, d fs.DirEntry, werr error) error { - clean_path := filepath.Clean(path) - if clean_path == local_path { - return nil + stack := []entry{{local_path, arcname}} + for len(stack) > 0 { + x := stack[0] + stack = stack[1:] + entries, err := os.ReadDir(x.path) + if err != nil { + if x.path == local_path { + return err } + continue + } + err = cb(&tar.Header{Typeflag: tar.TypeDir}, nil, x.arcname) + if err != nil { + return err + } + for _, e := range entries { + entry_path := filepath.Join(x.path, e.Name()) + aname := path.Join(x.arcname, e.Name()) + ok := true for _, pat := range exclude_patterns { - if excluded(pat, clean_path) { - return nil + if excluded(pat, entry_path) { + ok = false + break } } - if werr == nil { - rel, err := filepath.Rel(local_path, path) - if err == nil { - aname := filepath.Join(arcname, rel) - return get_file_data(callback, seen, clean_path, aname, nil, false) + if !ok { + continue + } + if e.IsDir() { + stack = append(stack, entry{entry_path, aname}) + } else { + err = get_file_data(callback, seen, entry_path, aname, exclude_patterns) + if err != nil { + return err } } - return nil - }) + } } case 0: // Regular file fid := file_unique_id{dev: u.Dev, inode: u.Ino} if prev, ok := seen[fid]; ok { // Hard link - err = cb(&tar.Header{Typeflag: tar.TypeLink, Linkname: prev}, nil) + err = cb(&tar.Header{Typeflag: tar.TypeLink, Linkname: prev}, nil, arcname) if err != nil { return err } @@ -314,7 +331,7 @@ func get_file_data(callback func(h *tar.Header, data []byte) error, seen map[fil if err != nil { return err } - err = cb(&tar.Header{Typeflag: tar.TypeReg}, data) + err = cb(&tar.Header{Typeflag: tar.TypeReg}, data, arcname) if err != nil { return err } @@ -327,7 +344,7 @@ func (ci *CopyInstruction) get_file_data(callback func(h *tar.Header, data []byt for _, folder_name := range []string{"__pycache__", ".DS_Store"} { ep = append(ep, "**/"+folder_name, "**/"+folder_name+"/**") } - return get_file_data(callback, seen, ci.local_path, ci.arcname, ep, true) + return get_file_data(callback, seen, ci.local_path, ci.arcname, ep) } type ConfigSet struct { diff --git a/tools/cmd/ssh/main.go b/tools/cmd/ssh/main.go index c3380cfaa..cbe257247 100644 --- a/tools/cmd/ssh/main.go +++ b/tools/cmd/ssh/main.go @@ -275,7 +275,7 @@ func make_tarfile(cd *connection_data, get_local_env func(string) (string, bool) return } for _, ci := range cd.host_opts.Copy { - err = get_file_data(add, seen, ci.local_path, ci.arcname, ci.exclude_patterns, true) + err = ci.get_file_data(add, seen) if err != nil { return nil, err }