cull / pup alternative
pup pioneered
"CSS selectors in the pipe" and earned its 8k+ stars — but its last release,
v0.4.0, shipped in 2016, over a hundred issues sit open, and its custom
display-filter syntax (text{}, attr{},
json{}, slice{}) stops short of the structured
output most scraping pipelines actually need.
cull is an actively maintained alternative written in Rust: standard CSS selectors (the same html5ever/selectors engine as Firefox/Servo), ordinary flags instead of display filters, and output modes that go straight to JSON, CSV, or Markdown.
| pup | cull |
|---|---|
pup 'a' | cull 'a' |
pup 'a text{}' | cull 'a' -t |
pup 'a attr{href}' | cull 'a' -a href |
pup 'a json{}' | cull 'a' --json-nodes — or -j '{…}' to name the fields you want |
pup -f page.html 'a' | cull 'a' page.html |
pup --color 'div' | cull 'div' — color is automatic on a TTY (--color always|never to force) |
| pup's always-indented output | cull -p 'div' — indentation is opt-in; default is verbatim HTML |
pup 'div slice{0,1}' | cull 'div' -1, or CSS :nth-child() / :nth-of-type() |
pup 'div:has(p)' → “not a valid pseudo class” (pup#194) | cull 'div:has(p)' — :has(), :is(), :where() all work |
The big difference: pup's json{}
dumps the entire node structure and leaves you a jq pass to dig
the fields out. cull gives you both ends: --json-nodes is the
full dump (with attributes safely in their own attrs object and
collapsed text on every node), and -j takes a
jq-style template that emits exactly the object you asked for —
usually no jq pass at all.
# Shaped JSON — one clean NDJSON object per match, keys in your order $ cull '.post' -j '{title: h2, url: a @href, score: .pts | num, tags: [.tag]}' blog.html {"title":"Hello, world","url":"/posts/hello","score":128,"tags":["rust","intro"]}
# Any <table> to CSV or NDJSON — colspan/rowspan expanded, multi-row headers merged $ cull --table 'https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes' > codes.csv
# Page to Markdown (strip the chrome first with -r) $ cull article --md -r 'nav, footer, script, style' https://example.com/post
# Interactive selector mode — live preview TUI, Tab cycles output shape $ cull -I https://example.com
# Fetch URLs directly; relative hrefs resolve against the page URL $ cull a -a href https://example.com
# Multiple files & globs, grep-style -c / -l / exit codes $ cull 'meta[name=generator]' -l snapshots/*.html
cull also decodes non-UTF-8 pages (Shift_JIS, KOI8-R, windows-125x …)
browser-style — BOM, Content-Type header, then
<meta charset> — and removes nodes before selection with
-r, which pup can't do at all.
Behavior differences to know: pup prints each
text node on its own line in text{}; cull collapses whitespace
to one line per match. pup always re-indents HTML — which inserts whitespace that
can change what a browser renders (<b>a</b><i>b</i>, "ab",
re-renders as "a b") and drops the <!DOCTYPE> in whole-document
output; cull emits HTML verbatim unless you pass -p, its -p is
rendering-faithful (inline content stays on one line), and whole-document output
keeps the DOCTYPE (and -p never reformats inside
pre, textarea, script, or
style). cull exits 1 when nothing matched,
grep-style.
# Homebrew $ brew install rashida-thorne/cull/cull # or: curl | sh, scoop, nix, cargo binstall, cargo install — see the site
All options, prebuilt static binaries (Linux x86_64/arm64, macOS, Windows), and a demo GIF on the cull homepage. Benchmarked 1.2–1.6× faster than pup on typical pages, with a reproducible script.
Something in your pup workflow missing from the table above? Open an issue — migration gaps are treated as bugs.