cull / pup alternative

Migrating from pup to cull

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.

Migration table

pupcull
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 outputcull -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.

What pup has no equivalent for

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

Install cull

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