Skip to content

Releases: rashida-thorne/cull

v0.12.0

Choose a tag to compare

@github-actions github-actions released this 26 Jul 23:25

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 --pretty deletes 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

Choose a tag to compare

@github-actions github-actions released this 26 Jul 22:00

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 --xml with a comment-only body — aborted with html node missing in 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 -I interactive mode, and the WASM playground.

Internal

  • scripts/fuzz-smoke.py extended to cover XML mode, nested -j templates, --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)

Choose a tag to compare

@github-actions github-actions released this 26 Jul 21:25

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

demo

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

Choose a tag to compare

@github-actions github-actions released this 26 Jul 21:18

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)

Choose a tag to compare

@github-actions github-actions released this 26 Jul 18:17

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:

  • --xml forces XML parsing (malformed XML → hard error, exit 2); --html forces 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: pubDate matches <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
  • -t puts each XML field on its own line; serialization keeps <link> children intact
  • Charset sniffing now honors <?xml version="1.0" encoding="…"?> (after BOM and Content-Type, before <meta charset>)
  • Sitemaps: cull 'url > loc' -t https://blog.rust-lang.org/sitemap.xml (and sitemap > loc for sitemap indexes)

162 tests. See the cookbook §11 for live-verified recipes, and the CHANGELOG.

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 26 Jul 16:01

--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"]}
  • attrs is its own object — pup inlines attributes next to tag/text, so an attribute literally named tag collides; here it can't
  • text on every node is the collapsed subtree text (layout-aware: <br>/block boundaries become spaces, script/style excluded)
  • children interleaves 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
  • --array merges everything into one JSON array; -p pretty-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

Choose a tag to compare

@github-actions github-actions released this 26 Jul 15:10

Nested objects and filter chains in -j templates

The shaped-JSON templates can now express real hierarchy:

  • sel {…} evaluates the object inside sel's first match (null if 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 (-j values, --table cells, --has-text) is now layout-aware: A<br>B no longer glues to AB, and script/style content 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

Choose a tag to compare

@github-actions github-actions released this 26 Jul 08:04

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' matches price: <b>42</b>).
    $ cull 'tr' --has-text FAILED -t report.html

Changed

  • -t / --text now lays text out the way a browser would (innerText-style): <br> and block-element boundaries become newlines, <pre>/<textarea> stay verbatim, script/style/template are skipped, inline whitespace still collapses. Previously block boundaries were dropped entirely (<div>x<p>y</p></div>xy; now xy). Single-line values in -j templates and --table cells 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

Choose a tag to compare

@github-actions github-actions released this 26 Jul 03:57

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; overriding User-Agent suppresses the default cull/VERSION one
  • New --timeout SECS bounds each fetch (default 30 s, 0 disables) — 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

Choose a tag to compare

@github-actions github-actions released this 25 Jul 16:00

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 -l semantics)
  • -c/--count prints file:count lines when given multiple inputs
  • -1/--first applies per input (like grep -m1)
  • -j --array merges 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 --base still 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.