CLI tool
tavianator/bfs avatar
tavianator/bfs

bfs, a find that walks the filesystem shallow first

A breadth-first version of the UNIX find command

1,284 stars44 forksC0BSD

At a glance

What is it?
bfs is a drop-in variant of the UNIX find command that traverses breadth-first instead of depth-first, so a shallow file is reported before an unrelated deep subtree is exhausted. It keeps compatibility with POSIX, GNU and the BSDs, and it adds argument flexibility, typo suggestions and static checks that catch a wasted expression.
Who is it for?
bfs suits anyone who runs find on large trees and has ever waited for a deep subtree to be explored before seeing a file two levels down, and it suits scripts that would benefit from stricter argument handling. It is the wrong choice if your script depends on the exact order find produces, since breadth-first is a different order and anything that parses find's output positionally will break.
Can I use it commercially?
Yes. 0BSD is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 9 days ago.
What is it written in?
Mainly C, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why breadth-first changes what you see first

bfs is a variant of the UNIX find command that operates breadth-first rather than depth-first, and it is otherwise compatible with many versions of find, including POSIX, GNU, FreeBSD, OpenBSD, NetBSD and macOS.

The difference is visible in a small example, and the README uses a haystack with a deep chain and a shallow needle:

console
$ bfs haystack
haystack
haystack/deep
haystack/shallow
haystack/deep/1
haystack/shallow/needle
...

against find's output, which walks the entire deep directory tree before it ever gets to the shallow one that contains what you are looking for. bfs lists files from shallowest to deepest, so you never have to wait for it to explore an entire unrelated subtree.

That is the entire thesis of the project, and it is a real one. The classic failure mode of find on a large tree is that the file you wanted was in the first directory and find spent a minute in a node_modules tree on the other branch. Breadth-first fixes exactly that case, and it is a case that happens constantly in build directories, home directories and source trees.

The compatibility claim is the other half, and it is what makes the tool adoptable. The README links the POSIX specification for find, the GNU findutils documentation and the man pages for FreeBSD, OpenBSD, NetBSD and macOS, which is a list of the behaviours a reimplementation has to match to be a drop-in.

The project is written in C by Tavian Barnes, the licence is the 0BSD, the homepage is tavianator.com/projects/bfs.html, and the release tags are 4.1.4 from 2026-06-29, 4.1.3 from 2026-06-16 and 4.1.2 from 2026-05-06, with the last push on 2026-10-13.

Argument order, and the errors you would have got from find

The second feature the README describes is that bfs tries to be easier to use than find while remaining compatible, and the example is about where you put your arguments.

find requires paths to come before the expression, and treats anything after a path as part of the expression. bfs does not:

console
$ bfs -L -name 'needle' haystack
haystack/needle

$ bfs haystack -L -name 'needle'
haystack/needle

$ bfs -L haystack -name 'needle'
haystack/needle

The same three commands against find produce two errors and one success:

console
$ find -L -name 'needle' haystack
find: paths must precede expression: haystack

$ find haystack -L -name 'needle'
find: unknown predicate `-L'

This is the kind of friction that a shell user hits constantly and absorbs without noticing, because the error messages are informative enough to fix but not obvious enough to predict. bfs simply accepts the arguments in any order and works out for itself which are paths and which are predicates.

That is a design decision with a cost, and it is worth being clear about it: an argument that looks like a path but is meant as a predicate is ambiguous, and bfs has to guess. The compatibility suite is where that guessing gets tested.

The third feature is about errors and warnings, and this is the part that is genuinely different from find rather than merely more permissive. bfs detects and suggests corrections for typos:

console
$ bfs -nam needle
bfs: error: bfs -nam needle
bfs: error:     ~~~~
bfs: error: Unknown argument; did you mean -name?

And it includes a static analysis that catches a mistake find will happily make, an expression whose result is thrown away:

console
$ bfs -print -name 'needle'
bfs: warning: bfs -print -name needle
bfs: warning:            ~~~~~~~~~~~~
bfs: warning: The result of this expression is ignored.

The warning points at the specific expression, so you can see which part of the command is being ignored. That is a real quality of life improvement over a tool that either does the wrong thing or refuses to run.

The error format is worth noting on its own. The caret line under the offending argument, and the message on the following line, is a style that treats the user as someone who wants to fix the command rather than someone who should be told they were wrong.

Extensions that remove the expressions people write out of habit

The last of the headline features is that bfs adds some options that make common tasks easier, and the two that stand out are the ones that replace an expression most people copy from a forum post.

The first is -exclude, which skips over entire subtrees whenever an expression matches:

console
$ bfs -name config -exclude -name .git

The README says this is both more powerful and easier to use than the standard -prune action, and compares it to the equivalent find expression, which is a negated group with a prune inside it.

That comparison matters. The -prune idiom is one of the least readable things in the standard find vocabulary, because you have to write a negation to express a positive intent, and the negation has to come before the thing it negates. -exclude takes the expression in the order you would think about it, name the files you want and then name the directories you do not want to descend into.

The second extension is -nohidden, which skips over all hidden files and directories. That is a convenience rather than a necessity, but it is the kind of option that saves a predicate from being repeated in every invocation, and hidden directory exclusion is a common enough requirement that having it built in is reasonable.

The README points at the usage documentation, docs/USAGE.md, under the extensions section, for more about what bfs provides. The docs directory in the repository also holds BUILDING.md and CONTRIBUTING.md, and the top level of the repository shows a completions/ directory for shell completions, a bench/ directory, a build/ directory with the makefile fragments, a src/ directory and a tests/ directory.

The practical way to evaluate bfs is to run the same command under both and compare, because the compatibility claim is the whole point and the differences, where they exist, are in the corners of the find grammar rather than in the middle.

Installing it, and the build system that grew a configure step

bfs is packaged for essentially every Unix, and the README lists the commands per platform. On Alpine it is apk add bfs, on Arch pacman -S bfs, on Debian and Ubuntu apt install bfs, on Fedora dnf install bfs, on Gentoo emerge sys-apps/bfs, on GNU Guix guix install bfs, on NixOS nix-env -i bfs and on Void xbps-install -S bfs. On macOS there is Homebrew with brew install bfs and MacPorts with port install bfs, and the BSDs have pkg install bfs.

For macOS users, the command is:

bash
brew install bfs

The repository's own build is a two-step configure and make, and the Makefile says so at the top:

bash
$ ./configure
$ make

The configure step is new enough that the Makefile keeps a compatibility shim for the old way of doing things. bfs used to have flag-like targets such as make release, make asan ubsan and others, and those now print an error telling you to use ./configure --enable-<name> instead. The Makefile also guards against being run before configure, since the generated config.mk holds the configuration and a build without it would silently use the wrong settings.

The build itself produces a binary and a static library, and the all target lists the full set of outputs. The main binary is bin/bfs, the library is lib/libbfs.a, and there are several test helpers, bin/tests/mksock, ptyx, units, xspawnee and xtouch, plus a benchmark tool bin/bench/ioq. The source is split into a fine-grained list of objects, with src/alloc.o, src/eval.o, src/expr.o, src/trie.o and src/typo.o among them, and the trie and typo objects are the ones that implement the argument suggestions and the static analysis described earlier.

The licence is notable for a C project. The SPDX identifier is 0BSD, which is a public domain dedication with no warranty clause, and the CI and coverage badges show the project runs a GitHub Actions workflow and tracks coverage with codecov.

Editorial conclusion

bfs suits anyone who runs find on large trees and has ever waited for a deep subtree to be explored before seeing a file two levels down, and it suits scripts that would benefit from stricter argument handling. It is the wrong choice if your script depends on the exact order find produces, since breadth-first is a different order and anything that parses find's output positionally will break. Before switching, check the usage documentation at docs/USAGE.md for the extensions bfs adds, and note that the tool is not a fork of GNU find but a compatible reimplementation, so the exact behaviour of an unusual predicate is worth testing rather than assuming.

Frequently asked questions

What is bfs and how does it differ from find?

bfs is a variant of the UNIX find command that operates breadth-first rather than depth-first, so it reports files from shallowest to deepest and finds a shallow file before exploring an entire unrelated deep subtree. It is otherwise compatible with many versions of find, including POSIX, GNU, FreeBSD, OpenBSD, NetBSD and macOS.

Does bfs accept arguments in a different order than find?

Yes. bfs is less picky about where you put its arguments, so bfs -L -name 'needle' haystack, bfs haystack -L -name 'needle' and bfs -L haystack -name 'needle' all work, where find rejects two of those three with paths must precede expression and unknown predicate errors.

What does the -exclude option do?

It skips over entire subtrees whenever an expression matches, so bfs -name config -exclude -name .git expresses the intent directly. The README describes it as both more powerful and easier to use than the standard -prune action, which requires a negated group to express the same thing.

How do I build bfs from source?

Run ./configure and then make. The configure step is required, and the Makefile prints an error if make is run before it, and the old flag-like targets such as make release or make asan ubsan now tell you to use ./configure --enable-<name> instead.

Does bfs catch mistakes that find silently allows?

Yes. bfs suggests corrections for unknown arguments, for example reporting Unknown argument; did you mean -name? for a typo, and it includes a static analysis that warns when an expression's result is ignored, such as bfs -print -name 'needle', with a warning pointing at the specific expression.

Official sources

  1. License: 0BSD
  2. Project website
  3. README
  4. Releases
  5. tavianator/bfs on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tavianator-bfs.svg)](https://hysenlabs.com/projects/tavianator-bfs)

Was this useful?