You first and foremost need to know just whether there are any new items in your feeds. | There once were less lines of the below script when it was less robust.
set -uo pipefail
_lnp(){ echo "//*[local-name()='$1']/*[local-name()='$2']/text()" ; }
pubxpath="`_lnp item pubDate`|`_lnp entry published`;`_lnp entry updated`"
pubxpath="concat(${pubxpath//;/,\"|\",})"
alias each='while IFS= read -r; do'
alias each_none_pub='each [[ $REPLY != "|" ]] && echo "$REPLY" ||'
none_pub_filt(){ each_none_pub echo "Incorrect elements." >&2; done ; }
alias each_xmldates='xmllint --xpath "$pubxpath" - | none_pub_filt | each'
first_delim(){ REPLY=${REPLY#\|}; echo ${REPLY%%\|*} ; }
xml_pubdates(){ each_xmldates echo `first_delim`; done ; }
epoch_parse(){ date +%s -d"${REPLY:-+11111111 days}" ; }
days_ago(){ each expr $((`date +%s` - `epoch_parse`)) / 86400; done ; }
pub_days_ago(){ xml_pubdates | days_ago | sort -n | head -1 ; }
err_annot(){ awk -v p="$REPLY" '{print p ": " $0}' 1>&2; }
pub_piped(){ echo `pub_days_ago`d ago$'\t'$REPLY ; }
(each curl -sL "$REPLY" | pub_piped 2> >(err_annot) & done; wait)
Lines kept under 75 characters. Lines are self-contained and can be executed one by one, and amended interactively. Return key is not expected to be immediately pressed — you need to either add “< feeds” to put your list of feed URLs through it, or press Home/Ctrl+A and pipe it (or use Process Substitution and pipe it with <(command) syntax). Same if you save this to a file to be sourced with . or source command.
- Fail command if any in pipeline fails. Disallow silently accepting variables unset.
_lnpfunction produces XPath 1.0 selector for text content of $2 elements children of $1 elements anywhere in the document and regardless of XML namespace. While RSS tags are not namespaced, Atom tags are supposed to.pubxpathis defined first as three selectors joined in different ways: //item/pubDate (RSS) and //entry/published (Atom) are bound into alternative (|), while the other is separated by a placeholder semicolon.pubxpathis immediately redefined to substitute the semicolon for a,"|",and wrapped inconcat()— results of the two selectors will be concatenated with a pipe character between them (likeconcat($1,"|",$2)).eachis an abbreviating alias that immediately precedes a command that is supposed to be ran in a loop over the lines of piped input. It still requires a done keyword. The item from the loop is, per Bash defaults, put in the$REPLYvariable.each_none_pubis an abbreviating alias that immediately precedes a command that is supposed to be ran only for lines of the piped input that are just “|” (the pipe character by itself), instead of just echoing the line.none_pub_filtis a function that is a pipeline filter to just print “Incorrect elements.” to standard error output instead of passing the line through.each_xmldatesis an alias abbreviating execution of xmllint on standard input — a libxml utility that can also extract us things from XML properly using XPath — piped through thenone_pub_filtand supposed to immediately precede a command plainly just likeeachdoes, but with content from xmllint.first_delimis a function that echoes to standard output the$REPLYvariable without a leading pipe character and without anything that occurs after a pipe character. I used a pipe-character-delimited string as a union value representation allowing presence of both.xml_pubdatesis a function that for each union kind of result from xmllint that was not filtered out as nil, appliesfirst_delim, to its standard output.epoch_parseaccepts any sort of human-readable timestamp (RFC822 from RSS or ISO8601 from Atom) and turns it into Unix epoch seconds integer usingdateon the$REPLYvariable. In case of no produced timestamp, it fills in with one that will result in “11111111 days ago” for robustness.days_agoproduces a rounded date difference in days between now and the date in$REPLYvariable.pub_days_agopicks the one least count of days that resulted from comparing dates from the XML elements of the feed.err_annotis a filter that prints standard input to standard error output with the prefix of the$REPLYvariable (a URL).pub_pipedprints the least count of days since last publication in the format of “Nd ago” followed by a tab character and the URL.- The final line lacks a redirection to it of the list of feeds as explained before this enumeration. It runs curl with flags for silence and following HTTP redirections for each line of input as the URL, printing the count of days since most recent publication alongside the URL for each. Any error messages are getting annotated with the URL before they reach the terminal. Each pipeline starting with curl is turned into a simultaneous Bash job that are then all waited for to complete. There is no job control messages from a subshell. And a subshell invocation, even with a wait after the done, can still have a redirection appended or prepended around the whole line.
Just keep a list of your feeds as a bare list of URLs, one feed URL per line. Copy-paste it from somewhere whatever machine you are on; then copy-paste the 16 portable lines into Bash and direct one into the other. No read tracking, no caches, no unreads, no aggregation, no setup — just go to the friendly website when you see there is a fresh piece over there. Produce a list of XML feeds from wherever you think of.
Idea adapted from my previous post, Atom/RSS feeds dish for a browser capable of framesets — with some Perl.
I’m the most baffled REPLY=${REPLY#\|} parameter expansion doesn’t require quoting.
Could have perhaps be a piece of neater Nu shell. But nushell is not always reliable to install and use: the issue of it segfaulting on any attempt to use its http client remains unresolved.