---
title: 'Label Filter'
url: 'https://code-evolve.com/resources/label-filter'
markdown: 'https://code-evolve.com/resources/label-filter.md'
date: '2026-10-13'
description: 'label-filter A small pattern language for filtering lists of objects by their labels. One of the Code-Evolve utilities, small packages that each do a single job. Status Implementation Package Conformance Published TypeScript label-filter (npm) passes yes Rust label-filter (crates.io) passe…'
---

# label-filter

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

One of the [Code-Evolve utilities](https://www.code-evolve.com/products/utilities#label-filter), small packages that each do a single job.

# Status

| Implementation | Package | Conformance | Published |
|---|---|---|---|
| TypeScript | `label-filter` (npm) | passes | yes |
| 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 | `code-evolve/label-filter` (Packagist) | passes | not yet |

Every row above was established by running that implementation's suite against the shared fixture on **2026-10-13**, 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 · The other three registries, in their own time

# 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 | `code-evolve/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

```js
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 |
| `apple&pear` | both, in either order |
| `apple&\-\test` | include, except: contains `apple` and not `test` |
| `[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 |

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

**`&` binds looser than `|`**, so `a|b&c` is *(a OR b) AND c*. That is the opposite of the C family, and it is the one rule in this language a reader's prior knowledge argues against. `||a|b||&c` says the same thing with the grouping shown.

# Sets, groups and options

```text
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&b&c            & separates items, and every item must match, in any order
`src&md`         an item carries its own anchors: starts with src, ends with md
a&\-\test        include, except: the exclusion is one item of two
\c\App&\c-\Test  options do not span items, so to harden both, write both
```

**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.

**`\-\` inverts its item**: `-\a|b` is NOT (a OR b), while `apple&\-\test` is *apple, but not test*. An item is a whole pattern, which is why include-except needs no machinery of its own.

**Options do not span items.** `\c\App&\-\Test` is a case-sensitive `App` beside a case-**in**sensitive `Test`, so it hides `App test` as well as `App Test`. The repetition in `\c\App&\c-\Test` is the price of an item being whole, and it is visible rather than silent.

# How the implementations stay one language

One fixture holds the whole contract: **96 cases** lifted from numbered sections of the specification, **45 patterns that must be refused** (with the refusal then obliged to match every label) and **55 fragments of junk** that must not crash anything. 568 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.

# Licensing

The implementations are **MIT**. The specification text, `docs/syntax.md`, which ships inside the package, is **CC BY 4.0**. Copyright 2026 Steven Spungin.

Label Filter is a **Code-Evolve** product: the brand names the product line and the repository, `github.com/code-evolve/label-filter`, while the copyright names the author who holds it. They are independent, and a package may carry one without the other.

# 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.

---

## Navigation

- Previous: [Projects](https://code-evolve.com/resources/projects.md)
- Next: [to-caption](https://code-evolve.com/resources/to-caption.md)
