find

package module
v0.0.0-...-d884f04 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 1 Imported by: 0

README

go-ruby-find/find

find — go-ruby-find

License Go Coverage

A pure-Go (no cgo) reimplementation of the traversal algorithm of Ruby's Find module (require "find") — the deterministic, interpreter-independent core of MRI 4.0.5's lib/find.rb. It drives Find.find's exact top-down visit order and the Find.prune control flow over an injected directory lister, so the real filesystem access (Dir.children, File.lstat/File.directory?) stays where it belongs — host-side — while the order, the sort, the prune throw/catch and the error pass-through behaviour live here as portable Go.

It is the Find backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-regexp (the Onigmo engine), go-ruby-erb (the ERB compiler) and go-ruby-yaml (the Psych port).

What it is — and isn't. Reproducing Find.find's order — the depth-first walk with byte-wise-sorted children, the prune semantics, the missing-start-path error — is fully deterministic and needs no interpreter, so it lives here as pure Go. Touching the filesystem — opening a directory, stat-ing a path — is the host's job; this library asks for it through a small Lister interface that the host (rbgo) binds to Dir.children and File.lstat.

MRI faithfulness

Validated against the ruby binary on every non-Windows CI lane (the *_oracle* differential tests build a real temp tree and diff our order against ruby -rfind). The algorithm mirrors MRI 4.0.5's lib/find.rb exactly:

  • Order. Each start path is yielded first, then a depth-first walk of its contents. A directory's children are listed, sorted ascending byte-wise (so Capital.txt precedes a, and 9 precedes letters — MRI's String#<=>), reversed, and unshifted onto a FIFO queue, giving depth-first ascending order.
  • Find.prune. Returning find.ErrPrune from the yield callback prunes the current path: it has already been yielded, but if it is a directory it is not descended into — the engine's analogue of throw :prune.
  • Errors. A missing start path makes Walk return a *MissingPathError before anything is yielded (MRI raises Errno::ENOENT). A per-entry IsDir/ Children failure reached mid-walk is swallowed (entry skipped) when ignoreError is true — MRI's default — and propagated otherwise.

The Lister seam

Walk performs no I/O itself; the host injects all filesystem access:

type Lister interface {
    Exist(path string) bool                       // Ruby File.exist?  (start paths only)
    IsDir(path string) (isDir bool, err error)    // Ruby File.lstat(path).directory?
    Children(dir string) (entries []string, err error) // Ruby Dir.children (base names, unsorted ok)
}

func Walk(roots []string, l Lister, yield func(path string) error, ignoreError bool) error

rbgo supplies a Lister backed by its own Dir/File objects; the engine sorts the children itself, so the host need not. WalkJoin accepts a custom path joiner for hosts whose File.join differs from the default single-/ rule.

err := find.Walk([]string{"a", "b"}, hostLister, func(p string) error {
    if shouldSkipSubtree(p) {
        return find.ErrPrune // == Find.prune
    }
    fmt.Println(p)
    return nil
}, true)

Tests & coverage

go test ./...100% statement coverage, enforced in CI. The coverage is reached entirely by the deterministic, ruby-free tests (an in-memory Lister, no real filesystem), so the Windows and qemu lanes pass the gate without a Ruby runtime; the MRI oracle runs additionally on the ubuntu/macos lanes. Builds and tests on all six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) across Linux, macOS and Windows. CGO is not used.

License

BSD-3-Clause — see LICENSE. Copyright (c) 2026, the go-ruby-find/find authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package find is a pure-Go (CGO=0) reimplementation of the recursive-traversal algorithm of Ruby's standard library Find module (require "find"), faithful to MRI 4.0.5's lib/find.rb.

It implements only the deterministic, interpreter-independent core: the top-down walk over a set of start paths, MRI's exact visit order, and the Find.prune control flow. The real filesystem access — Dir.children, the File.exist?/File.lstat directory test — is NOT done here; it is injected by the host (rbgo supplies Dir.children + File.directory?/lstat). The engine drives the depth-first search, the sort/queue ordering, the prune throw/catch semantics and the per-path error pass-through exactly as MRI does, so that binding it to a real filesystem reproduces Find.find observable behaviour.

MRI algorithm

MRI's Find.find(*paths) first checks every start path with File.exist? and raises Errno::ENOENT for the first one missing. It then processes each start path through a FIFO queue: it shifts the front path, yields it, lstat's it, and if it is a directory lists its children, sorts them ascending (byte-wise), and unshifts them — in reverse order — onto the front of the queue. Because the children are reversed before being pushed to the front, shifting visits them in ascending order, descending into each subdirectory before its later siblings: a depth-first, ascending-sorted walk. Find.prune aborts the current path's catch(:prune) block, so a directory chosen for pruning is yielded but not descended into.

Per-path errors (lstat or Dir.children failing on an entry reached mid-walk) are swallowed when ignoreError is true (MRI's default), skipping that entry; when false they propagate. A missing START path always raises, regardless of ignoreError.

Index

Constants

This section is empty.

Variables

View Source
var ErrPrune error = pruneSignal{}

ErrPrune, when returned by the yield callback, prunes the current path: the path has already been yielded, but if it is a directory it is not descended into. It is the engine's equivalent of calling Find.prune inside the block. (The host binds Find.prune to returning this from the callback.) Returning ErrPrune is the ONLY in-band control value; any other non-nil error from yield stops the walk and is returned to the caller.

Functions

func Join

func Join(dir, name string) string

Join joins a directory and a child base name the way Ruby's File.join does for this one-segment case: a single "/" separator, but without doubling a slash when dir already ends in one (File.join("a/", "b") == "a/b"). The host may supply its own joiner via WalkJoin; this is the default used by Walk.

func Walk

func Walk(roots []string, lister Lister, yield func(path string) error, ignoreError bool) error

Walk performs MRI Find.find's traversal over roots, driving the injected lister and invoking yield for every visited path in MRI's exact order. It is the algorithmic core the host binds Find.find to.

Order: each root is processed in turn; within a root, the root is yielded first, then a depth-first walk of its contents with children visited in ascending (byte-wise) order.

Control flow: if yield returns ErrPrune, the current path is not descended into (it was already yielded) and the walk continues with the next queued path. If yield returns any other non-nil error, Walk stops immediately and returns it.

Errors: every root is first checked with lister.Exist; the first missing root makes Walk return a *MissingPathError without yielding anything (MRI raises Errno::ENOENT). Once walking, an IsDir or Children error on an entry is swallowed (the entry is skipped) when ignoreError is true, and returned otherwise. A start path is never subject to ignoreError for its existence check, but an IsDir/Children failure on a start path while walking follows the ignoreError policy, as in MRI.

func WalkJoin

func WalkJoin(roots []string, lister Lister, yield func(path string) error, ignoreError bool, join func(dir, name string) string) error

WalkJoin is Walk with a caller-supplied path joiner, letting the host reproduce a non-default File.join (e.g. an alternate separator). join must combine a directory with a single child base name.

Types

type Lister

type Lister interface {
	// Exist reports whether path exists. Called only for the start paths.
	Exist(path string) bool
	// IsDir reports whether path is a directory whose children should be walked.
	// Mirrors MRI's File.lstat(path).directory? — symlinks are NOT followed, so a
	// symlink to a directory must report false. err propagates the lstat failure
	// (e.g. a path that vanished mid-walk); IsDir is consulted for every yielded
	// path, including the start paths.
	IsDir(path string) (isDir bool, err error)
	// Children returns the entries of dir (Ruby Dir.children): base names only, no
	// "." / "..", in any order. Called only when IsDir(dir) was true.
	Children(dir string) (entries []string, err error)
}

Lister is the injected filesystem seam. The host (rbgo) supplies it, mapping onto Ruby's File/Dir operations:

  • Exist reports whether a START path exists (Ruby File.exist?). A start path for which Exist returns false makes Walk fail before any yield, mirroring MRI raising Errno::ENOENT.
  • Children lists the entries of dir, WITHOUT the "." and ".." pseudo-entries and WITHOUT the dir prefix (Ruby Dir.children). isDir reports whether the path is a directory to descend into (Ruby File.lstat(path).directory?). The engine never calls Children unless isDir(path) returned true. Children/isDir may return an error for an entry reached mid-walk (e.g. EACCES, ENOENT after a race); Walk applies the ignoreError policy to it.

Children returns entries in any order; Walk sorts them itself to match MRI, so the host need not sort. The returned slice is owned by Walk after the call.

type MissingPathError

type MissingPathError struct {
	// Path is the start path that did not exist.
	Path string
}

MissingPathError reports that a start path passed to Walk does not exist. It mirrors MRI raising Errno::ENOENT for a missing top-level path; the host maps it back to Errno::ENOENT.

func (*MissingPathError) Error

func (e *MissingPathError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL