cull / rss & xml → json

RSS to JSON from the command line

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.

A one-line feed reader

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.

Why your HTML tool silently breaks on RSS

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.

Atom feeds (the URL is in an attribute)

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

Namespaced tags: media:thumbnail & friends

Escape 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"}

Sitemaps, counts, filters

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

A new-posts watcher in two lines of cron

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

How this compares to xmllint / xmlstarlet

xmllintxmlstarletcull
Query languageXPath 1.0XPath 1.0CSS 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 HTMLlenient mode ≠ browser parser✓ (browser-grade html5ever)
CSV / Markdown outputCSV 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.

Install

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