From 8c146eac4b8a936618426ec36d58b3b094b9530e Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 10 Aug 2018 19:48:42 -0600 Subject: [PATCH 01/10] ls: Implement directory filter, optionally subfolders --- cmd/restic/cmd_ls.go | 47 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index f7996b438..cbfe73b42 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -2,6 +2,8 @@ package main import ( "context" + "path/filepath" + "strings" "github.com/spf13/cobra" @@ -11,7 +13,7 @@ import ( ) var cmdLs = &cobra.Command{ - Use: "ls [flags] [snapshot-ID ...]", + Use: "ls [flags] [snapshot-ID] [dir...]", Short: "List files in a snapshot", Long: ` The "ls" command allows listing files and directories in a snapshot. @@ -26,10 +28,11 @@ The special snapshot-ID "latest" can be used to list files and directories of th // LsOptions collects all options for the ls command. type LsOptions struct { - ListLong bool - Host string - Tags restic.TagLists - Paths []string + ListLong bool + Host string + Tags restic.TagLists + Paths []string + Recursive bool } var lsOptions LsOptions @@ -43,6 +46,7 @@ func init() { flags.StringVarP(&lsOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given") flags.Var(&lsOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot ID is given") flags.StringArrayVar(&lsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot ID is given") + flags.BoolVar(&lsOptions.Recursive, "recursive", false, "include files in subfolders of the listed directories") } func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { @@ -59,19 +63,48 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { return err } + // extract any specific directories to walk + dirs := args[1:] + ctx, cancel := context.WithCancel(gopts.ctx) defer cancel() - for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) { + for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args[:1]) { Verbosef("snapshot %s of %v at %s):\n", sn.ID().Str(), sn.Paths, sn.Time) err := walker.Walk(ctx, repo, *sn.Tree, nil, func(nodepath string, node *restic.Node, err error) (bool, error) { if err != nil { return false, err } - if node == nil { return false, nil } + + // apply any directory filters + if len(dirs) > 0 { + var nodeDir string + if !opts.Recursive { + // only needed for exact directory match; i.e. no subfolders + nodeDir = filepath.Dir(nodepath) + } + var match bool + for _, dir := range dirs { + if opts.Recursive { + if strings.HasPrefix(nodepath, dir) { + match = true + break + } + } else { + if nodeDir == dir { + match = true + break + } + } + } + if !match { + return true, nil + } + } + Printf("%s\n", formatNode(nodepath, node, lsOptions.ListLong)) return false, nil }) From 156d85a29bcd1f677a7c26548def407b30702675 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 10 Aug 2018 22:10:02 -0600 Subject: [PATCH 02/10] Minor fixes/tweaks; add docs --- changelog/unreleased/1941 | 15 +++++++++++++++ cmd/restic/cmd_ls.go | 27 +++++++++++++++++++++------ 2 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 changelog/unreleased/1941 diff --git a/changelog/unreleased/1941 b/changelog/unreleased/1941 new file mode 100644 index 000000000..d2a70a02d --- /dev/null +++ b/changelog/unreleased/1941 @@ -0,0 +1,15 @@ +Enhancement: Add directory filter to ls command + +The ls command can now be filtered by directories, so that only files in the +given directories will be shown. If the --recursive flag is specified, then +ls will traverse subfolders and list their files as well. + +It used to be possible to specify multiple snapshots, but that has been +replaced by only one snapshot and the possibility of specifying multiple +directories. + +Specifying directories constrains the walk, which can significantly speed up +the listing. + +https://github.com/restic/restic/issues/1940 +https://github.com/restic/restic/pull/1941 diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index cbfe73b42..8de5578b4 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -13,12 +13,21 @@ import ( ) var cmdLs = &cobra.Command{ - Use: "ls [flags] [snapshot-ID] [dir...]", + Use: "ls [flags] [snapshotID] [dir...]", Short: "List files in a snapshot", Long: ` -The "ls" command allows listing files and directories in a snapshot. +The "ls" command lists files and directories in a snapshot. -The special snapshot-ID "latest" can be used to list files and directories of the latest snapshot in the repository. +The special snapshot ID "latest" can be used to list files and +directories of the latest snapshot in the repository. The +--host flag can be used in conjunction to select the latest +snapshot originating from a certain host only. + +File listings can optionally be filtered by directories. Any +positional arguments after the snapshot ID are interpreted as +directory paths, and only files inside those directories will +be listed. If the --recursive flag is used, then the filter +will allow traversing into matching directories' subfolders. `, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { @@ -42,7 +51,6 @@ func init() { flags := cmdLs.Flags() flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode") - flags.StringVarP(&lsOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given") flags.Var(&lsOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot ID is given") flags.StringArrayVar(&lsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot ID is given") @@ -64,12 +72,19 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { } // extract any specific directories to walk - dirs := args[1:] + var dirs []string + if len(args) > 1 { + dirs = args[1:] + } ctx, cancel := context.WithCancel(gopts.ctx) defer cancel() for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args[:1]) { - Verbosef("snapshot %s of %v at %s):\n", sn.ID().Str(), sn.Paths, sn.Time) + dirsToPrint := opts.Paths + if len(dirs) > 0 { + dirsToPrint = dirs + } + Verbosef("snapshot %s of %v at %s):\n", sn.ID().Str(), dirsToPrint, sn.Time) err := walker.Walk(ctx, repo, *sn.Tree, nil, func(nodepath string, node *restic.Node, err error) (bool, error) { if err != nil { From 00e2fd8b5ff5466af7543dbdf5932332b8f91f9c Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sat, 11 Aug 2018 15:25:22 -0600 Subject: [PATCH 03/10] Apply feedback and use SkipNode --- cmd/restic/cmd_ls.go | 59 +++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index 8de5578b4..281763433 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -2,12 +2,13 @@ package main import ( "context" - "path/filepath" + "path" "strings" "github.com/spf13/cobra" "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/walker" ) @@ -25,9 +26,11 @@ snapshot originating from a certain host only. File listings can optionally be filtered by directories. Any positional arguments after the snapshot ID are interpreted as -directory paths, and only files inside those directories will -be listed. If the --recursive flag is used, then the filter +absolute directory paths, and only files inside those directories +will be listed. If the --recursive flag is used, then the filter will allow traversing into matching directories' subfolders. +Any directory paths specified must be absolute (starting with +a path separator); paths use the forward slash '/' as separator. `, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { @@ -75,16 +78,17 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { var dirs []string if len(args) > 1 { dirs = args[1:] + for _, dir := range dirs { + if !strings.HasPrefix(dir, "/") { + return errors.Fatal("All path filters must be absolute, starting with a forward slash '/'") + } + } } ctx, cancel := context.WithCancel(gopts.ctx) defer cancel() for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args[:1]) { - dirsToPrint := opts.Paths - if len(dirs) > 0 { - dirsToPrint = dirs - } - Verbosef("snapshot %s of %v at %s):\n", sn.ID().Str(), dirsToPrint, sn.Time) + Verbosef("snapshot %s of %v filtered by %v at %s):\n", sn.ID().Str(), sn.Paths, dirs, sn.Time) err := walker.Walk(ctx, repo, *sn.Tree, nil, func(nodepath string, node *restic.Node, err error) (bool, error) { if err != nil { @@ -96,31 +100,40 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { // apply any directory filters if len(dirs) > 0 { - var nodeDir string - if !opts.Recursive { - // only needed for exact directory match; i.e. no subfolders - nodeDir = filepath.Dir(nodepath) + // this first iteration ensures we do not traverse branches that + // are not in matching trees or will not lead us to matching trees + var walk bool + for _, dir := range dirs { + if fs.HasPathPrefix(nodepath, dir) || fs.HasPathPrefix(dir, nodepath) { + // we are either in or approaching the right tree + walk = true + break + } } + if !walk { + return false, walker.SkipNode + } + + // this second iteration ensures that we get an exact match + // according to the filter and whether we should match subfolders var match bool for _, dir := range dirs { - if opts.Recursive { - if strings.HasPrefix(nodepath, dir) { - match = true - break - } - } else { - if nodeDir == dir { - match = true - break - } + if opts.Recursive && fs.HasPathPrefix(dir, nodepath) { + match = true + break + } + if !opts.Recursive && path.Dir(nodepath) == dir { + match = true + break } } if !match { - return true, nil + return false, nil } } Printf("%s\n", formatNode(nodepath, node, lsOptions.ListLong)) + return false, nil }) if err != nil { From 7a468d1226cf098f273fa9d4b4a7b5f8ee77562c Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sat, 11 Aug 2018 16:18:09 -0600 Subject: [PATCH 04/10] Speed up nonrecursive queries; include exact filter match --- cmd/restic/cmd_ls.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index 281763433..bca2275b3 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -104,8 +104,20 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { // are not in matching trees or will not lead us to matching trees var walk bool for _, dir := range dirs { - if fs.HasPathPrefix(nodepath, dir) || fs.HasPathPrefix(dir, nodepath) { - // we are either in or approaching the right tree + approachingMatchingTree := fs.HasPathPrefix(nodepath, dir) + inMatchingTree := fs.HasPathPrefix(dir, nodepath) + + // this condition is complex, but it basically requires that we + // are either approaching a matching tree (not yet deep enough) + // or: if recursive, we have entered a matching tree; if non- + // recursive, then that we are at exactly the right depth + // (we can do the walk correctly by just using the condition of + // "approachingMatchingTree || inMatchingTree", but it will be + // much slower for non-recursive queries since it will continue + // to traverse subtrees that are too deep and won't match -- this + // extra check allows us to return SkipNode if we've gone TOO deep, + // which skips all its subfolders) + if approachingMatchingTree || opts.Recursive || (inMatchingTree && dir == path.Dir(nodepath)) { walk = true break } @@ -118,6 +130,15 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { // according to the filter and whether we should match subfolders var match bool for _, dir := range dirs { + if nodepath == dir { + // special case: match the directory filter exactly, + // which may or may not be desirable depending on your + // use case (for example, this is unnecessary when + // wanting to simply list the contents of a folder, + // rather than all files matching a directory prefix) + match = true + break + } if opts.Recursive && fs.HasPathPrefix(dir, nodepath) { match = true break From 11ce57289484e24edb0b504222852dba5f0196b7 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sat, 11 Aug 2018 17:17:43 -0600 Subject: [PATCH 05/10] Fix bug where some folder listings were empty --- cmd/restic/cmd_ls.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index bca2275b3..02935b85f 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -100,11 +100,13 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { // apply any directory filters if len(dirs) > 0 { + nodeDir := path.Dir(nodepath) + // this first iteration ensures we do not traverse branches that // are not in matching trees or will not lead us to matching trees var walk bool for _, dir := range dirs { - approachingMatchingTree := fs.HasPathPrefix(nodepath, dir) + approachingMatchingTree := fs.HasPathPrefix(nodeDir, dir) inMatchingTree := fs.HasPathPrefix(dir, nodepath) // this condition is complex, but it basically requires that we @@ -117,7 +119,7 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { // to traverse subtrees that are too deep and won't match -- this // extra check allows us to return SkipNode if we've gone TOO deep, // which skips all its subfolders) - if approachingMatchingTree || opts.Recursive || (inMatchingTree && dir == path.Dir(nodepath)) { + if approachingMatchingTree || opts.Recursive || (inMatchingTree && dir == nodeDir) { walk = true break } @@ -143,7 +145,7 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { match = true break } - if !opts.Recursive && path.Dir(nodepath) == dir { + if !opts.Recursive && nodeDir == dir { match = true break } From 0deb4e599485412bd719aa1f9a04cd707a3d136b Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 12 Aug 2018 21:59:57 +0200 Subject: [PATCH 06/10] ls: Check dirs before opening the repository Users get feedback instantly, and before any expensive network calls have been made. --- cmd/restic/cmd_ls.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index 02935b85f..9c7d8c0f4 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -65,15 +65,6 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { return errors.Fatal("Invalid arguments, either give one or more snapshot IDs or set filters.") } - repo, err := OpenRepository(gopts) - if err != nil { - return err - } - - if err = repo.LoadIndex(gopts.ctx); err != nil { - return err - } - // extract any specific directories to walk var dirs []string if len(args) > 1 { @@ -85,6 +76,15 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { } } + repo, err := OpenRepository(gopts) + if err != nil { + return err + } + + if err = repo.LoadIndex(gopts.ctx); err != nil { + return err + } + ctx, cancel := context.WithCancel(gopts.ctx) defer cancel() for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args[:1]) { From 7f617cfd7f3a06def7de3426cc6eecbdbd44b8f1 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 12 Aug 2018 22:01:38 +0200 Subject: [PATCH 07/10] ls: Use nodepath for filter --- cmd/restic/cmd_ls.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index 9c7d8c0f4..e6d11fa78 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -106,7 +106,7 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { // are not in matching trees or will not lead us to matching trees var walk bool for _, dir := range dirs { - approachingMatchingTree := fs.HasPathPrefix(nodeDir, dir) + approachingMatchingTree := fs.HasPathPrefix(nodepath, dir) inMatchingTree := fs.HasPathPrefix(dir, nodepath) // this condition is complex, but it basically requires that we From ace5cc4ed3f52a3e4da20d83963a23a2545d2183 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 12 Aug 2018 22:02:59 +0200 Subject: [PATCH 08/10] ls: Only skip directory nodes Special case for Walk(): When SkipDir is returned for a non-dir node, the remaining nodes for the current tree are skipped. We don't want that. --- cmd/restic/cmd_ls.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index e6d11fa78..b49cb01f0 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -125,7 +125,15 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { } } if !walk { - return false, walker.SkipNode + if node.Type == "dir" { + // signal Walk() that it should not descend into the tree. + return false, walker.SkipNode + } + + // we must not return SkipNode for non-dir nodes because + // then the remaining nodes in the same tree would be + // skipped, so return nil instead + return false, nil } // this second iteration ensures that we get an exact match From 7e34de4c29451dbd883f43d00be9856ebf92a88c Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 12 Aug 2018 22:18:44 +0200 Subject: [PATCH 09/10] ls: Add comments --- cmd/restic/cmd_ls.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index b49cb01f0..1fb08c7a2 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -106,7 +106,17 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { // are not in matching trees or will not lead us to matching trees var walk bool for _, dir := range dirs { + + // the current node path is a prefix for one of the + // directories, so we're interested in something deeper in the + // tree. Example: + // nodepath: "/test" + // dir: "/test/foo" approachingMatchingTree := fs.HasPathPrefix(nodepath, dir) + + // we're within one of the selected dirs, example: + // nodepath: "/test/foo" + // dir: "/test" inMatchingTree := fs.HasPathPrefix(dir, nodepath) // this condition is complex, but it basically requires that we From 9630398e3baaa137d7272d9b302e26c993d572d5 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 12 Aug 2018 23:13:34 +0200 Subject: [PATCH 10/10] ls: Rework and simplify logic This commit introduces two functions: withinDir() and approachingMatchingTree() Both bind the list of directories with a closure, so we don't need to iterate over the list in the function passed to Walk(). This reduces the indentation level and since we can just use return, we don't need the breaks any more. The case that len(dirs) == 0 can also be handled by the functions with a return, which saves another indentation level. The main function body of the function passed to Walk() was reduced to three cases: * Within one of the dirs: Print the node, and if recursive operation is requested, directly return, so the walker continues recursive traversal * Approaching one of the dirs: don't print anything, but continue recursive traversal. * Nothing of the two: abort walking this branch of the tree. --- cmd/restic/cmd_ls.go | 123 ++++++++++++++++++------------------------- 1 file changed, 51 insertions(+), 72 deletions(-) diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index 1fb08c7a2..e45ffb9d0 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -2,7 +2,6 @@ package main import ( "context" - "path" "strings" "github.com/spf13/cobra" @@ -76,6 +75,40 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { } } + withinDir := func(nodepath string) bool { + if len(dirs) == 0 { + return true + } + + for _, dir := range dirs { + // we're within one of the selected dirs, example: + // nodepath: "/test/foo" + // dir: "/test" + if fs.HasPathPrefix(dir, nodepath) { + return true + } + } + return false + } + + approachingMatchingTree := func(nodepath string) bool { + if len(dirs) == 0 { + return true + } + + for _, dir := range dirs { + // the current node path is a prefix for one of the + // directories, so we're interested in something deeper in the + // tree. Example: + // nodepath: "/test" + // dir: "/test/foo" + if fs.HasPathPrefix(nodepath, dir) { + return true + } + } + return false + } + repo, err := OpenRepository(gopts) if err != nil { return err @@ -98,85 +131,31 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { return false, nil } - // apply any directory filters - if len(dirs) > 0 { - nodeDir := path.Dir(nodepath) + if withinDir(nodepath) { + // if we're within a dir, print the node + Printf("%s\n", formatNode(nodepath, node, lsOptions.ListLong)) - // this first iteration ensures we do not traverse branches that - // are not in matching trees or will not lead us to matching trees - var walk bool - for _, dir := range dirs { - - // the current node path is a prefix for one of the - // directories, so we're interested in something deeper in the - // tree. Example: - // nodepath: "/test" - // dir: "/test/foo" - approachingMatchingTree := fs.HasPathPrefix(nodepath, dir) - - // we're within one of the selected dirs, example: - // nodepath: "/test/foo" - // dir: "/test" - inMatchingTree := fs.HasPathPrefix(dir, nodepath) - - // this condition is complex, but it basically requires that we - // are either approaching a matching tree (not yet deep enough) - // or: if recursive, we have entered a matching tree; if non- - // recursive, then that we are at exactly the right depth - // (we can do the walk correctly by just using the condition of - // "approachingMatchingTree || inMatchingTree", but it will be - // much slower for non-recursive queries since it will continue - // to traverse subtrees that are too deep and won't match -- this - // extra check allows us to return SkipNode if we've gone TOO deep, - // which skips all its subfolders) - if approachingMatchingTree || opts.Recursive || (inMatchingTree && dir == nodeDir) { - walk = true - break - } - } - if !walk { - if node.Type == "dir" { - // signal Walk() that it should not descend into the tree. - return false, walker.SkipNode - } - - // we must not return SkipNode for non-dir nodes because - // then the remaining nodes in the same tree would be - // skipped, so return nil instead - return false, nil - } - - // this second iteration ensures that we get an exact match - // according to the filter and whether we should match subfolders - var match bool - for _, dir := range dirs { - if nodepath == dir { - // special case: match the directory filter exactly, - // which may or may not be desirable depending on your - // use case (for example, this is unnecessary when - // wanting to simply list the contents of a folder, - // rather than all files matching a directory prefix) - match = true - break - } - if opts.Recursive && fs.HasPathPrefix(dir, nodepath) { - match = true - break - } - if !opts.Recursive && nodeDir == dir { - match = true - break - } - } - if !match { + // if recursive listing is requested, signal the walker that it + // should continue walking recursively + if opts.Recursive { return false, nil } } - Printf("%s\n", formatNode(nodepath, node, lsOptions.ListLong)) + // if there's an upcoming match deeper in the tree (but we're not + // there yet), signal the walker to descend into any subdirs + if approachingMatchingTree(nodepath) { + return false, nil + } + // otherwise, signal the walker to not walk recursively into any + // subdirs + if node.Type == "dir" { + return false, walker.SkipNode + } return false, nil }) + if err != nil { return err }