├── go.mod ├── Readme ├── example_test.go ├── filesystem.go ├── LICENSE ├── walk.go └── walk_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module "github.com/kr/fs" 2 | -------------------------------------------------------------------------------- /Readme: -------------------------------------------------------------------------------- 1 | Filesystem Package 2 | 3 | http://godoc.org/github.com/kr/fs 4 | 5 | --- 6 | 7 | Using Go 1.16 or later? 8 | 9 | You might be interested https://kr.dev/walk 10 | instead. That one uses the new package io/fs. 11 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package fs_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/kr/fs" 8 | ) 9 | 10 | func ExampleWalker() { 11 | walker := fs.Walk("/usr/lib") 12 | for walker.Step() { 13 | if err := walker.Err(); err != nil { 14 | fmt.Fprintln(os.Stderr, err) 15 | continue 16 | } 17 | fmt.Println(walker.Path()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /filesystem.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // FileSystem defines the methods of an abstract filesystem. 10 | type FileSystem interface { 11 | 12 | // ReadDir reads the directory named by dirname and returns a 13 | // list of directory entries. 14 | ReadDir(dirname string) ([]os.FileInfo, error) 15 | 16 | // Lstat returns a FileInfo describing the named file. If the file is a 17 | // symbolic link, the returned FileInfo describes the symbolic link. Lstat 18 | // makes no attempt to follow the link. 19 | Lstat(name string) (os.FileInfo, error) 20 | 21 | // Join joins any number of path elements into a single path, adding a 22 | // separator if necessary. The result is Cleaned; in particular, all 23 | // empty strings are ignored. 24 | // 25 | // The separator is FileSystem specific. 26 | Join(elem ...string) string 27 | } 28 | 29 | // fs represents a FileSystem provided by the os package. 30 | type fs struct{} 31 | 32 | func (f *fs) ReadDir(dirname string) ([]os.FileInfo, error) { return ioutil.ReadDir(dirname) } 33 | 34 | func (f *fs) Lstat(name string) (os.FileInfo, error) { return os.Lstat(name) } 35 | 36 | func (f *fs) Join(elem ...string) string { return filepath.Join(elem...) } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /walk.go: -------------------------------------------------------------------------------- 1 | // Package fs provides filesystem-related functions. 2 | package fs 3 | 4 | import ( 5 | "os" 6 | ) 7 | 8 | // Walker provides a convenient interface for iterating over the 9 | // descendants of a filesystem path. 10 | // Successive calls to the Step method will step through each 11 | // file or directory in the tree, including the root. The files 12 | // are walked in lexical order, which makes the output deterministic 13 | // but means that for very large directories Walker can be inefficient. 14 | // Walker does not follow symbolic links. 15 | type Walker struct { 16 | fs FileSystem 17 | cur item 18 | stack []item 19 | descend bool 20 | } 21 | 22 | type item struct { 23 | path string 24 | info os.FileInfo 25 | err error 26 | } 27 | 28 | // Walk returns a new Walker rooted at root. 29 | func Walk(root string) *Walker { 30 | return WalkFS(root, new(fs)) 31 | } 32 | 33 | // WalkFS returns a new Walker rooted at root on the FileSystem fs. 34 | func WalkFS(root string, fs FileSystem) *Walker { 35 | info, err := fs.Lstat(root) 36 | return &Walker{ 37 | fs: fs, 38 | stack: []item{{root, info, err}}, 39 | } 40 | } 41 | 42 | // Step advances the Walker to the next file or directory, 43 | // which will then be available through the Path, Stat, 44 | // and Err methods. 45 | // It returns false when the walk stops at the end of the tree. 46 | func (w *Walker) Step() bool { 47 | if w.descend && w.cur.err == nil && w.cur.info.IsDir() { 48 | list, err := w.fs.ReadDir(w.cur.path) 49 | if err != nil { 50 | w.cur.err = err 51 | w.stack = append(w.stack, w.cur) 52 | } else { 53 | for i := len(list) - 1; i >= 0; i-- { 54 | path := w.fs.Join(w.cur.path, list[i].Name()) 55 | w.stack = append(w.stack, item{path, list[i], nil}) 56 | } 57 | } 58 | } 59 | 60 | if len(w.stack) == 0 { 61 | return false 62 | } 63 | i := len(w.stack) - 1 64 | w.cur = w.stack[i] 65 | w.stack = w.stack[:i] 66 | w.descend = true 67 | return true 68 | } 69 | 70 | // Path returns the path to the most recent file or directory 71 | // visited by a call to Step. It contains the argument to Walk 72 | // as a prefix; that is, if Walk is called with "dir", which is 73 | // a directory containing the file "a", Path will return "dir/a". 74 | func (w *Walker) Path() string { 75 | return w.cur.path 76 | } 77 | 78 | // Stat returns info for the most recent file or directory 79 | // visited by a call to Step. 80 | func (w *Walker) Stat() os.FileInfo { 81 | return w.cur.info 82 | } 83 | 84 | // Err returns the error, if any, for the most recent attempt 85 | // by Step to visit a file or directory. If a directory has 86 | // an error, w will not descend into that directory. 87 | func (w *Walker) Err() error { 88 | return w.cur.err 89 | } 90 | 91 | // SkipDir causes the currently visited directory to be skipped. 92 | // If w is not on a directory, SkipDir has no effect. 93 | func (w *Walker) SkipDir() { 94 | w.descend = false 95 | } 96 | -------------------------------------------------------------------------------- /walk_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package fs_test 6 | 7 | import ( 8 | "os" 9 | "path/filepath" 10 | "runtime" 11 | "testing" 12 | 13 | "github.com/kr/fs" 14 | ) 15 | 16 | type PathTest struct { 17 | path, result string 18 | } 19 | 20 | type Node struct { 21 | name string 22 | entries []*Node // nil if the entry is a file 23 | mark int 24 | } 25 | 26 | var tree = &Node{ 27 | "testdata", 28 | []*Node{ 29 | {"a", nil, 0}, 30 | {"b", []*Node{}, 0}, 31 | {"c", nil, 0}, 32 | { 33 | "d", 34 | []*Node{ 35 | {"x", nil, 0}, 36 | {"y", []*Node{}, 0}, 37 | { 38 | "z", 39 | []*Node{ 40 | {"u", nil, 0}, 41 | {"v", nil, 0}, 42 | }, 43 | 0, 44 | }, 45 | }, 46 | 0, 47 | }, 48 | }, 49 | 0, 50 | } 51 | 52 | func walkTree(n *Node, path string, f func(path string, n *Node)) { 53 | f(path, n) 54 | for _, e := range n.entries { 55 | walkTree(e, filepath.Join(path, e.name), f) 56 | } 57 | } 58 | 59 | func makeTree(t *testing.T) { 60 | walkTree(tree, tree.name, func(path string, n *Node) { 61 | if n.entries == nil { 62 | fd, err := os.Create(path) 63 | if err != nil { 64 | t.Errorf("makeTree: %v", err) 65 | return 66 | } 67 | fd.Close() 68 | } else { 69 | os.Mkdir(path, 0770) 70 | } 71 | }) 72 | } 73 | 74 | func markTree(n *Node) { walkTree(n, "", func(path string, n *Node) { n.mark++ }) } 75 | 76 | func checkMarks(t *testing.T, report bool) { 77 | walkTree(tree, tree.name, func(path string, n *Node) { 78 | if n.mark != 1 && report { 79 | t.Errorf("node %s mark = %d; expected 1", path, n.mark) 80 | } 81 | n.mark = 0 82 | }) 83 | } 84 | 85 | // Assumes that each node name is unique. Good enough for a test. 86 | // If clear is true, any incoming error is cleared before return. The errors 87 | // are always accumulated, though. 88 | func mark(path string, info os.FileInfo, err error, errors *[]error, clear bool) error { 89 | if err != nil { 90 | *errors = append(*errors, err) 91 | if clear { 92 | return nil 93 | } 94 | return err 95 | } 96 | name := info.Name() 97 | walkTree(tree, tree.name, func(path string, n *Node) { 98 | if n.name == name { 99 | n.mark++ 100 | } 101 | }) 102 | return nil 103 | } 104 | 105 | func TestWalk(t *testing.T) { 106 | makeTree(t) 107 | errors := make([]error, 0, 10) 108 | clear := true 109 | markFn := func(walker *fs.Walker) (err error) { 110 | for walker.Step() { 111 | err = mark(walker.Path(), walker.Stat(), walker.Err(), &errors, clear) 112 | if err != nil { 113 | break 114 | } 115 | } 116 | return err 117 | } 118 | // Expect no errors. 119 | err := markFn(fs.Walk(tree.name)) 120 | if err != nil { 121 | t.Fatalf("no error expected, found: %s", err) 122 | } 123 | if len(errors) != 0 { 124 | t.Fatalf("unexpected errors: %s", errors) 125 | } 126 | checkMarks(t, true) 127 | errors = errors[0:0] 128 | 129 | // Test permission errors. Only possible if we're not root 130 | // and only on some file systems (AFS, FAT). To avoid errors during 131 | // all.bash on those file systems, skip during go test -short. 132 | if os.Getuid() > 0 && !testing.Short() { 133 | // introduce 2 errors: chmod top-level directories to 0 134 | os.Chmod(filepath.Join(tree.name, tree.entries[1].name), 0) 135 | os.Chmod(filepath.Join(tree.name, tree.entries[3].name), 0) 136 | 137 | // 3) capture errors, expect two. 138 | // mark respective subtrees manually 139 | markTree(tree.entries[1]) 140 | markTree(tree.entries[3]) 141 | // correct double-marking of directory itself 142 | tree.entries[1].mark-- 143 | tree.entries[3].mark-- 144 | err := markFn(fs.Walk(tree.name)) 145 | if err != nil { 146 | t.Fatalf("expected no error return from Walk, got %s", err) 147 | } 148 | if len(errors) != 2 { 149 | t.Errorf("expected 2 errors, got %d: %s", len(errors), errors) 150 | } 151 | // the inaccessible subtrees were marked manually 152 | checkMarks(t, true) 153 | errors = errors[0:0] 154 | 155 | // 4) capture errors, stop after first error. 156 | // mark respective subtrees manually 157 | markTree(tree.entries[1]) 158 | markTree(tree.entries[3]) 159 | // correct double-marking of directory itself 160 | tree.entries[1].mark-- 161 | tree.entries[3].mark-- 162 | clear = false // error will stop processing 163 | err = markFn(fs.Walk(tree.name)) 164 | if err == nil { 165 | t.Fatalf("expected error return from Walk") 166 | } 167 | if len(errors) != 1 { 168 | t.Errorf("expected 1 error, got %d: %s", len(errors), errors) 169 | } 170 | // the inaccessible subtrees were marked manually 171 | checkMarks(t, false) 172 | errors = errors[0:0] 173 | 174 | // restore permissions 175 | os.Chmod(filepath.Join(tree.name, tree.entries[1].name), 0770) 176 | os.Chmod(filepath.Join(tree.name, tree.entries[3].name), 0770) 177 | } 178 | 179 | // cleanup 180 | if err := os.RemoveAll(tree.name); err != nil { 181 | t.Errorf("removeTree: %v", err) 182 | } 183 | } 184 | 185 | func TestBug3486(t *testing.T) { // http://code.google.com/p/go/issues/detail?id=3486 186 | root, err := filepath.EvalSymlinks(runtime.GOROOT()) 187 | if err != nil { 188 | t.Fatal(err) 189 | } 190 | lib := filepath.Join(root, "lib") 191 | src := filepath.Join(root, "src") 192 | seenSrc := false 193 | walker := fs.Walk(root) 194 | for walker.Step() { 195 | if walker.Err() != nil { 196 | t.Fatal(walker.Err()) 197 | } 198 | 199 | switch walker.Path() { 200 | case lib: 201 | walker.SkipDir() 202 | case src: 203 | seenSrc = true 204 | } 205 | } 206 | if !seenSrc { 207 | t.Fatalf("%q not seen", src) 208 | } 209 | } 210 | --------------------------------------------------------------------------------