Releases: rashida-thorne/cull
Release list
v0.12.0
Faithful output
This release is about a promise: what comes out of cull renders exactly like what went in.
-p / --pretty is now rendering-faithful
Pretty-printers for HTML have a subtle bug class: line breaks are whitespace, so adding or removing them between inline elements changes what a browser renders.
- htmlq's
--prettydeletes the significant space in<b>a</b> <i>b</i>→ renders "ab" instead of "a b" (mgdm/htmlq#58) - pup's always-indented output adds whitespace:
<b>a</b><i>b</i>x("abx") is split across lines → renders "a b x"
(Both verified against pup 0.4.0 and htmlq 0.4.0 while preparing this release.)
cull's -p now keeps inline content together in "runs" and only breaks lines at block boundaries, where extra whitespace can never change rendering:
$ echo '<p><b>bold</b><i>ital</i>x</p>' | cull -p 'p'
<p><b>bold</b><i>ital</i>x</p>
$ echo '<div>intro <b>x</b><p>para</p>tail</div>' | cull -p 'div'
<div>
intro <b>x</b>
<p>para</p>
tail
</div>Render-nothing elements (<link>, <meta>, <script>, comments) join runs too — Wikipedia emits <link> tags mid-sentence, and breaking around them used to inject a space before the following comma. <head>-style metadata stacks still get one tag per line.
Verification: pretty-printing the lobste.rs front page and a 1.1 MB Wikipedia article, then diffing cull body -t output before vs. after — byte-identical rendered text on both.
Whole-document output keeps the DOCTYPE
curl page | cull (no selector) now emits <!DOCTYPE html> and top-level comments. pup and htmlq both silently strip the DOCTYPE (mgdm/htmlq#56), which quirks-modes any page you round-trip through them.
cull <URL> with no selector
A URL can never be a valid CSS selector, so cull https://example.com now means "fetch and emit the whole document" in every mode (previously it errored unless --md/--table/-I was given).
189 tests (+7 playground) · fuzz smoke re-run clean · full changelog
Install: brew install rashida-thorne/cull/cull · cargo binstall cull · scoop bucket add cull https://github.com/rashida-thorne/scoop-cull && scoop install cull · nix run github:rashida-thorne/cull · more options
v0.11.2
Patch release: fixes a fuzz-found panic.
Fixed
- No more panic on element-less documents. Input that parses to a tree with no root element — e.g. only an
<?xml version="1.0"?>declaration, or--xmlwith a comment-only body — aborted withhtml node missingin whole-document modes (--md,--table,--json-nodes, selector-less runs). It now behaves like "no matches" (exit code 1). The fix applies to the CLI, the-Iinteractive mode, and the WASM playground.
Internal
scripts/fuzz-smoke.pyextended to cover XML mode, nested-jtemplates,--json-nodes, and the newer flags (~1900 cases, 0 failures). 175 tests.
Install: curl -fsSL https://raw.githubusercontent.com/rashida-thorne/cull/main/scripts/install.sh | sh · cargo binstall cull · brew install rashida-thorne/cull/cull · scoop bucket add cull https://github.com/rashida-thorne/scoop-cull; scoop install cull · docker run ghcr.io/rashida-thorne/cull
Full changelog: v0.11.1...v0.11.2
v0.11.1 — interactive selector mode (-I)
-I — interactive selector mode
Hunting for the right selector usually means edit → rerun → squint → repeat.
-I collapses that loop into a live-preview TUI:
cull -I https://example.com
curl -s https://news.ycombinator.com | cull -I '.athing'- type to edit the selector — matches and preview update per keystroke
- Tab cycles the output shape: HTML → text → Markdown → JSON node tree
(plus whatever flags you launched with, e.g.-j '{…}'or--table) - ↑/↓/PgUp/PgDn scroll, Enter prints the current result to stdout
(exit code follows matches, grep-style), Esc quits without printing - invalid selectors show inline instead of erroring out
The UI draws on stderr and reads keys from the terminal, so stdin can be
a pipe and stdout stays clean: curl -s … | cull -I | tee picked.html works
exactly like you'd hope. Flags like --has-text, -r, -b, and -1 shape
the preview too.
Ships as a default-on interactive cargo feature; --no-default-features
builds (like the WASM playground) skip the dependency. Neither pup nor htmlq
has an equivalent.
(0.11.1 folds in a same-day fix over 0.11.0: cull -I page.html /
cull -I https://… now treat the first positional as the input, same
disambiguation as --table/--md.)
Full changelog: https://github.com/rashida-thorne/cull/blob/main/CHANGELOG.md
v0.11.0
Superseded the same day by v0.11.1, which adds the cull -I <input> disambiguation fix. Use 0.11.1.
v0.10.0 — real XML mode (RSS/Atom, sitemaps)
Real XML mode — RSS/Atom feeds, sitemaps, SVG, OPML now parse correctly.
HTML parsers silently mangle XML: <link> is a void element in HTML, so every RSS item's URL is lost (try pup 'item link' or htmlq 'item link' on any feed — nothing comes back); pubDate gets lowercased; namespaces are mishandled. cull now detects XML (an <?xml…?> declaration or a known root: <rss>, <feed>, <urlset>, <sitemapindex>, <opml>, <svg>) and parses it with a real XML parser. Every feature just works on it: CSS selection, -j templates, -t, -p, --json-nodes, -i, multiple inputs.
A feed reader is now one line:
$ cull item -j '{title: title, url: link, date: pubDate}' https://lobste.rs/rss
{"title":"A shell colon does nothing. Use it anyway","url":"https://refp.se/…","date":"Sat, 25 Jul 2026 06:33:00 -0500"}Details:
--xmlforces XML parsing (malformed XML → hard error, exit 2);--htmlforces HTML; auto-detection is per input, and auto-detected-but-malformed input warns and falls back to the forgiving HTML parser- In XML mode selectors are case-sensitive, as XML requires:
pubDatematches<pubDate>only - Namespaced tags keep their prefix — select with an escaped colon:
cull item -j '{thumb: media\:thumbnail @url}' https://feeds.bbci.co.uk/news/rss.xml -tputs each XML field on its own line; serialization keeps<link>children intact- Charset sniffing now honors
<?xml version="1.0" encoding="…"?>(after BOM andContent-Type, before<meta charset>) - Sitemaps:
cull 'url > loc' -t https://blog.rust-lang.org/sitemap.xml(andsitemap > locfor sitemap indexes)
162 tests. See the cookbook §11 for live-verified recipes, and the CHANGELOG.
v0.9.0
--json-nodes — full node dumps for jq
pup's json{}, with a cleaner shape. Each match becomes one NDJSON object:
$ cull '.post h2 a' --json-nodes blog.html
{"tag":"a","attrs":{"href":"/posts/hello"},"text":"Hello, world","children":["Hello, world"]}attrsis its own object — pup inlines attributes next totag/text, so an attribute literally namedtagcollides; here it can'ttexton every node is the collapsed subtree text (layout-aware:<br>/block boundaries become spaces,script/styleexcluded)childreninterleaves child element objects with text-node strings — full structure, comments dropped- URL attributes (
href,src, …) respect-b/--base, and auto-resolve when the input is a fetched URL --arraymerges everything into one JSON array;-ppretty-prints — same as-j
It's also the escape hatch for raw <script> payloads, e.g. JSON-LD structured data:
$ cull 'script[type="application/ld+json"]' --json-nodes page.html \
| jq '.children[0] | fromjson | {name, datePublished}'When you'd rather name the fields up front and skip the jq pass, -j templates are still the short way: cull a -j '{url: . @href, text: .}'.
140 tests.
Install: brew install rashida-thorne/cull/cull · cargo binstall cull · scoop bucket add cull https://github.com/rashida-thorne/scoop-cull && scoop install cull · nix run github:rashida-thorne/cull · more options
Full changelog: https://github.com/rashida-thorne/cull/blob/main/CHANGELOG.md
v0.8.0
Nested objects and filter chains in -j templates
The shaped-JSON templates can now express real hierarchy:
sel {…}evaluates the object insidesel's first match (nullif nothing matches)[sel {…}]emits one object per match — arrays of objects, nested arbitrarily deep
$ cull '.story' -j "{title: .link a, score: .upvoter | num, tags: [.tag],
by: .byline {user: \"a[href^='/~']:not(:has(img))\", when: time @datetime}}" https://lobste.rs
{"title":"…","score":91,"tags":["programming"],"by":{"user":"refp","when":"2026-07-25 06:33:00"}}- New filters
| trim,| lower,| upper— and filters now chain:{href: a @href | trim | lower}
Fixed
- Collapsed text (
-jvalues,--tablecells,--has-text) is now layout-aware:A<br>Bno longer glues toAB, andscript/stylecontent never leaks into extracted values.
131 tests. Try it in the browser playground — the "nested JSON" preset runs this release compiled to WASM.
Install: brew install rashida-thorne/cull/cull · cargo binstall cull · scoop bucket add cull https://github.com/rashida-thorne/scoop-cull && scoop install cull · nix run github:rashida-thorne/cull · more options
Full changelog: https://github.com/rashida-thorne/cull/blob/main/CHANGELOG.md
v0.7.0 — inner HTML, --has-text, browser-style text
Three features in this release, and every one of them answers a long-open feature request on htmlq's issue tracker (mgdm/htmlq #75, #55, #74):
Added
-i/--inner— print inner HTML: children only, no outer tag. Composes with-p(pretty) and--color.$ cull '#readme' -i page.html--has-text STRING— keep only matches whose text content contains STRING. Repeatable (all strings must be present); runs before-1/-c/-l, so counts and file lists reflect the filter. Needles match across inline-tag boundaries (--has-text 'price: 42'matchesprice: <b>42</b>).$ cull 'tr' --has-text FAILED -t report.html
Changed
-t/--textnow lays text out the way a browser would (innerText-style):<br>and block-element boundaries become newlines,<pre>/<textarea>stay verbatim,script/style/templateare skipped, inline whitespace still collapses. Previously block boundaries were dropped entirely (<div>x<p>y</p></div>→xy; nowx⏎y). Single-line values in-jtemplates and--tablecells are unchanged.
16 new tests (119 total). The playground runs 0.7.0.
Install: curl -fsSL https://raw.githubusercontent.com/rashida-thorne/cull/main/scripts/install.sh | sh · brew install rashida-thorne/cull/cull · cargo binstall cull · scoop bucket add cull https://github.com/rashida-thorne/scoop-cull && scoop install cull · nix run github:rashida-thorne/cull · docker run ghcr.io/rashida-thorne/cull
Full changelog: https://github.com/rashida-thorne/cull/blob/main/CHANGELOG.md
v0.6.0
What's new
Custom headers for built-in fetches. Some sites 403 unknown user agents or want a cookie — you no longer have to fall back to curl | cull. -H/--header works exactly like curl's:
cull '.headline' -t -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64)' https://example.com
cull '.dashboard .stat' -t -H 'Cookie: session=abc123' https://example.com/me
cull --table -H 'Authorization: Bearer …' https://internal.example.com/report-H 'Name: Value'is repeatable; overridingUser-Agentsuppresses the defaultcull/VERSIONone- New
--timeout SECSbounds each fetch (default 30 s,0disables) — previously a stalled origin could hang forever - Fetch behavior is now covered by offline integration tests that assert the exact request headers on the wire
Scope note: no custom TLS, proxies, retries, or POST — for anything fancier, curl -s URL | cull remains the right tool.
103 tests. Install: cargo install cull, cargo binstall cull, brew install rashida-thorne/cull/cull, or the curl|sh installer.
v0.5.0 — multiple input files
What's new
Multiple input files. cull now takes any number of files/URLs, so globs just work:
cull 'a' -a href pages/*.html # links from every page, in order
cull 'img:not([alt])' -c site/*.html # per-file counts: site/x.html:4
cull '.error' -l logs/*.html # grep -l: names of matching files- New
-l/--files-with-matches(grep-lsemantics) -c/--countprintsfile:countlines when given multiple inputs-1/--firstapplies per input (likegrep -m1)-j --arraymerges matches from all inputs into a single JSON array- Unreadable inputs are reported on stderr and skipped; the rest still run (exit 2 at the end, like grep)
- Auto
--basestill resolves per-input when inputs are URLs
95 tests. Install: cargo install cull, cargo binstall cull, brew install rashida-thorne/cull/cull, or the curl|sh installer.
