cull / rss & xml → json
cull parses RSS, Atom, sitemaps, and any other XML as real XML — then lets you select with plain CSS selectors (no XPath) and shape the result into JSON, NDJSON, CSV, or text. It fetches the URL itself. Single small static binary.
Real output from the lobste.rs RSS feed (first lines):
$ cull item -j '{title: title, url: link, date: pubDate}' https://lobste.rs/rss {"title":"Maybe we should revisit microkernels","url":"https://notes.hella.cheap/maybe-we-should-revisit-microkernels.html","date":"Sat, 25 Jul 2026 17:13:07 -0500"} {"title":"A shell colon does nothing. Use it anyway","url":"https://refp.se/articles/your-shell-and-the-magic-colon","date":"Sat, 25 Jul 2026 06:33:00 -0500"}
One JSON object per <item> — NDJSON, ready for
jq, a database, or a cron job. Selectors are case-sensitive in
XML mode, so pubDate just works.
In HTML, <link> is a void element — it can't
have content. So when an HTML parser reads an RSS feed, every
<link>https://…</link> loses its URL: the text gets
re-parented out of the element and the tools return nothing. This is why
pup and htmlq both come back empty when you ask a
feed for its links — not an error, just silently no URLs.
cull auto-detects XML (an <?xml…?> declaration or a
known root: <rss>, <feed>,
<urlset>, <sitemapindex>,
<opml>, <svg>) and switches to a real
XML parser. Force it with --xml, opt out with
--html. Piped input works the same:
curl -s "$FEED" | cull 'item > link' -t.
$ cull entry -j '{title: title, url: link @href, date: updated}' https://blog.rust-lang.org/feed.xml {"title":"Announcing Rust 1.97.1","url":"https://blog.rust-lang.org/2026/07/16/Rust-1.97.1/","date":"2026-07-16T00:00:00+00:00"} {"title":"crates.io: development update","url":"https://blog.rust-lang.org/2026/07/13/crates-io-development-update/","date":"2026-07-13T00:00:00+00:00"}
@href reads an attribute instead of text — same template
syntax as cull's HTML mode.
media:thumbnail & friendsEscape the colon in the selector:
$ cull item -j '{title: title, thumb: media\:thumbnail @url}' https://feeds.bbci.co.uk/news/rss.xml {"title":"Firefighters battle wildfire as tens of thousands evacuated near Bordeaux","thumb":"https://ichef.bbci.co.uk/ace/standard/240/cpsprodpb/7731/live/…jpg"}
# every URL in a sitemap (for a <sitemapindex>, select 'sitemap > loc') $ cull 'url > loc' -t https://blog.rust-lang.org/sitemap.xml https://blog.rust-lang.org/ https://blog.rust-lang.org/2014/09/15/Rust-1.0/ … # how many items is this feed carrying? $ cull item -c https://feeds.bbci.co.uk/news/rss.xml 34 # only items that have a <category>, with their tags as a JSON array $ curl -s https://lobste.rs/rss | cull 'item:has(category)' -j '{title: title, tags: [category]}' {"title":"Maybe we should revisit microkernels","tags":["osdev"]}
Modern selectors like :has(), :not(), and
:is() work in XML mode too.
$ cull 'item > link' -t "$FEED" | diff - seen.txt | grep '^<' # what's new? $ cull 'item > link' -t "$FEED" > seen.txt # remember for next run
More patterns (per-feed JSON archives, multi-feed batches) are in the cookbook.
| xmllint | xmlstarlet | cull | |
|---|---|---|---|
| Query language | XPath 1.0 | XPath 1.0 | CSS selectors (incl. :has()) |
| Shaped JSON output | — | — (text templates) | -j '{…}', nested, NDJSON |
| Namespaces without declaring them | — (local-name() tricks) | — (-N or _:) | media\:thumbnail just matches |
| Fetches HTTPS itself | — (http only, no TLS) | — (http only) | ✓ (-H headers, --timeout) |
| Also parses real-world HTML | lenient mode ≠ browser parser | — | ✓ (browser-grade html5ever) |
| CSV / Markdown output | — | CSV via templates | --table, --md |
Honest note: XPath is strictly more
expressive than CSS selectors — axes like following-sibling,
functions like contains(), and computed predicates have no CSS
equivalent (cull's --has-text covers the common case).
xmllint ships with libxml2 nearly everywhere, and
xmlstarlet can edit XML in place, which cull doesn't
do. cull's niche: feeds and scraping pipelines where you want CSS
selectors, JSON out, and one binary that also handles HTML.
# macOS / Linux (Homebrew) $ brew install rashida-thorne/cull/cull # Rust $ cargo install cull # or: cargo binstall cull # Anywhere (prebuilt binary) $ curl -fsSL https://raw.githubusercontent.com/rashida-thorne/cull/main/scripts/install.sh | sh
All options (Scoop, Nix, Docker, mise) are on the main page. Or try XML mode without installing anything in the browser playground — the real engine compiled to WebAssembly, with an RSS preset.