Skip to content

Commit 136345e

Browse files
authored
Merge pull request #62 from LandonTClipp/preOrderDFS
Add Pre-Order DFS walk
2 parents 9711280 + 965c128 commit 136345e

File tree

8 files changed

+120
-82
lines changed

8 files changed

+120
-82
lines changed

.github/workflows/golangci-lint.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ jobs:
1818
steps:
1919
- uses: actions/setup-go@v4
2020
with:
21-
go-version: '1.19'
21+
go-version: '1.21'
2222
cache: false
2323
- uses: actions/checkout@v3
2424
- name: golangci-lint
2525
uses: golangci/golangci-lint-action@v3
2626
with:
27-
version: v1.52.2
27+
version: v1.54

.github/workflows/testing.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
strategy:
1313
matrix:
1414
os: [ macos-latest, ubuntu-latest]
15-
go_vers: ['1.20']
15+
go_vers: ["1.21"]
1616
steps:
1717
- uses: actions/checkout@v2
1818
with:

.travis.yml

-19
This file was deleted.

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/chigopher/pathlib
22

3-
go 1.18
3+
go 1.21
44

55
require (
66
github.com/spf13/afero v1.4.0

path.go

-18
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,6 @@ func (p *Path) Parent() *Path {
319319
//
320320
// This will fail if the underlying afero filesystem does not implement
321321
// afero.LinkReader.
322-
//
323-
// THIS METHOD IS NOT TYPE SAFE.
324322
func (p *Path) Readlink() (*Path, error) {
325323
linkReader, ok := p.Fs().(afero.LinkReader)
326324
if !ok {
@@ -381,8 +379,6 @@ func resolveAllHelper(path *Path) (*Path, error) {
381379
// should be identical to the `readlink -f` command from POSIX OSs.
382380
// This will fail if the underlying afero filesystem does not implement
383381
// afero.LinkReader. The path will be returned unchanged on errors.
384-
//
385-
// THIS METHOD IS NOT TYPE SAFE.
386382
func (p *Path) ResolveAll() (*Path, error) {
387383
return resolveAllHelper(p)
388384
}
@@ -479,8 +475,6 @@ func (p *Path) RelativeToStr(other string) (*Path, error) {
479475
// afero.Lstater but returns false for the "lstat called" return value.
480476
//
481477
// A nil os.FileInfo is returned on errors.
482-
//
483-
// THIS METHOD IS NOT TYPE SAFE.
484478
func (p *Path) Lstat() (os.FileInfo, error) {
485479
lStater, ok := p.Fs().(afero.Lstater)
486480
if !ok {
@@ -493,22 +487,14 @@ func (p *Path) Lstat() (os.FileInfo, error) {
493487
return stat, err
494488
}
495489

496-
// *********************************
497-
// * filesystem-specific functions *
498-
// *********************************
499-
500490
// SymlinkStr symlinks to the target location. This will fail if the underlying
501491
// afero filesystem does not implement afero.Linker.
502-
//
503-
// THIS METHOD IS NOT TYPE SAFE.
504492
func (p *Path) SymlinkStr(target string) error {
505493
return p.Symlink(NewPathAfero(target, p.Fs()))
506494
}
507495

508496
// Symlink symlinks to the target location. This will fail if the underlying
509497
// afero filesystem does not implement afero.Linker.
510-
//
511-
// THIS METHOD IS NOT TYPE SAFE.
512498
func (p *Path) Symlink(target *Path) error {
513499
symlinker, ok := p.fs.(afero.Linker)
514500
if !ok {
@@ -518,10 +504,6 @@ func (p *Path) Symlink(target *Path) error {
518504
return symlinker.SymlinkIfPossible(target.path, p.path)
519505
}
520506

521-
// ****************************************
522-
// * chigopher/pathlib-specific functions *
523-
// ****************************************
524-
525507
// String returns the string representation of the path
526508
func (p *Path) String() string {
527509
return p.path

path_test.go

+1-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"errors"
55
"fmt"
66
"io"
7-
"io/ioutil"
87
"os"
98
"path/filepath"
109
"reflect"
@@ -24,12 +23,7 @@ type PathSuite struct {
2423
}
2524

2625
func (p *PathSuite) SetupTest() {
27-
// We actually can't use the MemMapFs because some of the tests
28-
// are testing symlink behavior. We might want to split these
29-
// tests out to use MemMapFs when possible.
30-
tmpdir, err := ioutil.TempDir("", "")
31-
require.NoError(p.T(), err)
32-
p.tmpdir = NewPath(tmpdir)
26+
p.tmpdir = NewPath(p.T().TempDir())
3327
}
3428

3529
func (p *PathSuite) TeardownTest() {

walk.go

+78-32
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"os"
7+
"slices"
78
)
89

910
// WalkOpts is the struct that defines how a walk should be performed
@@ -18,8 +19,7 @@ type WalkOpts struct {
1819

1920
// FollowSymlinks defines whether symlinks should be dereferenced or not. If True,
2021
// the symlink itself will never be returned to WalkFunc, but rather whatever it
21-
// points to. Warning!!! You are exposing yourself to substantial risk by setting this
22-
// to True. Here be dragons!
22+
// points to.
2323
FollowSymlinks bool
2424

2525
// MinimumFileSize specifies the minimum size of a file for visitation.
@@ -40,14 +40,9 @@ type WalkOpts struct {
4040
// VisitSymlinks specifies that we should visit symlinks during the walk.
4141
VisitSymlinks bool
4242

43-
// VisitFirst specifies that, in the algorithms where it is appropriate,
44-
// a node's contents should be visited first, before recursing down. If false,
45-
// a node's subdirectories will be recursed first before visiting any of its
46-
// other children.
47-
//
48-
// This option is not appropriate in the Basic algorithm, where ordering is
49-
// explicitly forbidden.
50-
// VisitFirst bool
43+
// SortChildren causes all children of a path to be lexigraphically sorted before
44+
// being sent to the WalkFunc.
45+
SortChildren bool
5146
}
5247

5348
// DefaultWalkOpts returns the default WalkOpts struct used when
@@ -62,6 +57,7 @@ func DefaultWalkOpts() *WalkOpts {
6257
VisitFiles: true,
6358
VisitDirs: true,
6459
VisitSymlinks: true,
60+
SortChildren: false,
6561
}
6662
}
6763

@@ -87,13 +83,17 @@ type Algorithm int
8783
const (
8884
// AlgorithmBasic is a walk algorithm. It iterates over filesystem objects in the
8985
// order in which they are returned by the operating system. It guarantees no
90-
// ordering of any kind. This is the most efficient algorithm and should be used
91-
// in all cases where ordering does not matter.
86+
// ordering of any kind. It will recurse into subdirectories as soon as it encounters them,
87+
// and will continue iterating the remaining children after the recursion is complete.
88+
// It behaves as a quasi-DFS algorithm.
9289
AlgorithmBasic Algorithm = iota
93-
// AlgorithmDepthFirst is a walk algorithm. It iterates over a filesystem tree
94-
// by first recursing as far down as it can in one path. Each directory is visited
95-
// only after all of its children directories have been recursed.
90+
// AlgorithmDepthFirst is a walk algorithm. More specifically, it is a post-order
91+
// depth first search whereby subdirectories are recursed into before
92+
// visiting the children of the current directory.
9693
AlgorithmDepthFirst
94+
// AlgorithmPreOrderDepthFirst is a walk algorithm. It visits all of a node's children
95+
// before recursing into the subdirectories.
96+
AlgorithmPreOrderDepthFirst
9797
)
9898

9999
// Walk is an object that handles walking through a directory tree
@@ -152,6 +152,12 @@ func WalkVisitSymlinks(value bool) WalkOptsFunc {
152152
}
153153
}
154154

155+
func WalkSortChildren(value bool) WalkOptsFunc {
156+
return func(config *WalkOpts) {
157+
config.SortChildren = value
158+
}
159+
}
160+
155161
// NewWalk returns a new Walk struct with default values applied
156162
func NewWalk(root *Path, opts ...WalkOptsFunc) (*Walk, error) {
157163
config := DefaultWalkOpts()
@@ -242,6 +248,17 @@ func (w *Walk) iterateImmediateChildren(root *Path, algorithmFunction WalkFunc)
242248
return err
243249
}
244250

251+
if w.Opts.SortChildren {
252+
slices.SortFunc[[]*Path, *Path](children, func(a *Path, b *Path) int {
253+
if a.String() < b.String() {
254+
return -1
255+
}
256+
if a.String() == b.String() {
257+
return 0
258+
}
259+
return 1
260+
})
261+
}
245262
var info os.FileInfo
246263
for _, child := range children {
247264
if child.String() == root.String() {
@@ -321,30 +338,59 @@ func (w *Walk) walkBasic(walkFn WalkFunc, root *Path, currentDepth int) error {
321338
return err
322339
}
323340

324-
// WalkFunc is the function provided to the Walk function for each directory.
325-
type WalkFunc func(path *Path, info os.FileInfo, err error) error
341+
func (w *Walk) walkPreOrderDFS(walkFn WalkFunc, root *Path, currentDepth int) error {
342+
if w.maxDepthReached(currentDepth) {
343+
return nil
344+
}
345+
dirs := []*Path{}
346+
err := w.iterateImmediateChildren(root, func(child *Path, info os.FileInfo, encounteredErr error) error {
347+
if IsDir(info.Mode()) {
348+
dirs = append(dirs, child)
349+
}
326350

327-
// Walk walks the directory using the algorithm specified in the configuration.
328-
func (w *Walk) Walk(walkFn WalkFunc) error {
351+
passesQuery, err := w.passesQuerySpecification(info)
352+
if err != nil {
353+
return err
354+
}
329355

330-
switch w.Opts.Algorithm {
331-
case AlgorithmBasic:
332-
if err := w.walkBasic(walkFn, w.root, 0); err != nil {
333-
if errors.Is(err, ErrStopWalk) {
334-
return nil
356+
if passesQuery {
357+
if err := walkFn(child, info, encounteredErr); err != nil {
358+
return err
335359
}
336-
return err
337360
}
338361
return nil
339-
case AlgorithmDepthFirst:
340-
if err := w.walkDFS(walkFn, w.root, 0); err != nil {
341-
if errors.Is(err, ErrStopWalk) {
342-
return nil
343-
}
362+
})
363+
if err != nil {
364+
return err
365+
}
366+
for _, dir := range dirs {
367+
if err := w.walkPreOrderDFS(walkFn, dir, currentDepth+1); err != nil {
344368
return err
345369
}
346-
return nil
347-
default:
370+
}
371+
return nil
372+
}
373+
374+
// WalkFunc is the function provided to the Walk function for each directory.
375+
type WalkFunc func(path *Path, info os.FileInfo, err error) error
376+
377+
// Walk walks the directory using the algorithm specified in the configuration.
378+
func (w *Walk) Walk(walkFn WalkFunc) error {
379+
funcs := map[Algorithm]func(walkFn WalkFunc, root *Path, currentDepth int) error{
380+
AlgorithmBasic: w.walkBasic,
381+
AlgorithmDepthFirst: w.walkDFS,
382+
AlgorithmPreOrderDepthFirst: w.walkPreOrderDFS,
383+
}
384+
algoFunc, ok := funcs[w.Opts.Algorithm]
385+
if !ok {
348386
return ErrInvalidAlgorithm
349387
}
388+
if err := algoFunc(walkFn, w.root, 0); err != nil {
389+
if errors.Is(err, ErrStopWalk) {
390+
return nil
391+
}
392+
return err
393+
}
394+
return nil
395+
350396
}

walk_test.go

+37-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ package pathlib
22

33
import (
44
"fmt"
5-
"io/ioutil"
5+
os "os"
66
"reflect"
77
"testing"
88

99
"github.com/spf13/afero"
10+
"github.com/stretchr/testify/assert"
1011
"github.com/stretchr/testify/mock"
1112
"github.com/stretchr/testify/require"
1213
"github.com/stretchr/testify/suite"
@@ -26,7 +27,7 @@ type WalkSuiteAll struct {
2627
func (w *WalkSuiteAll) SetupTest() {
2728
var err error
2829

29-
tmpdir, err := ioutil.TempDir("", "")
30+
tmpdir, err := os.MkdirTemp("", "")
3031
require.NoError(w.T(), err)
3132

3233
w.Fs = afero.NewOsFs()
@@ -294,3 +295,37 @@ func TestNewWalk(t *testing.T) {
294295
})
295296
}
296297
}
298+
299+
func TestWalkPreOrderDFS(t *testing.T) {
300+
root := NewPath(t.TempDir())
301+
children := []string{
302+
"1.txt",
303+
"2.txt",
304+
"3.txt",
305+
"subdir/4.txt",
306+
"subdir/5.txt",
307+
}
308+
for _, child := range children {
309+
c := root.Join(child)
310+
require.NoError(t, c.Parent().MkdirAll())
311+
require.NoError(t, c.WriteFile([]byte("hello")))
312+
313+
}
314+
walker, err := NewWalk(
315+
root,
316+
WalkAlgorithm(AlgorithmPreOrderDepthFirst),
317+
WalkSortChildren(true),
318+
WalkVisitDirs(false),
319+
)
320+
require.NoError(t, err)
321+
seenChildren := []string{}
322+
err = walker.Walk(func(path *Path, info os.FileInfo, err error) error {
323+
require.NoError(t, err)
324+
relative, err := path.RelativeTo(root)
325+
require.NoError(t, err)
326+
seenChildren = append(seenChildren, relative.String())
327+
return nil
328+
})
329+
require.NoError(t, err)
330+
assert.Equal(t, children, seenChildren)
331+
}

0 commit comments

Comments
 (0)