label-filter

A small pattern language for filtering lists of objects by their labels.

Visit Label Filter for more information.

Status

Implementation Package Conformance Published
TypeScript label-filter (npm) passes not yet
Rust label-filter (crates.io) passes not yet
Python label-filter (PyPI) passes not yet
Go module github.com/code-evolve/label-filter/go passes yes
PHP twelvetone/label-filter (Packagist) passes not yet

Every row above was established by running that implementation's suite against the shared fixture on 2026-09-23, as part of building this page. It is a report rather than a claim.

Next: Java port · The ports nobody has asked for yet: C#, Ruby, Swift, Kotlin

Implementations

Five, and they are the same language rather than five similar ones.

Language Package Requires
TypeScript label-filter (npm) ESM, Node 18+ or any bundler
Rust label-filter (crates.io) Rust 1.70+
Python label-filter (PyPI) Python 3.9+
Go module github.com/code-evolve/label-filter/go Go 1.12+
PHP twelvetone/label-filter (Packagist) PHP 7.4+

Some are not published yet. The names above are the intended ones; until a release happens the package is consumed locally, and a link here would be a 404 on the page whose job is to send you to it.

Every implementation has no runtime dependencies, and no dependencies in its test runner either.

Usage

import { compileLabelFilter } from 'label-filter'

const match = compileLabelFilter('invoice*|receipt*')
files.filter(match)

compileLabelFilter(pattern) returns match(label) => boolean. The other implementations follow the same shape under their own naming conventions — label_filter.compile_label_filter in Python, labelfilter.Compile in Go, label_filter::compile in Rust, LabelFilter::compile in PHP — and each answers the same two questions: why can this not be read, and does this label pass.

What to do when a pattern cannot be read

When the pattern cannot be read, error is a string and the matcher answers true for every label. A filter must never hide a row for a reason it cannot state, and "no results" is an answer a filter gives legitimately all the time — so an error that produced it would be invisible. This holds under \-\ as well: negation never inverts a refusal.

That gives a caller two different obligations, and the second is easy to miss:

  • Showing rows: hide nothing. Surface error beside the list. Better still, keep the last good result and show the message — no flicker, no false empty, no false everything. Every pattern passes through unreadable states while it is typed: [0-9] is invalid at [, [0, [0- and [0-9.
  • Acting on rows: do nothing. For anything destructive — delete the matches, move the matches — a matcher carrying an error must not be acted on at all. That is why error sits on the matcher rather than the language throwing or returning an empty set: the caller has to answer what do I do when this cannot be read, and the answer differs for showing and for doing.

The language

Pattern Means
apple contains, case-insensitive — the default
apple*pie * is any run of characters, including none
`apple anchored to the start
apple` anchored to the end
`apple` both — an exact match
apple|pear either alternative
[abc] one character from a set — [0-9], [a-z], [A-Za-z], [0-9A-F]
\c\apple options prefix; c makes literals case-sensitive
\* a backslash escapes a reserved character

Six characters are reserved and nothing else is special: * | ` \ [ ].

Sets, groups and options

foo[123]bar      one character from 123 — ranges too: [0-9], [a-z], [A-Za-z], [0-9A-F]
`[123]bar        beside an anchor, with nothing special required
*.||md|txt||     ||…|| groups alternatives, so a shared part is written once, not once each
`||a|b||`        exactly a, or exactly b — an anchor reaches every branch of the group
\-\test          not — hide every label the pattern finds
\b\app           whole word — app, app-1, my app, app_data; not apple or snapple
\c-b\App         options compose

A set is always case-sensitive, even in the default case-insensitive mode, because a set is the one construct that can say either case explicitly — [A-Za-z] — while nothing else could say this case only. \c\ governs literals.

A backtick is an anchor and nothing else: the first character, the last character, or a mistake.

A word boundary is the edge of the label or a neighbouring character that is not a letter or a number, so _ separates words — which a regular expression's own \b gets wrong for filenames.

\-\ is an exclusion filter: \-\a|b is NOT (a OR b). Apple but not test is deliberately two filters rather than an AND, because precedence would cost the language its first goal.

How the implementations stay one language

One fixture holds the whole contract: 73 cases lifted from numbered sections of the specification, 31 patterns that must be refused — with the refusal then obliged to match every label — and 43 fragments of junk that must not crash anything. 399 assertions, in one file, run by all of them.

A port is a translation of the reference implementation function for function, so the files read side by side; the fixture is what makes that claim checkable rather than aspirational. One command runs every implementation and prints one line each.

The reference implementation carries two suites the ports do not, because they test the language rather than an implementation of it: a property suite that walks every pattern up to a given length over the reserved alphabet and checks thousands of generated ones against an independently written oracle — a regular expression, which is precisely what the implementation refuses to be — and a performance suite with per-label ceilings, because a filter that is correct and slow is still broken.

Design goals

  1. Easy to learn — the default needs no syntax, and the whole language fits in a table.
  2. Easy to type — nothing common requires a modifier key.
  3. Easy to view — short patterns, no escaping thicket, no precedence to remember.
  4. Complete enough for most needs — and explicit about what it leaves out.

They are the tie-breaker for every proposed change, and each one is recorded with what serves it and what strains it, so a future addition argues against the goals rather than against taste.