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, theprunesemantics, 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 smallListerinterface that the host (rbgo) binds toDir.childrenandFile.lstat.
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.txtprecedesa, and9precedes letters — MRI'sString#<=>), reversed, and unshifted onto a FIFO queue, giving depth-first ascending order. -
Find.prune. Returningfind.ErrPrunefrom 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 ofthrow :prune. -
Errors. A missing start path makes
Walkreturn a*MissingPathErrorbefore anything is yielded (MRI raisesErrno::ENOENT). A per-entryIsDir/Childrenfailure reached mid-walk is swallowed (entry skipped) whenignoreErroris true — MRI's default — and propagated otherwise.
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) errorrbgo 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)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.
BSD-3-Clause — see LICENSE. Copyright (c) 2026, the go-ruby-find/find authors.
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, …)