
This page is among our best articles.

showme is a bash script to show me what I got.
It basically takes a dive into the black hole that is my filesystem and brings on the other side of the even horizon material which would be irretrievably lost to the information paradox if not for the tricky set of linux tools allowing to quickly and efficiently resurface something important.
It can work as a command line or as a widget on the kde menu (reading from the clipboard or offering extra editing facilities).
Invoked on a keyword, it will figure out what the most likely thing this keyword refers to, and show it to me in the corresponding viewer. It will offers alternatives if there is doubt, and can even retrieve content from the web if it's not found locally.
It is used mainly with material which I tend to watch a lot. This includes: The pdf file of a paper I've got in my collection:
showme delvalle12a
opens the pdf file. It works if the file is in the bibTeX database (i.e., in ~/bib somewhere) and has .pdf extension. There is bash autocompletion, which is welcome for zubi... papers (?!). Since October 2026, Tab completes files and folders too, as it does after ls (see Source).
Since v°4.0, it opens any other file as well, found by its name alone: see Any file. Since v°4.4, an AF tag opens its post: see AF tags.
Since v°5.0, it sits in the panel of the desktop too, a widget of KDE: the cromulon on its black hole at the top of this page (click it to see it eat). A middle click feeds it what is selected; a click opens what is in the clipboard in a little editor with the keys of Emacs (C-a, C-e, C-k, C-yâŠ), to put it right first, and Enter feeds it that; a file can also be dropped on it. showme then shows it as if I had typed it in a terminal, but no terminal comes up unless it asks something or fails, and while it works out of sight the head bobs. The black hole is M87*, the first photograph of one (Event Horizon Telescope Collaboration, 2019, ESO, CC BY 4.0).
One can use another viewer or application (e.g., gimp):
showme delvalle12a --with evince
the --with can be put anywhere.
The option -2 also opens another copy in the right window manager (which I often need to work on two copies simultaneously):
showme delvalle12a -2
The option -p (or --path) prints where the file is instead of opening it, which is what one wants when feeding the path to something else:
showme delvalle12a -p
The option -b (or --bib), since v°4.20, prints the paper's bibTeX entry and, below it, its reference as it would be typeset, and opens nothing:
showme delvalle12a -b
The option -s (or --size), since v°4.25, says how big the file is, in bytes and roughly as ls -lh puts it, and opens nothing either.
The option -v (or --version) displays the cromulon headâthe script prints its own header, so the version and its date come straight from the horse's mouth.
The option -h (or --help), since v°4.2, prints a summary of what is on this page.
With nothing to show at all, since v°4.1, showme alone has the cromulon ask for something: the head of the banner is drawn and its mouth says Show me what you got, a shape for each sound (sh-ow, m-ee, wh-a-t, y-ou, g-o-t), the jaw dropping as it opens, while the words of the logo come out one by one as they are said. Off a terminal, or on one too narrow, the head is just printed, words and all.
Since v°4.5, on a Konsole, what showme says to me comes after a little cromulon head, the one of the wiki's boxes. Off a terminal (in a pipe, or for another script that reads what it says), nothing is added.
A doi does as well as a key, since v°3.2, in whichever form it comes pastedâbare, with its doi: prefix, as a doi.org address, or with the reference list's period still attached:
$ showme 10.1038/1781449a0
showme: 10.1038/1781449a0 is purcell56a
The bibliography is asked which entry carries this DOI in its url or doi field, and that entry's key takes over from there: this is showme purcell56a, whatever follows includedâthe pdf is opened, or fetched back from ~/Downloads, or the entry is displayed with the sci, huh? offer. The match is case-insensitive, as DOIs are, and must fill the whole field, so that 10.1038/nphys327 does not pass for 10.1038/nphys3272. A DOI that no entry carries is an error, which quotes the doi2bib line that would add it.
A citation pasted from the wiki does as well, since v°3.3: showme {{bromberg09a}} is showme bromberg09a, and so are {{cite|bromberg09a}}, {{onlinecite|âŠ}}, {{citer|âŠ}}, {{citet|âŠ}} and {{doi|âŠ}}, which goes on to the DOI lookup above. The braces go, the template name goes, and so does a period or comma left stuck to the closing braces. A citation holding several keys, {{cite|bromberg09a,he08b}} or {{cite|bromberg09a|he08b}}, shows them one after the other, with the same options:
$ showme '{{cite|bromberg09a,he08b}}'
showme: 2 keys in '{{cite|bromberg09a,he08b}}', one after the other: bromberg09a he08b
Quote anything with a pipe in it: to the shell, {{cite|bromberg09a}} unquoted is a pipeline, and the script says so when it is handed the {{cite that is left of it. Bare {{bromberg09a}} needs no quotes.
A reference as a paper prints it does as well, since v°3.4, so that a reference list leads straight to the pdf:
$ showme 'Nano Letters 22, 2023â2029 (2022).'
showme: Nano Letters 22, 2023â2029 (2022) is chen22a
The volume and first page find the candidates in the bibliography, and the journal must then be the entry's own, under any of its spellings: the @string of its key, a name that iso4 sends to it, or the key itself (PRL 98, 117402 (2007)). The names are compared word by word the way j2bib compares them, so that Nano Letters is Nano Lett. and Proc. R. Soc. A 242 is a Proc. Roy. Soc filed with volume A 242; a match that needed a contraction (Natl. for National) is taken, but said to be loose. The ACS order, Nano Lett. 2022, 22, 2023â2029, is understood too, and so is a whole reference with its authors and title in front, an issue number, an article number (eaau6969, L041101, 241306(R)) or no year at all. When two entries share journal, volume and page, the year decides, and if it cannot they are listed; when the bibliography has the paper under another year, it says so. A reference that no entry matches is an error, which lists the entries with the same volume and first page in another journal, if there are any. Quote the reference: to the shell, (2022) is a subshell. Without the year, the words are put back together and showme Nano Lett. 22, 2023 works as it stands.
A stop or comma copied after any of these, since v°4.7, is dropped: showme chen22a. is showme chen22a, and showme 0K62vb, is showme 0K62vb.
Three things are looked at, in order, and the first that yields a match wins.
First locate, as always, but since v°4.3 on the names of the files only, and for the pdfs in ~/bib only: it had looked for the key anywhere in the path, and for bib anywhere too, so that showme laussy, every path holding /home/laussy/, opened whatever came first of 2608 files. A part of a key that several papers share lists them, numbered, as for files below: showme laussy lists twenty of my papers, and showme laussy N opens the N-th. Then, if that came up empty, ~/bib/sci and ~/bib/transit are searched directly: locate's database is rebuilt once a night, so a paper filed this morning is invisible to it until tomorrow and would otherwise be declared missing.
Finally, if the paper is in neither place, ~/Downloads is searchedâbecause sometimes I download a paper and forget to file it. If it is there, it is moved into ~/bib/transit and then opened, so the mistake repairs itself the first time I go looking for the paper:
$ showme basov25a
showme: filed basov25a.pdf from Downloads into /home/laussy/bib/transit
An exact <key>.pdf wins outright and only then is a looser *key* match tried. A browser's duplicate suffix is undone on the wayârau14a (1).pdf is filed as rau14a.pdfâbut only when the rest of the name is exactly the search term, so that showme rau14 can never truncate rau14a.pdf to rau14.pdf. Nothing in ~/bib/transit is ever overwritten: if the paper turns out to be filed already, that copy is opened and the download is left where it is. And if several candidates match loosely, they are listed and nothing is moved, since guessing which one I meant is worse than not guessing. Since v°4.0, this is done only for a key the bibliography has: a report-0Ksfub.pdf lying in ~/Downloads is no paper to be filed away in transit, but a file (see Any file).
If there is no pdf anywhere but the key is in the bibTeX database (sci.bib, arXiv.bib or Books.bib), the entry is displayed instead of an error, both raw and nicely formattedâthe journal @string abbreviations expanded, the TeX accents turned into Unicode and the title set in italics:
$ showme agarwal90b
showme: no PDF for 'agarwal90b', but the bibliography knows it (sci.bib):
@Article{agarwal90b,
author = {G. S. Agarwal},
title = {Dressed-state lasers and masers},
journal = pra,
year = 1990,
volume = 42,
pages = 686,
url = {doi:10.1103/PhysRevA.42.686}
}
G. S. Agarwal,
âDressed-state lasers and masersâ,
Phys. Rev. A 42, 686 (1990).
See sci huh? [Y/n]
showme: agarwal90b.pdf is in the clipboard, for the save box
The last two lines appear when the entry carries a doi (fished out of the url or doi field): the question, and what a bare Enter (or anything but n) does about itâopen chrome on the paper at the alternative supplier, https://sci-hub.ru/10.1103/PhysRevA.42.686 in this case, and put agarwal90b.pdf in the clipboard, which is the name the download should be saved under, so that the save box is a paste away and the next showme agarwal90b finds the file in ~/Downloads and files it by itself. The key is spelled as the bibliography has it, whatever the case it was typed in.
Since v°4.0, what is not a paper is a file, and showme finds it by its name alone, wherever it is, and opens it with what shows its kind: gwenview for a picture, inkscape for an svg drawing (since v°4.8), okular for a pdf, emacs for text (a script, a .bib, a .tex, some json), dolphin for a folder, and whatever the desktop opens it with for the rest (a Mathematica notebook, a video, a web page, a Word file). --with still has the last word. A program, though, is not run (since v°4.18): showme /usr/bin/ls says that it is one.
showme last-report-0Ksfub.pdf
The paper has priority: a name is taken for a file only when no pdf is filed under it and the bibliography has no such keyâor, for words with spaces in them, when they are no reference either, so that showme 'my notes.txt' works too. A name with an extension must then be the whole name of the paper's file: showme chen22a.pdf is the paper, but report.pdf is not taken for some annual-report.pdf in transit.
The name is looked up in the locate database: the file's exact name, a glob ('report-*.pdf', quoted), or the end of a path (kgon/report.pdf). A path that exists, ./fig.png, is opened as it is, and a file of that name in the current folder is found even if it was made today. Without an extension, showme report-0Ksfub takes any report-0Ksfub.*, but not the .aux, .log and other leftovers of LaTeX, unless that is all there is. A name the database does not knowâthe file is newer than last night's rebuildâhas the database rebuilt with sudo updatedb, as my lls does, and is looked up once more.
No file of that name at all, since v°4.6: the files that have it in their name. An AF tag mostly: showme 0LrpRb.pdf opens landau-group-frames-0LrpRb.pdf (the extension given stays), and showme 0LrpRb all its files, the .png, the .pdf, the .svg. It takes three letters at least, and no glob or path, which say what they want. The case of the letters counts. Only files in sight are taken so: not in a hidden folder, where a program keeps what it has of a file (Okular's notes on a pdf, which had opened in chrome as xml), since v°4.17, nor in the system's own folders, /usr, /var and the like, since v°4.18.
Several files of that name are versions: the newest is opened, and the others are listed, newest first, with how to open another one:
$ showme MZI.pdf
showme: 4 files are called MZI.pdf, the newest first; another one: showme MZI.pdf N
â 1 2022-03-29 22:16 25K ~/publi/books/lectures-of-physics/10--electrodynamics/21-22/figs/12/MZI.pdf
2 2022-03-29 22:16 25K ~/publi/books/lectures-of-physics/10--electrodynamics/22-23/figs/12/MZI.pdf
3 2022-03-29 22:16 25K ~/publi/books/lectures-of-physics/10--electrodynamics/23-24/figs/12/MZI.pdf
4 2022-03-29 22:16 25K ~/publi/books/lectures-of-physics/10--electrodynamics/24-25/figs/12/MZI.pdf
and then
showme MZI.pdf 3
opens the third. Links to the same file count onceâthe many sci.bib of my projects are links to ~/bib/sci.bib, which is the one listedâand copies in hidden folders (the Trash, .git, .ocr-backup) come after the others. A long list shows its first fifteen; -p prints all the paths, in the same order, so that showme -p report.pdf | cat -n numbers them all (with a number, -p prints that one only). The number works for a paper too, among its pdfs as -p lists them: showme alhilfy85a 2 opens the copy in transit.
Since v°4.4, an AF tag opens its post of đ or †in chrome, on the local wiki, else on laussy.org:
$ showme 0K62vb
showme: 0K62vb is Thursday 15 October 2026, 01:34:27 UTC+08
showme: Y-0K62vb is on October (2026)
The tag comes as in the anchor of the post, Y-0K62vb or Z-0K62vb (that branch only), with its # or without, or bare, when †goes before đ for a post that is on both. A word is taken for a tag when it has the shape of one, five letters or digits and perhaps the timezone letter, and reads a moment between the start of the era and a year from now: a key like li08a would be in 2048, and stays a key. What the tag reads is said first, since v°4.11. The post is looked for in the month of that moment and the months on either side, on the local wiki first. laussy.org, a few seconds away, is asked only when it must be, and since v°4.9 for all its pages at once, with a line to say so, so that the wait does not feel like a hang.
A bare tag of no post goes to the files that have it in their name (since v°4.6), standing apart in it (since v°4.18): no letter or digit just before it, and after it at most its timezone letter. Glued to more, it is chance: 01143 in DSC01143.JPG, or a run of letters in the hex name of a file of the system, which KDE then refused to launch.
$ showme 0LrpRb
showme: 0LrpRb is Thursday 15 October 2026, 10:58:14 UTC+08
showme: 5 files are called 0LrpRb in part, the newest first; another one: showme 0LrpRb N
â 1 2026-10-15 04:58 1,3M ~/Microcavities-3rd/Material/landau-group-0LyiWb/landau-group-frames-0LrpRb.png
2 2026-10-15 04:58 416K ~/Microcavities-3rd/1--manuscript/cfig/3rd/ch10/landau-group-frames-0LrpRb.pdf
3 2026-10-15 04:58 416K ~/Microcavities-3rd/Material/landau-group-0LyiWb/landau-group-frames-0LrpRb.pdf
4 2026-10-15 04:58 380K ~/Microcavities-3rd/Material/landau-group-0LyiWb/landau-group-frames-0LrpRb.svg
5 2026-10-15 01:59 30K ~/Microcavities-3rd/Material/landau-group-0LyiWb/contours-for-0LrpRb.json
A file with the tag in the current folder, a figure made a minute ago, is found at once, since v°4.10, with neither laussy.org asked nor the database rebuilt.
In no file either, the tag goes to the pages of the wiki that use it (since v°4.14), the most first, read off the copy of every page that wikigrep keeps up to date:
$ showme 00Yps
showme: 00Yps is Tuesday 18 August 2026, 11:11:01 CEST
showme: no post 00Yps on the local wiki, asking laussy.org (a few seconds)...
showme: nothing called '00Yps', not even in part, in the locate database, updating it (lls)...
showme: 00Yps is on no post and in no file, but on 8 pages of the wiki, 40 times:
â 1 10Ă MediaWiki templates
2 8Ă Template:Afh
3 7Ă Blog:Hacks/Anno Fabri
4 5Ă Module:AF
5 4Ă Template:Af
6 3Ă AF
7 2Ă MediaWiki:Gadget-aftool.js
8 1Ă Blog:Fabrice/100 days of Anno Fabri
Open which? ââ Enter, q none:
showme asks which page to open (since v°4.15), and the list is a menu (since v°4.16): â and â move the arrow, Enter opens, a number goes straight to its line, q opens none. One page only is asked about with a [Y/n], and showme 00Yps 3 opens the third with no question. The page opens on the first use of the tag, highlighted.
Not even there, the tag may be mistyped. Since v°4.12, the tags of its fabriad and triduum (its first two letters, see AF) with its last three in another order or case are looked for, in posts, files and pages, and showme asks before going on with one:
$ showme 0Bomo
showme: 0Bomo is Saturday 19 September 2026, 22:37:34 CEST
showme: no post 0Bomo on the local wiki, asking laussy.org (a few seconds)...
showme: nothing called '0Bomo', not even in part, in the locate database, updating it (lls)...
showme: nothing carries 0Bomo, but 0Boom does, a post on September (2026): Friday 18 September 2026, 16:25:54 CEST
Go on with 0Boom? [Y/n]
Several such tags are a menu too. Off a terminal, nothing is opened on a guess: the error says which tag to try, or how to open the N-th page.
The first version that served me well for almost 600 days was a simple hack:
#!/bin/bash
# v°0.1 Sun 31 Dec 2023
okular `locate $1 | grep -i ".pdf$" | grep -i bib`
Last version:
#!/bin/bash
# #@=----===@%:
# :#:::-=---===+=@=-
# +=%* :-+=:.... .:@- ____ _
# @-+#-*%+**+#=+*#@@@@#@ / ___|| |__ _____ _
# @=%.% # @ .# %= \___ \| '_ \ / _ \ \ /\ / /
# #:.. @ @# * @ @# ## ___) | | | | (_) \ V V /
# #.=:% .%. @-=* @% |__ __| |_|_|\___/ \_/\_/
# %@.*@ :#@@@-. @ +%@%- @ | \/ | ___
# *@@% #@ .. =+.. ...@ | |\/| |/ _ \ what
# @:#-.:-= .:.+#-: .. : @ | | | | __/ you got
# @.:---::@. ..:-++=:.-%.% |_| |_|\___|
# @=-:-:@ @++==-:-=++-@%. v2.2 Wed Oct 28 2026
# @-::. : . . .. . :@ 2.4 Fri Aug 7 2026
# @:--:...........=* 3.0 Mon Aug 10 2026
# **=-=::.......:.@ 3.1 Fri Sep 11 2026
# *=:-=.:::::::: @ 3.2 Sat Sep 12 2026
# %@.::.::::::.+# 3.3 Sun Sep 13 2026
# -@@+=:.....+@ 3.4 Tue Oct 6 2026
# 4.0 Thu Oct 15 2026
# 4.1 Thu Oct 15 2026
# 4.2 Thu Oct 15 2026
# 4.3 Thu Oct 15 2026
# 4.4 Thu Oct 15 2026
# 4.5 Thu Oct 15 2026
# 4.6 Thu Oct 15 2026
# 4.7 Thu Oct 15 2026
# 4.8 Thu Oct 15 2026
# 4.9 Thu Oct 15 2026
# 4.10 Thu Oct 15 2026
# 4.11 Wed Oct 21 2026
# 4.12 Wed Oct 21 2026
# 4.13 Wed Oct 21 2026
# 4.14 Wed Oct 21 2026
# 4.15 Wed Oct 21 2026
# 4.16 Wed Oct 21 2026
# 4.17 Wed Oct 21 2026
# 4.18 Wed Oct 21 2026
# 4.19 Fri Oct 23 2026
# 4.20 Sun Oct 25 2026
# 4.21 Tue Oct 27 2026
# 4.22 Wed Oct 28 2026
# 4.23 Wed Oct 28 2026
# 4.24 Wed Oct 28 2026
# 4.25 Fri Oct 30 2026
# 4.26 Sat Oct 31 2026
# 4.27 Tue Nov 3 2026
# 5.0 Thu Nov 5 2026
# laussy.org/wiki/showme
# F.P. Laussy
# Default program
PROGRAM="okular"
# The little cromulon: on a terminal, what showme says is said by a head one
# line tall. In Konsole it is a picture, the wiki's Cromulon.png (the head of
# {{show|wyg}}), made two character cells by one, as the terminal says they
# are, and kept in ~/.cache/showme: Konsole shrinks a big picture point by
# point, and that would speckle it. Elsewhere (tmux, Emacs, another terminal,
# no picture to hand) it is drawn in letters, Êïž”Ê, in its yellow. In CROM
# for what goes to stderr, in CROM1 for stdout; nothing when that is no
# terminal, so what reads showme's words reads them as they were.
# SHOWME_CROMULON=text asks for the letters, =off for no cromulon.
CROMULON_PNG=/var/www/html/laussywiki/images/8/8b/Cromulon.png
little_cromulon() {
local stat fd reply w h png tmp
CROM=
[ "$SHOWME_CROMULON" = off ] && return
if [ "${TERM:-dumb}" = dumb ]; then CROM='Êïž”Ê '; else CROM=$'\e[38;5;221mÊïž”Ê\e[0m '; fi
[ "$SHOWME_CROMULON" = text ] && return
[ "${KONSOLE_VERSION:-0}" -ge 230400 ] 2>/dev/null && [ -z "$TMUX$STY$INSIDE_EMACS" ] \
&& [ -r "$CROMULON_PNG" ] || return
# Konsole tells its cell size, if asked from the foreground: a job in the
# background that reads the terminal is stopped, and the shell would get
# the answer. Fields 3 and 6 after the name are our group and the
# terminal's foreground group.
read -r stat < /proc/$$/stat
set -- ${stat##*) }
[ "$3" = "$6" ] || return
{ exec {fd}<>/dev/tty; } 2>/dev/null || return
printf '\e[16t' >&$fd
IFS= read -rs -t 0.5 -d t -u $fd reply
exec {fd}>&-
[[ "$reply" =~ \[6\;([0-9]+)\;([0-9]+)$ ]] || return
h=${BASH_REMATCH[1]}; w=${BASH_REMATCH[2]}
png="${XDG_CACHE_HOME:-$HOME/.cache}/showme/cromulon-${w}x$h.png"
if ! [ "$png" -nt "$CROMULON_PNG" ]; then
mkdir -p "${png%/*}" && tmp=$(mktemp "$png.XXXXXX") || return
convert "$CROMULON_PNG" -trim +repage -resize "$((2 * w))x$h" -background none \
-gravity center -extent "$((2 * w))x$h" "PNG32:$tmp" 2>/dev/null \
&& mv -f "$tmp" "$png" || { rm -f "$tmp"; return; }
fi
CROM=$'\e]1337;File=inline=1;width=2;height=1:'$(base64 -w0 "$png")$'\a '
}
if [ -t 1 ] || [ -t 2 ]; then little_cromulon; fi
CROM1=; [ -t 1 ] && CROM1=$CROM
[ -t 2 ] || CROM=
# Lines from elsewhere (the reference reader), the cromulon in front of each
# that is a saying
said() {
local l
while IFS= read -r l; do
[[ "$l" == showme:* || "$l" == Error:* ]] && l=$CROM$l
printf '%s\n' "$l"
done <<< "$1" >&2
}
# Parse options with getopt
PARSED=$(getopt -o w:2psbvh -l with:,two,path,size,bib,version,help -n 'showme' -- "$@")
if [ $? -ne 0 ]; then
echo "${CROM}Error: Invalid option" >&2
exit 1
fi
eval set -- "$PARSED"
# Initialize two-files flag
TWO_FILES=false
# Initialize path-only flag
PATH_ONLY=false
# Size-only flag: how big the file is, nothing opened
SIZE_ONLY=false
# Bib-only flag: the BibTeX entry and the reference, typeset, no pdf opened
BIB_ONLY=false
# Whether the program was chosen with -w: a file's kind chooses it otherwise
WITH=false
# The options as given, to hand them on when one call is several keys
OPTS=()
# Process options
while true; do
case "$1" in
-w|--with)
PROGRAM="$2"
WITH=true
OPTS+=(-w "$2")
shift 2
;;
-2|--two)
TWO_FILES=true
OPTS+=(-2)
shift
;;
-p|--path)
PATH_ONLY=true
OPTS+=(-p)
shift
;;
-s|--size)
SIZE_ONLY=true
OPTS+=(-s)
shift
;;
-b|--bib)
BIB_ONLY=true
OPTS+=(-b)
shift
;;
-h|--help)
cat <<'HELP'
Usage: showme [-w PROG] [-2] [-p] [-s] [-b] WHAT [N]
Show me what you got: the paper, or else the file, WHAT.
A paper first, as one has it at hand:
chen22a its key in sci.bib, arXiv.bib, Books.bib
10.1021/acs.nanolett.1c04800 its DOI, also as doi:... or https://doi.org/...
'{{cite|chen22a}}' a wiki citation, as on the page
'Nano Lett. 22, 2023 (2022)' a reference, as printed
With no pdf filed, its BibTeX entry, and sci-hub offered. With no DOI on
record either, Crossref is asked for it, and showme offers to write it into
the entry (and, on azag, into the wiki's Template:Key, its date kept). With
-b, its BibTeX entry and below it the reference, typeset, whether filed or not.
An AF tag goes to its post, in the browser, †before đ if on both:
0K62vb Y-0K62vb Z-0K62vb on the local wiki, else on laussy.org
A tag of no post goes to the files that have it in their name (0LrpRb),
standing apart (01143 is not the tag in DSC01143.JPG), and in none to the
pages of the wiki that use it, the most first (00Yps):
showme asks which to open, unless showme TAG N said it. Where it asks
which, the list is a menu: â â and Enter, a number, q for none.
What the tag reads is said first. Neither post nor file: the tags of its
fabriad and triduum (its first two letters) with its last three in another
order or case are looked for (0Bomo: 0Boom), and showme asks before going
on with one.
Else a file, found by its name alone wherever it is (the locate database;
~ itself, searched, when the file is newer than the database; lls for one
made elsewhere since):
last-report-0Ksfub.pdf that file name
report-0Ksfub no extension: report-0Ksfub.pdf, .tex, ...
'report-*.pdf' kgon/report.pdf a glob; the end of a path
0LrpRb.pdf none called so: a part of the name,
landau-group-frames-0LrpRb.pdf (not in a
hidden folder nor /usr, /var...)
A stop or comma copied after any of these is dropped, and a bracket or
quote around it: chen22a. is chen22a, '(fig.png),' is fig.png; a ~ in
quotes is the home folder all the same. A whole path that is not there is
said so at once, with what of it is.
Several of that name: the newest opens and the others are listed, numbered;
showme WHAT N opens the N-th
Pictures open in gwenview, svg drawings in inkscape, pdfs in okular, text in
emacs, the rest in what the desktop opens them with; a program is not run.
Options:
-w, --with PROG open with PROG
-2, --two a second window too, on the next desktop
-p, --path say where, open nothing
-s, --size say how big, in bytes and roughly (79K), open nothing
-b, --bib a paper's BibTeX entry and its reference, typeset
-v, --version the cromulon, and the versions
-h, --help this
Quote what has a | ( or space in it. With no WHAT, the cromulon asks.
On a terminal a little cromulon, one line tall, says what showme says: a
picture in Konsole, Êïž”Ê elsewhere. SHOWME_CROMULON=text for the letters,
SHOWME_CROMULON=off for none.
In the panel, the cromulon on its black hole is showme too: a middle click
feeds it the selection, a click the clipboard, or drop a file on it; showme
runs unseen, in a terminal that comes up only if it asks or fails
(~/bin/showme-paste).
laussy.org/wiki/showme
HELP
exit 0
;;
-v|--version)
# Show me what you got: the cromulon head is its own version banner
sed -n '/^# #@=/,/^# *F\.P\. Laussy/p' "$0" | cut -c2-
exit 0
;;
--)
shift
break
;;
*)
echo "${CROM}Error: Invalid option" >&2
exit 1
;;
esac
done
# A number after the name picks one of several files of that name, numbered
# as they are listed: showme last-report-0Ksfub.pdf 3 opens the third. Two
# words only: a reference ends on a number too, but has three words at least
PICK=
STOPS='[.,;:!]'
if [ $# -eq 2 ] && [[ "$2" =~ ^([0-9]+)$STOPS*$ ]]; then
PICK=$((10#${BASH_REMATCH[1]}))
set -- "$1"
fi
# A reference typed without quotes arrives in pieces: put it back together
[ $# -gt 1 ] && set -- "$*"
# How many times $2 is in $1, in N
count_of() { local x=${1//"$2"/}; N=$(( (${#1} - ${#x}) / ${#2} )); }
# A name copied with the sentence it was in, "see chen22a." or "0LrpRb, then"
# or "(in /tmp/.../fig.png)," or "`fig.png`.", is that name: the stops and
# commas after it go, and the brackets and quotes around it, a closing one
# after it with none to open it in the name, an opening one before it with
# none to close it, or a pair that holds it all; showme carries on as if they
# had never been copied. Not when they belong to the name: a file there called
# so (.. or notes.), a bracket closed in it (rau14a (1).pdf, {{cite|chen22a}},
# Nano Lett. 22, 2023 (2022)), and not when they are all there is.
STOPPED='^(.*[^.,;:![:space:]])[.,;:![:space:]]+$'
unwrap() {
local s=$1 o c i more=true n_o n_c
local -a pairs=('(' ')' '[' ']' '{' '}' '<' '>' 'â' 'â' 'â' 'â' '«' '»' '"' '"' "'" "'" '`' '`')
while $more; do
more=false
if [[ "$s" =~ $STOPPED ]]; then
s=${BASH_REMATCH[1]}; more=true
fi
for ((i = 0; i < ${#pairs[@]}; i += 2)); do
o=${pairs[i]}; c=${pairs[i+1]}
[[ "$s" == ?*"$c" || "$s" == "$o"?* ]] || continue
count_of "$s" "$o"; n_o=$N
count_of "$s" "$c"; n_c=$N
if [ "$o" = "$c" ]; then
# a quote: one too many, or a pair around it all
if [ $((n_o % 2)) -eq 1 ]; then
if [[ "$s" == ?*"$c" ]]; then s=${s%"$c"}; else s=${s#"$o"}; fi
more=true
elif [ ${#s} -gt 2 ] && [[ "$s" == "$o"*"$c" ]]; then
s=${s#"$o"}; s=${s%"$c"}; more=true
fi
elif [[ "$s" == ?*"$c" ]] && [ $n_c -gt $n_o ]; then
s=${s%"$c"}; more=true
elif [[ "$s" == "$o"?* ]] && [ $n_o -gt $n_c ]; then
s=${s#"$o"}; more=true
elif [ "$o" != '{' ] && [ ${#s} -gt 2 ] && [[ "$s" == "$o"*"$c" ]]; then
# (fig.png): around it all; but {{cite|...}} is the citation
s=${s#"$o"}; s=${s%"$c"}; more=true
fi
done
done
printf '%s' "$s"
}
# A path given in quotes keeps its ~, which the shell has not made the home
# folder: '~/kgon/fig.png' is ~/kgon/fig.png all the same
home_of() {
if [ "$1" = '~' ]; then printf '%s' "$HOME"
elif [[ "$1" == '~/'* ]]; then printf '%s' "$HOME/${1#\~/}"
else printf '%s' "$1"; fi
}
set -- "$(home_of "$1")"
CUT=
if [ ! -e "$1" ]; then
NAME=$(unwrap "$1")
if [ "$NAME" != "$1" ]; then
# what was left out, said where a name is not found: â,â after it
BEFORE=${1%%"$NAME"*}; AFTER=${1#*"$NAME"}
CUT=" (â$BEFOREâ before it and â$AFTERâ after it left out)"
[ -z "$BEFORE" ] && CUT=" (â$AFTERâ after it left out)"
[ -z "$AFTER" ] && CUT=" (â$BEFOREâ before it left out)"
set -- "$(home_of "$NAME")"
fi
fi
# Show me what you got: called with nothing to show, the cromulon says it.
# Its head is the banner's, the one -v prints, with the mouth redrawn for
# each sound, and the words of the logo come as they are said.
cromulon() {
local -a art holes
local -A logo=()
local r s m h t out dur n prev=0 eol=$'\n' cw cy cg
mapfile -t art < <(sed -n '/^# #@=/,/^# *F\.P\. Laussy/p' "$0" | cut -c2-)
# A mouth: its upper lip (13 columns from column 11 of row 11), the rows
# it is open by, and its lower lip (11 columns from column 11, between the
# cheeks of row 12). The jaw drops as many rows as the mouth opens.
local -A lip=([ee]='@%*=-----=*%@' [ah]='@%*=-----=*%@' [oh]='@++=*%@%*=++@' [oo]='@++==*@*==++@')
local -A hole=([ee]='@.:.:.:.:.@' [ah]='@.:.:.:.:.@;% %; # # '
[oh]=' @* *@ ; @ @ ' [oo]=' @ @ ')
local -A low=([ee]='%=+=====+=%' [ah]=' .%=+=+=%. ' [oh]=' .%=+=%. ' [oo]=' .%+%. ')
# The logo right of the head, rows 2-10, as it stands once s words are
# said: Show, Me, what, you, got
t=${art[8]%%what*}; cw=$((${#t} - 26))
t=${art[9]%%you*}; cy=$((${#t} - 26))
t=${art[9]%%got*}; cg=$((${#t} - 26))
for ((s = 0; s <= 5; s++)); do
for ((r = 2; r <= 10; r++)); do
t=${art[r]:26}
if [ $r -le 6 ]; then
[ $s -ge 1 ] || t=
else
local me=${t:0:cw} rest=${t:cw}
[ $s -ge 2 ] || me=${me//?/ }
if [ $r -eq 8 ]; then
[ $s -ge 3 ] || rest=
elif [ $r -eq 9 ]; then
local you=${t:cy:cg-cy} got=${t:cg}
[ $s -ge 4 ] || you=${you//?/ }
[ $s -ge 5 ] || got=
rest=$you$got
fi
t=$me$rest
fi
logo[$s,$r]=$t
done
done
# The head saying a sound, with s words of the logo out
draw() {
out=
for ((r = 0; r <= 10; r++)); do out+="${art[r]:0:26}${logo[$s,$r]}$eol"; done
if [ "$1" = closed ]; then
for ((r = 11; r <= 17; r++)); do out+="${art[r]:0:26}$eol"; done
n=18
else
out+="${art[11]:0:11}${lip[$1]}${art[11]:24:2}$eol"
IFS=';' read -ra holes <<< "${hole[$1]}"
for h in "${holes[@]}"; do out+="${art[12]:0:11}$h${art[12]:22:2}$eol"; done
out+="${art[12]:0:11}${low[$1]}${art[12]:22:2}$eol"
for ((r = 13; r <= 17; r++)); do out+="${art[r]:0:26}$eol"; done
n=$((18 + ${#holes[@]}))
fi
[ $prev -gt 0 ] && printf '\e[%dA\r' $prev
printf '%s' "$out"
[ $prev -gt 0 ] && printf '\e[J'
prev=$n
}
# Not on a terminal, or too narrow a one, to move in: the head, said
if ! [ -t 1 ] || [ "$(tput cols 2>/dev/null || echo 80)" -lt 56 ]; then
s=5; prev=0; draw closed
return
fi
eol=$'\e[K\n'
printf '\e[?25l'
trap 'printf "\e[?25h"' EXIT
trap 'exit 130' INT TERM
s=0; draw closed; sleep 0.5
# Each word: how many are out once it is said, then its sounds and how
# long each lasts. SHOW (sh-ow) ME (m-ee) WHAT (wh-a-t) YOU (y-ou) GOT (g-o-t)
for t in '1 oo .07 oh .26 oo .08 closed .07' '2 closed .09 ee .24 closed .06' \
'3 oo .07 ah .24 ee .07 closed .06' '4 ee .07 oo .26 closed .06' \
'5 ah .09 oh .30 closed .1'; do
set -- $t
s=$1; shift
while [ $# -gt 0 ]; do
draw "$1"; sleep "$2"
shift 2
done
done
}
# Check if a search term is provided: with none, the cromulon asks for one
if [ -z "$1" ]; then
if [ "$PATH_ONLY" = true ] || [ "$SIZE_ONLY" = true ]; then
echo "${CROM}Error: No search term provided" >&2
exit 1
fi
cromulon
exit 0
fi
# Where papers live, where they land when the download goes wrong, and where
# they are on record even without a pdf
SCIDIR="$HOME/bib/sci"
TRANSITDIR="$HOME/bib/transit"
DOWNLOADS="$HOME/Downloads"
BIBFILES=("$HOME/bib/sci.bib" "$HOME/bib/arXiv.bib" "$HOME/bib/Books.bib")
# The bibliography's entry for a key, in ENTRY, and the file it is in, in
# BIBFILE. The key is compared as a string, not a pattern: a file name with
# a [ or a ( in it may come this way.
find_entry() {
ENTRY=
for BIBFILE in "${BIBFILES[@]}"; do
[ -r "$BIBFILE" ] || continue
ENTRY=$(awk -v key="$1" '
BEGIN { key = tolower(key) }
!found && /^@[A-Za-z]+[{(]/ {
k = $0
sub(/^@[A-Za-z]+[{(][ \t]*/, "", k); sub(/[ \t]*,.*$/, "", k)
if (tolower(k) == key) found = 1
}
found { print }
found && /^[})]/ { exit }
' "$BIBFILE")
[ -n "$ENTRY" ] && return 0
done
return 1
}
# ENTRY as the bibliography has it, and below it the reference as it reads
# in print: the authors, "A, B and C"; an article's title in quotes and its
# journal, volume, pages (year); a book's title in italics, with its
# publisher; a chapter, a paper of proceedings, a thesis each its own way.
# The @string journals (nl) written out, TeX's accents made letters (\"a is
# À), a field over several lines read whole. Bold and italics on a terminal
# only.
show_bib() {
local tty=
[ -t 1 ] && tty=1
printf '%s\n\n' "$ENTRY"
perl - "$ENTRY" "$tty" "${BIBFILES[@]}" <<'PERL'
use strict;
use warnings;
use utf8;
use Encode qw(decode_utf8);
use Unicode::Normalize qw(NFC);
binmode(STDOUT, ':utf8');
my ($entry, $tty, @bibs) = @ARGV;
$entry = decode_utf8($entry);
my ($B, $I, $O) = $tty ? ("\e[1m", "\e[3m", "\e[0m") : ('', '', '');
# The journals' @strings, nl = "Nano Lett."
my %string;
for my $bib (@bibs) {
open my $fh, '<:encoding(UTF-8)', $bib or next;
while (<$fh>) {
$string{lc $1} = $2 if /^\@string\s*\{\s*([^\s=]+)\s*=\s*"(.*)"\s*\}/i;
}
}
# The fields: {braced}, "quoted", a number or an @string, joined by #
my ($type) = $entry =~ /^\s*\@(\w+)/;
$type = lc($type // '');
(my $body = $entry) =~ s/^\s*\@\w+\s*[{(][^,]*,//s;
my %f;
while ($body =~ /\G[\s,]*([A-Za-z][\w-]*)\s*=\s*/gc) {
my ($name, $v) = (lc $1, '');
while (1) {
if ($body =~ /\G\{/gc) {
my $d = 1;
while ($d && $body =~ /\G([^{}]*)([{}])/gc) {
$v .= $1;
$d += $2 eq '{' ? 1 : -1;
$v .= $2 if $d;
}
} elsif ($body =~ /\G"((?:[^"{}]|\{[^{}]*\})*)"/gc) {
$v .= $1;
} elsif ($body =~ /\G([\w.:-]+)/gc) {
$v .= $string{lc $1} // $1;
} else {
last;
}
last unless $body =~ /\G\s*#\s*/gc;
}
$f{$name} //= $v;
}
# TeX made text: accents on their letters, \ss, \o..., dashes, no braces
my %acc = ('"' => "\x{308}", "'" => "\x{301}", '`' => "\x{300}", '^' => "\x{302}",
'~' => "\x{303}", '=' => "\x{304}", '.' => "\x{307}", u => "\x{306}",
v => "\x{30C}", H => "\x{30B}", c => "\x{327}", k => "\x{328}", r => "\x{30A}");
my %sym = (ss => 'Ă', o => 'Ăž', O => 'Ă', l => 'Ć', L => 'Ć', aa => 'Ă„', AA => 'Ă
',
ae => 'ĂŠ', AE => 'Ă', oe => 'Ć', OE => 'Ć', i => 'ı', j => 'È·');
sub detex {
my $s = shift // '';
$s =~ s/\\(["'`^~=.])\s*(?:\{\s*\\?([A-Za-z])\s*\}|\\?([A-Za-z]))/($2 \/\/ $3) . $acc{$1}/ge;
$s =~ s/\\([uvHckr])(?:\s*\{\s*\\?([A-Za-z])\s*\}|\s+\\?([A-Za-z]))/($2 \/\/ $3) . $acc{$1}/ge;
$s =~ s/\\(ss|aa|AA|ae|AE|oe|OE|[oOlLij])(?![A-Za-z])\s*(?:\{\})?/$sym{$1}/g;
$s =~ s/\\([&%\$#_])/$1/g;
$s =~ s/---/\x{2014}/g;
$s =~ s/--/\x{2013}/g;
$s =~ s/~|\\[ ,;]/ /g;
$s =~ s/\$//g;
$s =~ s/\\([A-Za-z]+)\s*/$1/g;
$s =~ s/[{}]//g;
$s =~ s/\s+/ /g;
$s =~ s/^ | $//g;
return NFC($s);
}
# "Coleman, A.J. and Yukalov, V.I." as "A.J. Coleman and V.I. Yukalov": a
# Jr. after the comma stays where it is
sub names {
my @a = grep { $_ ne '' } map { s/^\s+|\s+$//gr } split /\s+and\s+/, shift // '';
@a = map { /^([^,.]+),\s*([^,]+)$/ && $2 !~ /^(Jr|Sr|I{1,3}|IV)\.?$/i ? "$2 $1" : $_ } @a;
@a = map { $_ eq 'others' ? 'et al.' : detex($_) } @a;
return '' unless @a;
return $a[0] if @a == 1;
return join(', ', @a[0 .. $#a - 1]) . ' and ' . $a[-1];
}
my %t = map { $_ => detex($f{$_}) } keys %f;
$t{$_} //= '' for qw(title chapter volume);
my ($year, $pages) = ($t{year} // '', $t{pages} // '');
my $pp = $pages eq '' ? '' : ($pages =~ /\x{2013}/ ? "pp. $pages" : "p. $pages");
# (publisher, year), or what there is of it
my $pubyear = join(', ', grep { defined && $_ ne '' } $t{publisher} // $t{school}, $year);
$pubyear = "($pubyear)" if $pubyear ne '';
my @lines;
my $who = names($f{author});
$who = names($f{editor}) . ' (ed.)' if $who eq '' && ($f{editor} // '') ne '';
push @lines, $who if $who ne '';
if ($type eq 'book') {
my $vol = $t{volume} ne '' ? ", Vol. $t{volume}" : '';
push @lines, "$I$t{title}$O$vol" . ($pubyear ne '' ? " $pubyear" : '');
} elsif ($type eq 'inbook' && $t{chapter} ne '') {
push @lines, "$I\x{201C}$t{chapter}\x{201D}$O";
push @lines, "in $I$t{title}$O" . ($pubyear ne '' ? " $pubyear" : '') . ($pp ne '' ? ", $pp" : '');
} elsif ($type =~ /^(incollection|inproceedings|inbook)$/) {
push @lines, "$I\x{201C}$t{title}\x{201D}$O";
my $in = 'in ' . $I . ($t{booktitle} // $t{title}) . $O;
$in .= ', edited by ' . names($f{editor}) if ($f{editor} // '') ne '' && ($f{author} // '') ne '';
$in .= " $pubyear" if $pubyear ne '';
$in .= ", $pp" if $pp ne '';
push @lines, $in;
} elsif ($type =~ /^(phdthesis|mastersthesis)$/) {
push @lines, "$I\x{201C}$t{title}\x{201D}$O";
my $what = $type eq 'phdthesis' ? 'PhD thesis' : "Master's thesis";
push @lines, join(', ', $what, grep { $_ ne '' } $t{school} // '') . ($year ne '' ? " ($year)" : '');
} else {
push @lines, "$I\x{201C}$t{title}\x{201D}$O" if $t{title} ne '';
my $src = $t{journal} // $t{howpublished} // $t{note} // $t{publisher} // '';
my $ref = $src ne '' ? "$B$src$O" : '';
$ref .= " $B$t{volume}$O" if $t{volume} ne '';
$ref .= ($ref ne '' ? ', ' : '') . $pages if $pages ne '';
$ref .= ($ref ne '' ? ' ' : '') . "($year)" if $year ne '';
push @lines, $ref if $ref ne '';
}
# A comma at the end of each line but the last, a stop at the end of that,
# outside the italics, and not twice (et al.)
for my $i (0 .. $#lines) {
my $end = $i < $#lines ? ',' : '.';
$end = '' if $end eq '.' && $lines[$i] =~ /\.(?:\Q$O\E)?$/;
print " $lines[$i]$end\n";
}
PERL
}
# Not a paper, then: a file, any file, found by its name alone wherever the
# locate database has it, and opened with what shows its kind: gwenview for
# a picture, inkscape for an svg drawing, okular for a pdf, emacs for text,
# the desktop's choice for the rest. A name the database does not know (the
# file is newer than its last rebuild, at night or by lls) is looked for in
# the home folder itself, by name: a second or two, where rebuilding the
# database took ten and sudo's password. Several files of that name are
# versions: the newest is opened, the others are listed, numbered, and
# showme NAME N opens the N-th.
# The home folder as it is now, for a name the database does not know: one
# walk, by name alone (no file stat'ed), keeping every path any look of
# show_file's could want, in FRESH: those that have the name, the name's
# stem, or end in the path. bfs, if installed, walks with every core; find
# else. Left out: the stores of packages and caches no one names a file in,
# 1.46 of the 1.91 million entries of ~ on azag (uv's cache alone a million).
# Not -newer the database: a file moved since keeps its old time.
LOCATEDB=/var/lib/plocate/plocate.db
SEEKPRUNE=(.cache .local/share/flatpak .julia .npm)
fresh_walk() {
local name="${1#./}" stem test=-name pat p walker=find
local -a prune=()
if [[ "$name" == */* ]]; then
test=-path; pat="*/${name#/}*"
elif [[ "$name" == *[*?[]* ]]; then
pat="$name*"
else
stem=$name
[[ "$name" == ?*.* ]] && stem=${name%.*}
pat="*$stem*"; test=-iname
fi
for p in "${SEEKPRUNE[@]}"; do prune+=(-path "$HOME/$p" -prune -o); done
command -v bfs >/dev/null && walker=bfs
FRESH=()
mapfile -d '' FRESH < <($walker "$HOME" -xdev "${prune[@]}" $test "$pat" -print0 2>/dev/null)
}
# locate -0 [-b] -- PATTERN, from the database; or, FRESHLY set, from the
# walk of ~ in FRESH, matched as plocate does: a pattern with no * ? [ is a
# part of the name, -b matches the file's name, else its whole path
seek() {
if [ -z "$FRESHLY" ]; then locate "$@"; return; fi
local b= a f pat=${!#}
for a; do [ "$a" = -b ] && b=1; done
[[ "$pat" == *[*?[]* ]] || pat="*$pat*"
for f in "${FRESH[@]}"; do
if [ -n "$b" ]; then [[ "${f##*/}" == $pat ]]; else [[ "$f" == $pat ]]; fi \
&& printf '%s\0' "$f"
done
return 0
}
# The paths the database has for a name: its file name exactly; or a glob
# (report-*.pdf), which plocate matches whole; or, with a / in it, a path
# ending so (kgon/report.pdf)
locate_name() {
local name="${1#./}"
if [[ "$name" == */* ]]; then
seek -0 -- "*/${name#/}"
elif [[ "$name" == *[*?[]* ]]; then
seek -0 -b -- "$name"
else
# plocate's \name is no exact match, as mlocate's is: rule.png found
# Gema-enslaving-rule.png. Take what has it, keep what is it.
local f
seek -0 -b -- "$name" | while IFS= read -r -d '' f; do
[ "${f##*/}" = "$name" ] && printf '%s\0' "$f"
done
fi
}
# The files that exist (the database may be a day old), each once, newest
# first: in FOUND, with MTIME and SIZE alongside. Once means once, however
# many links lead to it: the 69 sci.bib of the projects are ~/bib/sci.bib,
# and that path is the one kept.
sort_files() {
local -A seen=() pos=()
local m s id f
FOUND=(); MTIME=(); SIZE=()
[ $# -eq 0 ] && return
while IFS=$'\t' read -r -d '' m s id f; do
[ -n "${seen[$f]}" ] && continue
seen[$f]=1
if [ -n "${pos[$id]}" ]; then
[ -L "${FOUND[pos[$id]]}" ] && [ ! -L "$f" ] && FOUND[pos[$id]]=$f
continue
fi
pos[$id]=${#FOUND[@]}
FOUND+=("$f"); MTIME+=("$m"); SIZE+=("$s")
done < <(printf '%s\0' "$@" | xargs -0 stat -L --printf '%Y\t%s\t%d:%i\t%n\0' -- 2>/dev/null \
| sort -z -t$'\t' -k1,1nr -k4)
}
# What shows a file: gwenview a picture, inkscape an svg drawing (to be
# worked on, not just looked at), okular a pdf, emacs text, the desktop's
# own choice everything else (a notebook, a video, a web page)
viewer_for() {
local f="$1" mime def
local editors='^(vim|nvim|gvim|emacs|emacsclient|kate|kwrite|org\.kde\.kate|org\.kde\.kwrite|gedit|org\.gnome\.gedit|nano|code)\.desktop$'
if [ -d "$f" ]; then echo dolphin; return; fi
if [ ! -s "$f" ]; then echo emacs; return; fi
mime=$(xdg-mime query filetype "$f" 2>/dev/null)
[ -z "$mime" ] && mime=$(file -b -L --mime-type -- "$f")
case "$mime" in
application/pdf|application/postscript|application/x-dvi|image/vnd.djvu*|application/epub+zip)
echo okular ;;
image/svg+xml|image/svg+xml-compressed)
echo inkscape ;;
image/*)
echo gwenview ;;
text/html|application/xhtml+xml)
echo xdg-open ;;
text/*)
echo emacs ;;
*)
# Text all the same, a script or some json, unless a program of its
# own is set for it (a Mathematica notebook is text too)
def=$(xdg-mime query default "$mime" 2>/dev/null)
if [ "$(file -b -L --mime-encoding -- "$f")" != binary ] && [[ -z "$def" || "$def" =~ $editors ]]; then
echo emacs
else
echo xdg-open
fi ;;
esac
}
# One line of the list: number, date, size, where
list_line() {
local i=$1 mark=" " on="" off="" size
if [ "$i" -eq "${PICK:-1}" ]; then
mark=" â "
[ -t 2 ] && { on=$'\e[1m'; off=$'\e[0m'; }
fi
if [ -d "${FILES[i-1]}" ]; then size=dir; else size=$(numfmt --to=iec "${SIZE[i-1]}"); fi
printf '%s%s%3d %(%Y-%m-%d %H:%M)T %6s %s%s\n' "$mark" "$on" "$i" "${MTIME[i-1]}" \
"$size" "${FILES[i-1]/#$HOME/\~}" "$off" >&2
}
# The name as it would be typed again
typed() {
if [[ "$1" =~ [^A-Za-z0-9._/+~-] ]]; then printf "'%s'" "$1"; else printf '%s' "$1"; fi
}
# Several FILES, numbered, the one opened marked, under a heading ($1) that
# says what they are, and how to open another one ($2 is the name typed).
# The first fifteen only; those in hidden folders, from number $3 + 1 on,
# after the others.
list_files() {
local n=${#FILES[@]} nvis=${3:-${#FILES[@]}} shown=15 i again=$2
[ "$SIZE_ONLY" = true ] && again="-s $2"
echo "${CROM}showme: $1; another one: showme $again N" >&2
for ((i = 1; i <= n && i <= shown; i++)); do
[ $i -eq $((nvis + 1)) ] && [ $nvis -gt 0 ] && echo " in hidden folders:" >&2
list_line $i
done
if [ $n -gt $shown ]; then
[ "${PICK:-1}" -gt $shown ] && { echo " ..." >&2; list_line "$PICK"; }
echo " and $((n - shown)) more: showme -p $2 | cat -n lists them all" >&2
fi
}
# Which of N lines, the list just printed: SHOWN of them, and EXTRA lines
# below them before the question. The list is the menu: â and â move the
# arrow (LINEFN i draws line i, the arrow on SEL), a number goes to that
# line, Enter takes the one with the arrow, q, n or Esc none. In CHOICE;
# false for none. Off a terminal, or on one too short to hold the list, a
# number is asked for instead, Enter for the first.
choose() {
local n=$1 shown=$2 extra=$3 linefn=$4 prompt=$5
local cur=1 typed= key rest rows=0 new
read -r rows _ < <(stty size </dev/tty 2>/dev/null)
if [ ! -t 0 ] || [ ! -t 2 ] || [ "${rows:-0}" -lt $((shown + extra + 2)) ]; then
read -r -p "${CROM}$prompt [1] " CHOICE
[ -z "$CHOICE" ] && CHOICE=1
[[ "$CHOICE" =~ ^[0-9]+$ ]] && [ "$CHOICE" -ge 1 ] && [ "$CHOICE" -le $n ]
return
fi
# Line i again, the arrow where it now is; the cursor kept where it was
redraw() {
local up=$((shown - $1 + extra + 1))
printf '\e7\e[%dA\r' $up >&2
SEL=$cur $linefn "$1"
printf '\e8' >&2
}
goto() {
local old=$cur
cur=$1
[ "$old" -ne "$cur" ] && { redraw "$old"; redraw "$cur"; }
}
printf '%s' "${CROM}$prompt ââ Enter, q none: " >&2
while IFS= read -rsn1 key; do
case "$key" in
'')
if [[ "$typed" =~ ^[0-9]+$ ]] && [ "$typed" -ge 1 ] && [ "$typed" -le $n ]; then CHOICE=$typed
else CHOICE=$cur; fi
[ -z "$typed" ] && printf '%s' "$CHOICE" >&2
echo >&2
return 0 ;;
$'\e')
rest=
IFS= read -rsn2 -t 0.05 rest
case "$rest" in
'[A'|'OA') typed=; [ $cur -gt 1 ] && goto $((cur - 1)) ;;
'[B'|'OB') typed=; [ $cur -lt $shown ] && goto $((cur + 1)) ;;
'') echo >&2; return 1 ;;
esac ;;
[0-9])
new=$typed$key
if [ "$((10#$new))" -ge 1 ] && [ "$((10#$new))" -le $n ]; then
typed=$new
printf '%s' "$key" >&2
[ "$((10#$typed))" -le $shown ] && goto $((10#$typed))
typed=$((10#$typed))
fi ;;
$'\x7f'|$'\b')
if [ -n "$typed" ]; then
typed=${typed%?}
printf '\b \b' >&2
[ -n "$typed" ] && [ "$typed" -le $shown ] && goto "$typed"
fi ;;
[qQnN]) echo >&2; return 1 ;;
esac
done
return 1
}
# A file that has a part of the name asked for, and is his: not in a hidden
# folder (what a program keeps of a file there, Okular's notes on a pdf,
# ~/.local/share/okular/docdata/40167125.Name.pdf.xml, is never what a part of
# a name meant: it opened in Chrome, as xml), nor in the system's own, /usr,
# /var... (Flatpak's store names its files by their hash, in hex)
in_sight() {
[[ "${1%/*}/" != */.* ]] && [[ ! "$1" =~ ^/(usr|var|opt|etc|boot|snap|srv|root|s?bin|lib[^/]*)/ ]]
}
# An AF tag in a file name stands apart: no letter or digit just before it,
# and after it at most its timezone letter (0LrpR in landau-group-frames-
# 0LrpRb.pdf), then none. Glued to more, it is chance: a tag-like run in
# the hex name of a file of Flatpak's store (a library, which KDE refused
# to launch), 01143 in DSC01143.JPG, 024px in 1024px-âŠ
tag_in_name() {
local tz='[A-Za-z]?'
[ ${#1} -gt 5 ] && tz=
[[ "${2##*/}" =~ (^|[^0-9A-Za-z])"$1"$tz([^0-9A-Za-z]|$) ]]
}
# A file with tag $1 in its name: in this very folder (a figure made a minute
# ago, that no locate database knows yet), or in sight in the database
tag_has_file() {
local f
while IFS= read -r f; do
tag_in_name "$1" "$f" && return 0
done < <(compgen -G "*$1*")
while IFS= read -r -d '' f; do
in_sight "$f" && tag_in_name "$1" "$f" && return 0
done < <(locate -0 -b -- "*$1*")
return 1
}
# One of the file names in NAMES (one a line) has tag $1
names_have() {
local n
while IFS= read -r n; do
[ -n "$n" ] && tag_in_name "$1" "$n" && return 0
done <<< "$NAMES"
return 1
}
# The .aux, .log... LaTeX leaves beside a report go out of raw (show_file's),
# unless they are all there is
drop_byproducts() {
local i
local -a byproducts=()
for i in "${!raw[@]}"; do
if [[ "${raw[i]}" =~ \.(aux|log|out|toc|lof|lot|bbl|blg|bcf|run\.xml|fls|fdb_latexmk|synctex\.gz|synctex|xdv|nav|snm|vrb|idx|ilg|ind)$ ]]; then
byproducts+=("$i")
fi
done
if [ ${#byproducts[@]} -lt ${#raw[@]} ]; then
for i in "${byproducts[@]}"; do unset 'raw[i]'; done
fi
}
# The last three letters of an AF tag may be typed in the wrong case
# (db-0SmIB for db-0SmIb): the rest of the name must match exactly. Sets
# CIFIXED to the name as it is, true if there is one other than $1. From the
# database (one locate -i call) or, "walk", from the walk of ~ in FRESH.
ci_fix() {
local name="$1" astag="$2" how="$3" stem ext head lstem lb pre act f
CIFIXED=
[[ "$name" == */* || "$name" == *[*?[]* ]] && return 1
stem=$name; ext=
[[ "$name" == ?*.* ]] && { stem=${name%.*}; ext=.${name##*.}; }
[ ${#stem} -ge 5 ] || return 1
head=${stem%???}; lstem=${stem,,}
while IFS= read -r -d '' f; do
f=${f##*/}; lb=${f,,}
[[ "$lb" == *"$lstem"* ]] || continue
pre=${lb%%"$lstem"*}; act=${f:${#pre}:${#stem}}
[ "${act:0:${#head}}" = "$head" ] && [ "$act" != "$stem" ] || continue
[ -z "$astag" ] || tag_in_name "$act" "$f" || continue
[ "$(LC_ALL=C; [[ "${f,,}" == "${lstem}${ext,,}"* || -n "$ext" ]] && echo y)" = y ] || continue
CIFIXED=$act$ext
echo "${CROM}showme: nothing called '$name', but '$CIFIXED' is (case of the last three letters)" >&2
return 0
done < <(if [ "$how" = db ]; then timeout 3 locate -0 -i -b -- "*$stem*$ext" 2>/dev/null
else printf '%s\0' "${FRESH[@]}"; fi)
return 1
}
# Find the file called $1 and make it FILES, with PROGRAM to show it; or,
# none being called so, those that have it in their name (PART is then
# true), standing apart in it if $2 says it is a tag. False when there is no
# such file, even with the database rebuilt.
show_file() {
local name="$1" astag="$2" pass i n shown=15 q stem ext f inpart built FRESHLY=
local -a FRESH=()
local -a raw=() order=()
# A whole path that is not there is nowhere: the locate database could only
# have it where it is not, and knows nothing of /tmp (pruned). Said at once,
# with what of the path is there, and no updatedb. A path from no folder
# there is (/kgon/report.pdf) is the end of one, as kgon/report.pdf.
local top=${name#/} dir
if [[ "$name" == /* ]] && [ ! -e "$name" ] && [ -d "/${top%%/*}" ]; then
dir=${name%/*}
while [ -n "$dir" ] && [ ! -d "$dir" ]; do dir=${dir%/*}; done
[ -z "$dir" ] && dir=/
if [ "$dir" = "${name%/*}" ]; then
echo "${CROM}Error: no file ${name/#$HOME/\~}$CUT: its folder is there, but not ${name##*/}" >&2
else
echo "${CROM}Error: no file ${name/#$HOME/\~}$CUT: of its path only ${dir/#$HOME/\~} is there" >&2
fi
echo " showme $(typed "${name##*/}") looks for that name anywhere" >&2
exit 1
fi
local CIDONE= pass=0
while [ $((++pass)) -le 2 ]; do
raw=(); PART=false; inpart=
if [[ "$name" == */* ]] && [ -e "$name" ]; then
# a path, ./notes.txt or ~/kgon/fig.png: that very file
raw=("$(realpath -s -- "$name")")
else
mapfile -d '' raw < <(locate_name "$name")
# made here today, too new for the database
[[ "$name" != */* ]] && [ -e "$name" ] && raw+=("$(pwd -P)/$name")
# no extension given: report-0Ksfub is report-0Ksfub.pdf or .tex, but
# not the .aux or .log LaTeX leaves beside them unless that is all
if [ ${#raw[@]} -eq 0 ] && [[ "${name##*/}" != ?*.* ]]; then
mapfile -d '' raw < <(locate_name "$name.*")
drop_byproducts
fi
# Nothing of that name: a part of one. An AF tag mostly, 0LrpRb.pdf for
# landau-group-frames-0LrpRb.pdf, and 0LrpRb for all its files, the
# .pdf, the .png, the .svg. An extension given stays, *0LrpRb*.pdf.
# Three letters at least, and no glob or path: those say what they want.
# Files in sight only (in_sight), and a tag standing apart (tag_in_name)
if [ ${#raw[@]} -eq 0 ] && [[ "$name" != */* && "$name" != *[*?[]* ]]; then
stem=$name; ext=
[[ "$name" == ?*.* ]] && { stem=${name%.*}; ext=.${name##*.}; }
if [ ${#stem} -ge 3 ]; then
inpart=", not even in part,"
raw=()
while IFS= read -r -d '' f; do
in_sight "$f" && { [ -z "$astag" ] || tag_in_name "$stem" "$f"; } && raw+=("$f")
done < <(seek -0 -b -- "*$stem*$ext")
while IFS= read -r f; do
[ -e "$f" ] && { [ -z "$astag" ] || tag_in_name "$stem" "$f"; } && raw+=("$(pwd -P)/$f")
done < <(compgen -G "*$stem*$ext")
[ -z "$ext" ] && drop_byproducts
PART=true
fi
fi
fi
sort_files "${raw[@]}"
[ ${#FOUND[@]} -gt 0 ] && break
if [ $pass -eq 2 ]; then
# The walk of ~ may have it in another case
if [ -z "$CIDONE" ] && ci_fix "$name" "$astag" walk; then name=$CIFIXED; CIDONE=1; pass=1; continue; fi
# Nor in ~: a file made elsewhere since the database needs lls, said
# (a tag's files are in ~)
[ -z "$astag" ] && echo "${CROM}showme: none in ~ either; one made outside ~ since ${built:-the database}: lls, then showme again" >&2
return 1
fi
# Wrong case in the last three letters of a tag (db-0SmIB for db-0SmIb)?
# One locate -i call, no walk of ~: the name is put right, and looked up
# again, once.
if ci_fix "$name" "$astag" db; then name=$CIFIXED; pass=0; continue; fi
# Not in the database: too new for it, perhaps. Look in ~ as it is now
built=$(LC_ALL=C date -r "$LOCATEDB" '+%a %H:%M' 2>/dev/null)
echo "${CROM}showme: nothing called '$name'$inpart in the locate database${built:+ ($built)}, looking in ~ itself..." >&2
fresh_walk "$name"
FRESHLY=1
done
# Copies in hidden folders (.git, .cache, the Trash, .ocr-backup) go after
# the others, the newest of those that are in plain sight opened first
order=()
for i in "${!FOUND[@]}"; do [[ "${FOUND[i]%/*}/" != */.* ]] && order+=("$i"); done
local nvis=${#order[@]}
for i in "${!FOUND[@]}"; do [[ "${FOUND[i]%/*}/" == */.* ]] && order+=("$i"); done
FILES=(); local -a mt=() sz=()
for i in "${order[@]}"; do FILES+=("${FOUND[i]}"); mt+=("${MTIME[i]}"); sz+=("${SIZE[i]}"); done
MTIME=("${mt[@]}"); SIZE=("${sz[@]}")
n=${#FILES[@]}
q=$(typed "$name")
local one="one file only is called $q" are="files are called $q"
if [ "$PART" = true ]; then
one="no file is called $q, one only in part"
are="files are called $q in part"
fi
if [ -n "$PICK" ] && { [ "$PICK" -lt 1 ] || [ "$PICK" -gt $n ]; }; then
if [ $n -eq 1 ]; then
echo "${CROM}Error: $one, no number $PICK: ${FILES[0]/#$HOME/\~}" >&2
else
echo "${CROM}Error: $n $are, no number $PICK:" >&2
local p=$PICK; PICK=0
for ((i = 1; i <= n; i++)); do list_line $i; done
PICK=$p
fi
exit 1
fi
# Several: say so, and how to open another one. One, but only in part: say
# which it is
if [ $n -gt 1 ] && [ "$PATH_ONLY" != true ]; then
list_files "$n $are, the newest first" "$q" $nvis
elif [ "$PART" = true ] && [ "$PATH_ONLY" != true ]; then
echo "${CROM}showme: $q is ${FILES[0]/#$HOME/\~}" >&2
fi
[ "$WITH" = true ] || PROGRAM=$(viewer_for "${FILES[${PICK:-1} - 1]}")
return 0
}
# Show FILES with PROGRAM: the one picked, the first one by default (or with
# -p, say where they are)
open_files() {
local f="${FILES[${PICK:-1} - 1]}"
# A program is not shown by the desktop but run, and KDE will not ("For
# security reasons, launching executables is not allowed in this context")
if [ "$PATH_ONLY" != true ] && [ "$SIZE_ONLY" != true ] && [ "$PROGRAM" = xdg-open ] && [ -f "$f" ] && [ -x "$f" ] \
&& [[ "$(xdg-mime query filetype "$f" 2>/dev/null)" =~ ^application/(x-executable|x-pie-executable|x-sharedlib|x-msdownload|vnd\.microsoft\.portable-executable)$ ]]; then
echo "${CROM}Error: ${f/#$HOME/\~} is a program, which showme does not run" >&2
exit 1
fi
if [ "$SIZE_ONLY" = true ]; then
# How big it is, the one picked: in bytes, then roughly, as ls -lh has
# it. A folder is what is in it; a link, what it leads to. With -p, where
# it is first.
local b
[ "$PATH_ONLY" = true ] && printf '%s\n' "$f"
if [ -d "$f" ]; then b=$(du -sbD -- "$f" 2>/dev/null | cut -f1); else b=$(stat -L -c %s -- "$f"); fi
printf '%s bytes\n%s\n' "$b" "$(numfmt --to=iec "$b")"
elif [ "$PATH_ONLY" = true ]; then
# Just print the location(s) of the matching file(s), don't open anything
if [ -n "$PICK" ]; then printf '%s\n' "$f"; else printf '%s\n' "${FILES[@]}"; fi
elif [ "$TWO_FILES" = true ]; then
# Open the first instance on the current desktop
$PROGRAM "$f" &>/dev/null &
# Open the same file again in a second instance
$PROGRAM "$f" &>/dev/null &
# Wait briefly for the second window to appear
sleep 0.5
# Get the window ID of the second instance
WINDOW_ID=$(wmctrl -l | grep -i "$PROGRAM" | tail -n 1 | awk '{print $1}')
# Get the current desktop number
CURRENT_DESKTOP=$(wmctrl -d | grep '*' | awk '{print $1}')
# Calculate the next desktop number
TOTAL_DESKTOPS=$(wmctrl -d | wc -l)
NEXT_DESKTOP=$(( (CURRENT_DESKTOP + 1) % TOTAL_DESKTOPS ))
# Move the second window to the next desktop
wmctrl -i -r "$WINDOW_ID" -t "$NEXT_DESKTOP"
else
# Original behavior: open the first file. Dolphin is given a window of
# its own: handed a folder it may raise a window of a running instance
# on some other desktop, and nothing shows where showme was run.
if [ "$PROGRAM" = dolphin ]; then
dolphin --new-window "$f" &>/dev/null &
else
$PROGRAM "$f" &>/dev/null &
fi
fi
}
# A wiki citation does as well as a key, pasted as it stands on the page:
# showme {{bromberg09a}} is showme bromberg09a, and so are {{cite|bromberg09a}},
# {{onlinecite|âŠ}}, {{citer|âŠ}}, {{citet|âŠ}} and {{doi|âŠ}}, which goes on to the
# DOI lookup below. The braces go, the template name goes, a period or comma
# stuck to the closing braces goes. Several keys in one citation, {{cite|a,b}}
# or {{cite|a|b}}, are shown one after the other, the options handed on.
# Quote anything with a pipe in it: to the shell, {{cite|a}} is a pipeline.
if [[ "$1" == *"{{"* ]]; then
if [[ "$1" != *"}}"* ]]; then
echo "${CROM}Error: '$1' has no closing braces: quote the citation, the shell took its | for a pipe" >&2
exit 1
fi
INNER=$(printf '%s' "$1" | sed -E 's/^[[:space:]]*\{\{//; s/\}\}[[:space:]]*[.,;:]*[[:space:]]*$//')
case "$INNER" in
[Cc]ite\|*|[Oo]nlinecite\|*|[Cc]iter\|*|[Cc]itet\|*|[Cc]ite/mark\|*|[Dd]oi\|*) INNER="${INNER#*|}" ;;
esac
KEYS=()
while IFS= read -r k; do
k="${k#"${k%%[![:space:]]*}"}"; k="${k%"${k##*[![:space:]]}"}"
[ -n "$k" ] && KEYS+=("$k")
done < <(printf '%s\n' "$INNER" | tr '|,' '\n\n')
if [ ${#KEYS[@]} -eq 0 ]; then
echo "${CROM}Error: no key in '$1'" >&2
exit 1
fi
if [ ${#KEYS[@]} -gt 1 ]; then
echo "${CROM}showme: ${#KEYS[@]} keys in '$1', one after the other: ${KEYS[*]}" >&2
STATUS=0
for k in "${KEYS[@]}"; do
"$0" "${OPTS[@]}" "$k" || STATUS=$?
done
exit $STATUS
fi
set -- "${KEYS[0]}" "${@:2}"
fi
# A DOI does as well as a key: showme 10.1038/1781449a0 is showme purcell56a.
# The bibliography knows which entry carries it (in its url or doi field) and
# that entry's key takes over from here, since the pdf is filed under the key.
# The forms a DOI is pasted in are accepted too: doi:âŠ, a doi.org address, the
# reference list's trailing period.
LOOKUP="$1"
LOOKUP=$(printf '%s' "$LOOKUP" | sed -E 's#^(doi:|https?://(dx\.)?doi\.org/)##I; s/[.,;]+$//')
if [[ "$LOOKUP" =~ ^10\.[0-9]{4,9}/. ]]; then
KEY=
for BIBFILE in "${BIBFILES[@]}"; do
[ -r "$BIBFILE" ] || continue
KEY=$(awk -v doi="$LOOKUP" '
BEGIN { doi = tolower(doi); n = length(doi) }
# the key of the entry we are in
/^[ \t]*@[A-Za-z]+[{(]/ {
key = $0
sub(/^[ \t]*@[A-Za-z]+[{(][ \t]*/, "", key); sub(/[ \t]*,.*$/, "", key)
next
}
# DOIs are case-insensitive, and this one must fill its field up to the
# closing brace, quote or comma: 10.1038/nphys327 is not 10.1038/nphys3272
key != "" && tolower($0) ~ /^[ \t]*(doi|url)[ \t]*=/ {
line = tolower($0)
i = index(line, doi)
if (i && substr(line, i + n, 1) ~ /^[}",; \t]?$/) { print key; exit }
}
' "$BIBFILE")
[ -n "$KEY" ] && break
done
if [ -z "$KEY" ]; then
echo "${CROM}Error: no entry in the bibliography carries the DOI $LOOKUP (doi2bib -w $LOOKUP would add one)" >&2
exit 1
fi
echo "${CROM}showme: $LOOKUP is $KEY" >&2
set -- "$KEY" "${@:2}"
fi
# A reference as a paper prints it does as well: showme 'Nano Letters 22,
# 2023â2029 (2022).' is showme chen22a. So does the ACS order, 'Nano Lett.
# 2022, 22, 2023â2029', and a whole reference with its authors and title in
# front. Volume and first page find the entries, and the journal must be the
# entry's own under one of its spellings: the @string, a name iso4 sends to
# it, the key itself (PRL) -- words compared the way j2bib compares them,
# "Lett." for "Letters". Keys and DOIs never hold a space; a reference always
# does. Quote it: to the shell, (2022) is a subshell. What is no reference,
# or none the bibliography has, may still be a file: my notes.txt.
if [[ "$1" =~ [[:space:]] ]]; then
REFERR=$(mktemp)
KEY=$(perl - "$1" "$HOME/bib/doi2bib/iso4" "${BIBFILES[@]}" <<'PERL' 2>"$REFERR"
use strict;
use warnings;
use Encode qw(decode_utf8);
use Unicode::Normalize qw(NFKD);
binmode(STDERR, ':utf8');
my ($given, $iso4, @bibs) = @ARGV;
$given = decode_utf8($given);
(my $shown = $given) =~ s/^\s+|[\s.,;]+$//g;
# j2bib's small words, dropped when two journal names are compared
my %small = map { $_ => 1 } qw(of the and in on for an at to
de du der die das des la le les et fur und di del della);
# Cut the reference into journal, volume, first page and year
my $r = $given;
$r =~ s/'{2,}|\*//g; # wiki and markdown emphasis
$r =~ s/[\x{2010}-\x{2015}\x{2212}]/-/g; # en and em dashes, minus signs
$r =~ s/\s+/ /g;
$r =~ s/^ | ?[.,;]+ ?$//g;
my $vl = qr/[A-Za-z]?\d+[A-Za-z]?|[IVXLC]+/; # 22, 35A, T123, XVII
my $pg = qr/[A-Za-z0-9]*\d[A-Za-z0-9]*(?: ?\([A-Za-z]\))?/; # 2023, L041101, eaau6969, 241306(R)
my ($pre, $vol, $first, $year);
if ($r =~ /^(.*?)[ ,]+(\d{4}) ?, ?($vl)(?: ?\([^()]*\))? ?, ?($pg)(?: ?-+ ?$pg)?$/) {
($pre, $year, $vol, $first) = ($1, $2, $3, $4); # Nano Lett. 2022, 22, 2023-2029
} elsif ($r =~ /^(.*?)[ ,]+($vl)(?: ?\([^()]*\))? ?[,:] ?($pg)(?: ?-+ ?$pg)?(?: ?\((?:[^()]* )?(\d{4})\))?$/) {
($pre, $vol, $first, $year) = ($1, $2, $3, $4); # Nano Lett. 22, 2023-2029 (2022)
} else {
print STDERR "Error: '$shown' is no key, DOI or reference showme can read "
. "(Journal Volume, Page (Year))\n";
exit 1;
}
# Volumes and pages compared on what matters: 22 = {22}, 35A = 35,
# {A 242} = 242, L041101 = 041101, 241306(R) = 241306, 2023--2029 starts at
# 2023
sub volume_of {
my $v = shift // '';
return lc $1 if $v =~ /^[{"\s]*([IVXLC]+)[}"\s]*$/;
return $v =~ /(\d+)/ ? $1 : '';
}
sub page_of {
my $p = lc(shift // '');
$p =~ s/^[{"\s]+//;
$p =~ s/\s*[-\x{2013}].*//s;
$p =~ s/[^a-z0-9]//g;
$p =~ s/^[a-z]+|[a-z]+$//g;
return $p;
}
# j2bib's words_of and word_match: a name cut into the words that carry it,
# and two words for the same one -- equal, a truncation (Lett. for Letters),
# a contraction keeping the letters in order (Natl. for National). A lone
# letter at a name's end is a series letter, equal only to itself: Phys. Rev.
# A is not Phys. Rev. Applied. Before that it may be a word: J. for Journal,
# R. for Roy. (j2bib allows the front only, it has no volume to go by.)
sub words_of {
my $s = NFKD(shift);
$s =~ s/\p{Mn}//g;
$s =~ s/&/ /g;
$s =~ s/\\[A-Za-z]+//g;
$s =~ s/[\\'"`^~]//g;
$s = lc $s;
$s =~ s/[^a-z0-9]+/ /g;
return grep { $_ ne '' && !$small{$_} } split ' ', $s;
}
sub word_match {
my ($a, $b, $first) = @_;
return 1 if $a eq $b;
my ($short, $long) = length $a <= length $b ? ($a, $b) : ($b, $a);
return 0 if length($short) == 1 && !$first;
return 1 if index($long, $short) == 0;
return 0 unless length($short) >= 3 && substr($short, 0, 1) eq substr($long, 0, 1);
my $i = 0;
for my $c (split //, $short) {
$i = index($long, $c, $i);
return 0 if $i < 0;
$i++;
}
return 0.5;
}
# Do these words, what stands before the volume, end on this spelling of the
# journal? Their last words are compared, so authors and title in front do no
# harm. 1 when every word is equal or truncated, 0.5 when one needed a
# contraction ("Res." would do for "Reviews": only looks alike), 0 when not.
my @pre = words_of($pre);
sub ends_on {
my ($w, $spelling) = @_;
my @s = words_of($spelling);
return 0 unless @s && @$w;
if (@$w >= @s) {
my @tail = @$w[$#$w - $#s .. $#$w];
my $score = 1;
for my $i (0 .. $#s) {
my $m = word_match($tail[$i], $s[$i], $i < $#s);
$score = $m if $m < $score;
last unless $m;
}
return $score if $score;
}
# an acronym: PRL for Phys. Rev. Lett.
return (@s >= 2 && $w->[-1] =~ /^[a-z]{2,}$/
&& $w->[-1] eq join('', map { substr($_, 0, 1) } @s)) ? 1 : 0;
}
# The spellings each journal key goes by: its @string, and every name iso4
# sends straight to it
my (%string, %spell);
if (open my $fh, '<:encoding(UTF-8)', $bibs[0]) {
while (<$fh>) {
$string{$1} = $2 if /^\@string\s*\{\s*([^\s=]+)\s*=\s*"(.*)"\s*\}/i;
}
}
if (open my $fh, '<:encoding(UTF-8)', $iso4) {
while (<$fh>) {
next unless m{^s/(.*)/([^/]*)/\s*$};
my ($name, $k) = ($1, $2);
$name =~ s/\\//g;
push @{ $spell{$k} }, $name if exists $string{$k};
}
}
# Every entry with a volume and pages
my @entries;
for my $bib (@bibs) {
open my $fh, '<:encoding(UTF-8)', $bib or next;
(my $file = $bib) =~ s{.*/}{};
my $e;
while (<$fh>) {
if (/^\s*\@[A-Za-z]+\s*[{(]\s*([^\s,=]+)\s*,/) {
$e = { key => $1, file => $file };
push @entries, $e;
} elsif ($e && /^\s*(journal|volume|pages|year)\s*=\s*(.*?)\s*,?\s*$/i) {
$e->{lc $1} //= $2;
}
}
}
# A journal is a key (nl) or written out, braced: {Opt. Express}. Only the
# outer braces go, Preu{\ss}ischen keeps its own.
sub journal_of {
my $j = shift // '';
return $j =~ /^[{"]/ ? $j =~ s/^[{"]|[}"]$//gr : $string{$j} // $j;
}
sub spellings_of {
my $j = shift // '';
return ($j =~ /^[{"]/) ? (journal_of($j)) : ($string{$j} // (), @{ $spell{$j} || [] });
}
# The entry's journal, under any of its spellings or its key (PRL). A series
# letter the bibliography keeps in the volume, {A 242} in a Proc. Roy. Soc,
# stands before the volume in the reference: Proc. R. Soc. A 242.
sub journal_ok {
my $e = shift;
my $j = $e->{journal} // '';
my @w = (\@pre);
my ($letter) = $e->{volume} =~ /^[{"\s]*([A-Za-z]+)\s*\d/;
push @w, [ @pre[0 .. $#pre - 1] ] if $letter && @pre > 1 && lc $letter eq $pre[-1];
my $best = 0;
for my $w (@w) {
return 1 if $j !~ /^[{"]/ && @$w && $w->[-1] eq lc $j;
for (spellings_of($j)) {
my $s = ends_on($w, $_);
$best = $s if $s > $best;
}
}
return $best;
}
sub cited {
my $e = shift;
my $y = ($e->{year} // '') =~ s/[{}"]//gr;
return "$e->{key} = " . journal_of($e->{journal}) . " " . ($e->{volume} =~ s/[{}"]//gr)
. ", " . ($e->{pages} =~ s/[{}"]//gr) . ($y ne '' ? " ($y)" : "")
. ($e->{file} eq 'sci.bib' ? "" : " in $e->{file}");
}
my @same = grep { defined $_->{volume} && defined $_->{pages}
&& volume_of($_->{volume}) eq volume_of($vol)
&& page_of($_->{pages}) eq page_of($first) } @entries;
$_->{score} = journal_ok($_) for @same;
my @hits = grep { $_->{score} } @same;
# The same paper entered twice, or another with its volume and page by
# mistake: the year may still tell which
if (@hits > 1 && defined $year) {
my @y = grep { ($_->{year} // '') =~ s/[{}"\s]//gr eq $year } @hits;
@hits = @y if @y == 1;
}
if (@hits == 1) {
my $e = $hits[0];
print STDERR "showme: $shown is $e->{key}\n";
print STDERR "showme: matched loosely, to ", cited($e) =~ s/^\S+ = //r, "\n"
if $e->{score} < 1;
my $y = ($e->{year} // '') =~ s/[{}"]//gr;
print STDERR "showme: but the bibliography has it in $y\n"
if defined $year && $y ne '' && $y ne $year;
print "$e->{key}\n";
exit 0;
}
if (@hits > 1) {
print STDERR "Error: several entries are $shown, say which:\n";
print STDERR " ", cited($_), "\n" for @hits;
exit 3;
}
print STDERR "Error: no entry in the bibliography is $shown\n";
if (@same) {
print STDERR " the same volume and first page, but another journal:\n";
print STDERR " ", cited($_), "\n" for @same;
}
exit 2;
PERL
)
STATUS=$?
REFMSG=$(cat "$REFERR"); rm -f "$REFERR"
# 0: the paper; 2: a reference, but to no paper the bibliography has; 3:
# several papers, say which; 1: no reference, perhaps a file
[ -n "$REFMSG" ] && [ $STATUS -ne 1 ] && said "$REFMSG"
[ $STATUS -ge 2 ] && exit 1
if [ $STATUS -eq 1 ]; then
# -b asks for a paper's entry: no file is opened for it
[ "$BIB_ONLY" = true ] && { said "$REFMSG"; exit 1; }
show_file "$1" && { open_files; exit 0; }
said "$REFMSG"
echo " and no file is called that either, whole or in part" >&2
exit 1
fi
set -- "$KEY" "${@:2}"
fi
# With -b, a paper's entry and its reference, typeset, and nothing opened:
# the key as it is, or as the DOI, citation or reference above made it. A
# key such as rau14a is asked for here, before it could be read as a tag.
if [ "$BIB_ONLY" = true ]; then
if ! find_entry "$1"; then
echo "${CROM}Error: no entry '$1' in ${BIBFILES[*]##*/}" >&2
exit 1
fi
[ "${BIBFILE##*/}" = sci.bib ] || echo "${CROM}showme: $1 is in ${BIBFILE##*/}" >&2
show_bib
exit 0
fi
# An AF tag does as well: showme 0K62vb is the post of đ or †it names,
# shown in the browser at its place on its month page. †comes before đ,
# since a cross-posted entry carries the one tag on both and †has it whole;
# Y-0K62vb or Z-0K62vb asks for that branch. The local wiki is asked first,
# laussy.org after (a post from the phone is there until syncy brings it),
# its pages all at once, and saying so, since that takes a few seconds.
# A word is a tag only when af reads it as a moment of Anno Fabri, a year
# ahead at most: a key such as li08a would be in 2048.
AFTAG=${1#\#}
BRANCHES="Z Y"
BARE=true
if [[ "$AFTAG" =~ ^([YZ])-(.+)$ ]]; then
BRANCHES=${BASH_REMATCH[1]}
AFTAG=${BASH_REMATCH[2]}
BARE=false
fi
if [[ "$AFTAG" =~ ^[0-9A-Za-z]{5}[A-Za-z]?$ ]] && READ=$(AF -b "$AFTAG" 2>/dev/null) \
&& T=${READ%%$'\t'*} && [[ "$T" =~ ^[0-9]+$ ]] \
&& [ "$T" -ge 1787004000 ] && [ "$T" -le $(( $(date +%s) + 31622400 )) ]; then
WHEN=${READ##*$'\t'}
LABEL=$AFTAG; [ "$BARE" = true ] || LABEL=$BRANCHES-$AFTAG
# What the tag reads, before anything else: a tag copied wrong is seen at
# once to be of the wrong day
echo "${CROM}showme: $LABEL is $WHEN" >&2
# The pages, "B TITLE", that a post of the moment af reads (its 3rd
# column) is on: the month of the day the stamp reads, in the tag's own
# offset, and the months either side, for a post near the turn of one;
# †before đ, the tag's own month before its neighbours
month_titles() {
local b d m y
[[ "$1" =~ ^[A-Za-z]+\ [0-9]+\ ([A-Za-z]+)\ ([0-9]{4}), ]] || return
m=${BASH_REMATCH[1]}; y=${BASH_REMATCH[2]}
for b in $BRANCHES; do
for d in "" "-1 month" "+1 month"; do
d=$(LC_ALL=C date -d "15 $m $y $d" '+%B_(%Y)')
if [ "$b" = Z ]; then echo "Z Z/$d"; else echo "Y $d"; fi
done
done
}
# The post is <blockquote id="Z-0K62vb"> in the page's wikitext. The pages
# of a site are fetched together, in one curl. laussy.org takes a second or
# two over each page, and six of them one after the other were 18 s of
# silence: ASKING, when set, says beforehand that they are being asked for.
# Each page is fetched once from each site (0 here, 1 laussy.org), and the
# posts on it go into POST["S B-TAG"] = its title.
SITES=(http://azag/laussywiki/index.php https://laussy.org)
declare -A FETCHED=() POST=()
NPAGE=0
PAGES=$(mktemp -d)
trap 'rm -rf "$PAGES"' EXIT
fetch_pages() {
local s=$1 bt f id i
local -a fetch=() got=()
shift
for bt; do
[ -n "${FETCHED[$s $bt]}" ] && continue
FETCHED[$s $bt]=1
f=$PAGES/$((NPAGE++))
fetch+=("${SITES[s]%/index.php}/index.php?title=${bt#* }&action=raw" -o "$f")
got+=("$bt" "$f")
done
[ ${#fetch[@]} -eq 0 ] && return
[ "$s" -eq 1 ] && [ -n "$ASKING" ] && echo "${CROM}showme: $ASKING" >&2
curl -s -m 10 -Z --parallel-immediate -A 'Mozilla/5.0 (X11; Linux x86_64) showme' "${fetch[@]}"
for ((i = 0; i < ${#got[@]}; i += 2)); do
bt=${got[i]}; f=${got[i+1]}
[ -s "$f" ] || continue
while IFS= read -r id; do
[ -z "${POST[$s $id]}" ] && POST[$s $id]=${bt#* }
done < <(grep -o "id=\"${bt%% *}-[0-9A-Za-z]*\"" "$f" | sed 's/^id="//; s/"$//')
done
}
# Is TAG a post on site S, †before đ: URL, and B, TITLE, SITE and HIT
post_of() {
local s=$1 t=$2 b
for b in $BRANCHES; do
if [ -n "${POST[$s $b-$t]}" ]; then
B=$b; TITLE=${POST[$s $b-$t]}; SITE=${SITES[s]}; HIT=$t
URL="$SITE/$TITLE#$B-$t"
return 0
fi
done
return 1
}
mapfile -t TITLES < <(month_titles "$WHEN")
URL=
FILES_TRIED=false
fetch_pages 0 "${TITLES[@]}"
if ! post_of 0 "$AFTAG"; then
# No post here, but a file in plain sight carries the tag in its name
# (landau-group-frames-0LrpRb.pdf): the tag is the file's, laussy.org
# need not be asked. In this very folder first: a figure made a minute
# ago is here, and no locate database knows it yet.
if [ "$BARE" = true ] && tag_has_file "$AFTAG"; then
FILES_TRIED=true
show_file "$AFTAG" tag && { open_files; exit 0; }
fi
ASKING="no post $LABEL on the local wiki, asking laussy.org (a few seconds)..."
fetch_pages 1 "${TITLES[@]}"
post_of 1 "$AFTAG"
fi
# No post: the files with the tag in their name, if any
if [ -z "$URL" ] && [ "$BARE" = true ] && [ "$FILES_TRIED" = false ]; then
show_file "$AFTAG" tag && { open_files; exit 0; }
fi
# Nor a file: the pages of the wiki that use it, read off wikigrep's copy
# of every page (wikigrep brings it up to date first, from recentchanges):
# "TAG<tab>N<tab>TITLE<tab>URL", N the times the tag is on the page, the
# most first. A tag is used where it stands alone, not inside a longer
# word: 0Boom is not in 0Boomb. The URL opens the page at its first use.
wiki_uses() {
python3 - "$@" <<'PY'
import json, os, re, sys, urllib.parse
tags = sys.argv[1:]
rx = re.compile(r'(?<![0-9A-Za-z])(' + '|'.join(map(re.escape, tags)) + r')(?![0-9A-Za-z])')
uses = []
with open(os.path.expanduser('~/.cache/wikigrep/pages.jsonl'), encoding='utf-8') as f:
for line in f:
if not any(t in line for t in tags):
continue
page = json.loads(line)
n = {}
for m in rx.finditer(page.get('text') or ''):
n[m.group(1)] = n.get(m.group(1), 0) + 1
uses += [(t, k, page['title']) for t, k in n.items()]
for t, k, title in sorted(uses, key=lambda u: (tags.index(u[0]), -u[1], u[2])):
url = ('http://azag/laussywiki/index.php/'
+ urllib.parse.quote(title.replace(' ', '_'), safe="/:()_-.,'!*~")
+ '#:~:text=' + t)
print(f'{t}\t{k}\t{title}\t{url}')
PY
}
# The pages of USES: listed, and showme asks which to open in the browser,
# Enter for the first, the one with the most uses; showme TAG N has said
# it already. Off a terminal, the list only. With -p, where they are.
show_uses() {
local t=$1 n=${#USES[@]} total=0 i k title url mark on off line
local -a K=() TITLE=() U=()
for line in "${USES[@]}"; do
IFS=$'\t' read -r _ k title url <<<"$line"
K+=("$k"); TITLE+=("$title"); U+=("$url"); total=$((total + k))
done
if [ -n "$PICK" ] && { [ "$PICK" -lt 1 ] || [ "$PICK" -gt $n ]; }; then
echo "${CROM}Error: $t is on $n page$([ $n -gt 1 ] && echo s) of the wiki, no number $PICK" >&2
exit 1
fi
if [ "$SIZE_ONLY" = true ]; then
echo "${CROM}Error: $t is in no file, only on pages of the wiki: no size" >&2
exit 1
fi
if [ "$PATH_ONLY" = true ]; then
if [ -n "$PICK" ]; then printf '%s\n' "${U[PICK-1]}"; else printf '%s\n' "${U[@]}"; fi
exit 0
fi
n_times() { if [ "$1" -eq 1 ]; then echo once; else echo "$1 times"; fi; }
use_line() {
local i=$1 mark=" " on="" off=""
if [ $i -eq "${SEL:-${PICK:-1}}" ]; then mark=" â "; [ -t 2 ] && { on=$'\e[1m'; off=$'\e[0m'; }; fi
printf '%s%s%3d %4s %s%s\n' "$mark" "$on" $i "${K[i-1]}Ă" "${TITLE[i-1]}" "$off" >&2
}
if [ $n -eq 1 ]; then
echo "${CROM}showme: $t is on no post and in no file, but on the wiki, $(n_times $total), on ${TITLE[0]}" >&2
else
echo "${CROM}showme: $t is on no post and in no file, but on $n pages of the wiki, $(n_times $total):" >&2
for ((i = 1; i <= n && i <= 15; i++)); do use_line $i; done
if [ $n -gt 15 ]; then
[ "${PICK:-1}" -gt 15 ] && { echo " ..." >&2; use_line "$PICK"; }
echo " and $((n - 15)) more: showme -p $t | cat -n lists them all" >&2
fi
fi
if [ -z "$PICK" ]; then
if [ ! -t 0 ]; then
if [ $n -eq 1 ]; then
echo "${CROM}showme: showme $t 1 opens it" >&2
else
echo "${CROM}showme: showme $t N opens the N-th" >&2
fi
exit 0
fi
if [ $n -eq 1 ]; then
read -r -p "${CROM}Open ${TITLE[0]}? [Y/n] " ANSWER
case "$ANSWER" in [nN]*) exit 0 ;; esac
PICK=1
else
choose $n $((n < 15 ? n : 15)) $((n > 15 ? 1 : 0)) use_line "Open which?" || exit 0
PICK=$CHOICE
fi
fi
[ "$WITH" = true ] || PROGRAM=google-chrome
$PROGRAM "${U[${PICK:-1} - 1]}" &>/dev/null &
exit 0
}
USES=()
if [ -z "$URL" ] && [ "$BARE" = true ] && [ -n "$(wikigrep -l -F "$AFTAG" 2>/dev/null)" ]; then
mapfile -t USES < <(wiki_uses "$AFTAG")
[ ${#USES[@]} -gt 0 ] && show_uses "$AFTAG"
fi
# Neither: the tags near it, a tag copied wrong. Near is of the same
# fabriad and triduum, the first two letters, and the last three in
# another order (0Bomo is 0Boom) or another case (v for V), the timezone
# letter kept: 47 at most, nearest first, the fewest letters moved, then
# the fewest cases changed. All of the same 2.76 days, so of the month
# pages in hand already; af reads them in one go, and those of a moment
# to come (a day of grace) are dropped.
near_tags() {
perl -e '
my $tag = shift;
my ($era, $core, $tz) = (substr($tag, 0, 2), substr($tag, 0, 5), substr($tag, 5));
sub orders { my ($done, @rest) = @_; return ($done) unless @rest;
map { my @r = @rest; my ($x) = splice(@r, $_, 1); orders($done . $x, @r) } 0 .. $#rest }
my (%seen, @near);
for my $p (orders("", split //, substr($core, 2))) {
my @v = ($era);
for my $c (split //, $p) {
@v = map { my $h = $_; ($h . $c, $c =~ /[A-Za-z]/ ? ($h . ($c eq lc($c) ? uc($c) : lc($c))) : ()) } @v;
}
for my $v (@v) {
next if $v eq $core || $seen{$v}++;
my ($moved, $cased) = (0, 0);
for my $i (2 .. 4) {
my ($x, $y) = (substr($v, $i, 1), substr($core, $i, 1));
if (lc($x) ne lc($y)) { $moved++ } elsif ($x ne $y) { $cased++ }
}
push @near, [$moved, $cased, $v . $tz];
}
}
print "$$_[2]\n" for sort { $$a[0] <=> $$b[0] || $$a[1] <=> $$b[1] || $$a[2] cmp $$b[2] } @near;
' "$1"
}
if [ -z "$URL" ]; then
NEAR=(); NEARWHEN=(); HITS=()
mapfile -t CAND < <(near_tags "$AFTAG")
if [ ${#CAND[@]} -gt 0 ]; then
mapfile -t CREAD < <(AF -b "${CAND[@]}" 2>/dev/null)
LIMIT=$(( $(date +%s) + 86400 ))
if [ ${#CREAD[@]} -eq ${#CAND[@]} ]; then
for i in "${!CAND[@]}"; do
T=${CREAD[i]%%$'\t'*}
[[ "$T" =~ ^[0-9]+$ ]] && [ "$T" -ge 1787004000 ] && [ "$T" -le $LIMIT ] || continue
NEAR+=("${CAND[i]}"); NEARWHEN+=("${CREAD[i]##*$'\t'}")
done
fi
fi
if [ ${#NEAR[@]} -gt 0 ]; then
# laussy.org not asked yet if a file seemed to carry the tag
ASKING="asking laussy.org for tags near $LABEL..."
fetch_pages 1 "${TITLES[@]}"
# The files in plain sight with one of them in their name: one locate,
# blind to case, for each order of the last three, and this very folder
NAMES=
if [ "$BARE" = true ]; then
declare -A FOLD=()
for t in "${NEAR[@]}"; do FOLD[${t,,}]=1; done
while IFS= read -r -d '' f; do
in_sight "$f" && [ -e "$f" ] && NAMES+="${f##*/}"$'\n'
done < <(for t in "${!FOLD[@]}"; do
locate -0 -i -b -- "*$t*"
(shopt -s nocaseglob; compgen -G "*$t*" | tr '\n' '\0')
done)
fi
# The pages of the wiki that use one, the copy brought up to date
# just above
declare -A NPAGES=()
if [ "$BARE" = true ]; then
while IFS=$'\t' read -r t _; do
NPAGES[$t]=$(( ${NPAGES[$t]:-0} + 1 ))
done < <(wiki_uses "${NEAR[@]}")
fi
HITS=(); HITWHEN=(); HITWHERE=()
for i in "${!NEAR[@]}"; do
t=${NEAR[i]}
if post_of 0 "$t"; then where="a post on ${TITLE//_/ }"
elif post_of 1 "$t"; then where="a post on ${TITLE//_/ }, on laussy.org only"
elif [ "$BARE" = true ] && names_have "$t"; then where="in a file name"
elif [ -n "${NPAGES[$t]}" ]; then
where="on ${NPAGES[$t]} page$([ "${NPAGES[$t]}" -gt 1 ] && echo s) of the wiki"
else continue
fi
HITS+=("$t"); HITWHEN+=("${NEARWHEN[i]}"); HITWHERE+=("$where")
done
URL=
fi
rm -rf "$PAGES"
if [ ${#HITS[@]} -eq 0 ]; then
NEARNOT=
[ ${#NEAR[@]} -gt 0 ] && NEARNOT=", nor is any tag of its triduum with its last three in another order or case"
echo "${CROM}Error: $AFTAG is the tag of no post of đ or â€, here or on laussy.org$([ "$BARE" = true ] && echo ', nor of any file, nor on any page of the wiki')$NEARNOT" >&2
exit 1
fi
# A tag near it is a guess: said, and asked about before showme goes on
# with it, Enter for yes. Off a terminal, nothing is done, and the error
# says what it might have been.
near_label() { if [ "$BARE" = true ]; then echo "$1"; else echo "$BRANCHES-$1"; fi; }
if [ ${#HITS[@]} -eq 1 ]; then
echo "${CROM}showme: nothing carries $LABEL, but $(near_label "${HITS[0]}") does, ${HITWHERE[0]}: ${HITWHEN[0]}" >&2
else
echo "${CROM}showme: nothing carries $LABEL, but ${#HITS[@]} tags near it do:" >&2
near_line() {
local i=$1 mark=" " on="" off=""
if [ $i -eq "${SEL:-1}" ]; then mark=" â "; [ -t 2 ] && { on=$'\e[1m'; off=$'\e[0m'; }; fi
printf '%s%s%3d %s %s, %s%s\n' "$mark" "$on" $i "$(near_label "${HITS[i-1]}")" \
"${HITWHEN[i-1]}" "${HITWHERE[i-1]}" "$off" >&2
}
for i in "${!HITS[@]}"; do near_line $((i + 1)); done
fi
if [ ! -t 0 ]; then
echo "${CROM}Error: $LABEL is the tag of nothing; showme $(near_label "${HITS[0]}") if that is the one" >&2
exit 1
fi
if [ ${#HITS[@]} -eq 1 ]; then
read -r -p "${CROM}Go on with $(near_label "${HITS[0]}")? [Y/n] " ANSWER
case "$ANSWER" in [nN]*) exit 1 ;; esac
k=0
else
choose ${#HITS[@]} ${#HITS[@]} 0 near_line "Go on with which?" || exit 1
k=$((CHOICE - 1))
fi
t=${HITS[k]}
if [[ "${HITWHERE[k]}" == "in a file name" ]]; then
show_file "$t" tag && { open_files; exit 0; }
exit 1
fi
if [[ "${HITWHERE[k]}" == *" of the wiki" ]]; then
mapfile -t USES < <(wiki_uses "$t")
show_uses "$t"
fi
post_of 0 "$t" || post_of 1 "$t"
fi
ONLINE=
[ "$SITE" = https://laussy.org ] && ONLINE=", on laussy.org only"
echo "${CROM}showme: $B-$HIT is on ${TITLE//_/ }$ONLINE" >&2
if [ "$SIZE_ONLY" = true ]; then
echo "${CROM}Error: $B-$HIT is a post, not a file: no size" >&2
exit 1
elif [ "$PATH_ONLY" = true ]; then
printf '%s\n' "$URL"
else
[ "$WITH" = true ] || PROGRAM=google-chrome
$PROGRAM "$URL" &>/dev/null &
fi
exit 0
fi
# Get matching PDF files: in the library, ~/bib/sci, transit and books (not
# the hidden .ocr-backup), by the file's name. Not by its path: every path
# holds "laussy", and when any pdf with "bib" anywhere in its path would do,
# showme laussy opened the first of 2608 (footnote-bibentry-0KOohb.pdf, a
# Bible, an ERC bibliography...), and showme Microcavities another. A part of
# a key still does (showme rau14), and the paper of that very key comes first.
FILES=()
while IFS= read -r -d '' f; do
FILES+=("$f")
done < <(locate -0 -b -- "$1" | grep -z "^$HOME/bib/.*\.[Pp][Dd][Ff]$" | while IFS= read -r -d '' f; do
[[ "$f" == "$HOME/bib/"* && "$f" != */.* && "${f,,}" == *.pdf && -e "$f" ]] || continue
b=${f##*/}
r=1; [ "${b,,}" = "${1,,}.pdf" ] && r=0
printf '%s\t%s\t%s\0' "$r" "$b" "$f"
done | sort -z -t$'\t' -k1,1 -k2,2 -k3,3 | cut -z -f3-)
# A name with a dot is a file's whole name (keys have none): chen22a.pdf is
# the paper, but report.pdf is no part of annual-report.pdf in transit
if [[ "$1" == *.* ]]; then
KEEP=()
for f in "${FILES[@]}"; do [ "${f##*/}" = "$1" ] && KEEP+=("$f"); done
FILES=("${KEEP[@]}")
fi
# locate's database is rebuilt once a night, so a paper filed today is invisible
# to it. Look at the real directories before giving up on them.
if [ ${#FILES[@]} -eq 0 ]; then
while IFS= read -r f; do
FILES+=("$f")
done < <(find "$SCIDIR" "$TRANSITDIR" -maxdepth 1 -iname "*$1*.pdf" 2>/dev/null | sort)
fi
# No pdf filed under that name, and no entry in the bibliography either: no
# paper, then. A file of that name, anywhere?
if [ ${#FILES[@]} -eq 0 ] && ! find_entry "$1"; then
show_file "$1" && { open_files; exit 0; }
echo "${CROM}Error: '$1'$CUT is no paper (no PDF, no BibTeX entry) and no file either, whole or in part" >&2
exit 1
fi
# Still nothing: it may have been downloaded to the wrong place. Fetch it back.
# Only for a key the bibliography has: report-0Ksfub.pdf in Downloads is no
# paper to file away in transit.
if [ ${#FILES[@]} -eq 0 ] && [ -d "$DOWNLOADS" ]; then
DL=()
# an exact <key>.pdf wins outright; only then try a looser match
while IFS= read -r f; do
DL+=("$f")
done < <(find "$DOWNLOADS" -maxdepth 1 -iname "$1.pdf" 2>/dev/null)
if [ ${#DL[@]} -eq 0 ]; then
while IFS= read -r f; do
DL+=("$f")
done < <(find "$DOWNLOADS" -maxdepth 1 -iname "*$1*.pdf" 2>/dev/null | sort)
fi
if [ ${#DL[@]} -gt 1 ]; then
echo "${CROM}Error: several candidates in $DOWNLOADS, file the right one yourself:" >&2
printf ' %s\n' "${DL[@]}" >&2
exit 1
fi
if [ ${#DL[@]} -eq 1 ]; then
SRC="${DL[0]}"
NAME="${SRC##*/}"
# "rau14a (1).pdf" downloaded twice is still rau14a.pdf
case "$NAME" in
"$1 ("*").pdf") NAME="$1.pdf" ;;
esac
DEST="$TRANSITDIR/$NAME"
if [ -e "$DEST" ]; then
echo "${CROM}showme: $DEST already exists, leaving $SRC alone" >&2
FILES=("$DEST")
elif mv -n "$SRC" "$DEST"; then
echo "${CROM1}showme: filed $NAME from Downloads into $TRANSITDIR"
FILES=("$DEST")
else
echo "${CROM}Error: could not move $SRC to $TRANSITDIR" >&2
exit 1
fi
fi
fi
# No PDF anywhere: maybe the paper is only on record in the bibliography.
# Show its BibTeX entry instead, and offer the alternative supplier.
if [ ${#FILES[@]} -eq 0 ]; then
if [ "$PATH_ONLY" = true ] || [ "$SIZE_ONLY" = true ]; then
echo "${CROM}Error: No matching PDF files found" >&2
exit 1
fi
echo "${CROM1}showme: no PDF for '$1', but the bibliography knows it (${BIBFILE##*/}):"
echo
show_bib
echo
# A one-line field: strip "name = " and the {âŠ}/"âŠ" wrapping
bibfield() {
printf '%s\n' "$ENTRY" | grep -i -m1 "^[[:space:]]*$1[[:space:]]*=" \
| sed -E 's/^[^=]*=[[:space:]]*//; s/^[{"]//; s/[}"]?,?[[:space:]]*$//'
}
# The DOI hides as doi:10.xxxx/⊠inside the url (or doi) field
DOI=$(printf '%s\n' "$ENTRY" | grep -o -m1 'doi:10\.[^},"]*' | head -1 | sed 's/^doi://')
[ -z "$DOI" ] && DOI=$(bibfield doi | grep -o '10\.[^},"]*')
# The key as the bibliography spells it, not as it was typed (the lookup
# above is case-insensitive)
KEY=$(printf '%s\n' "$ENTRY" | head -1 | sed -E 's/^@[A-Za-z]+[{(][[:space:]]*//; s/[[:space:]]*,.*$//')
[ -z "$KEY" ] && KEY="$1"
# No DOI on record (kira97a): Crossref is asked for one, by the title, the
# first author and the year. Its answer is trusted only when the work it
# names has the entry's volume and first page, or its very title within a
# year of the entry's. Prints "DOI<TAB>what it is", the DOI spelled as the
# publisher spells it (PhysRevLett, not Crossref's physrevlett); status 1
# when nothing fits, 2 when Crossref does not answer (said on stderr).
crossref_doi() {
python3 - "$ENTRY" "$CROM" <<'PY'
import difflib, json, re, sys, unicodedata, urllib.parse, urllib.request
entry, crom = sys.argv[1:3]
def field(name):
m = re.search(r'^\s*' + name + r'\s*=\s*', entry, re.I | re.M)
if not m:
return ''
s = entry[m.end():]
if s[:1] == '{':
depth = 0
for i, c in enumerate(s):
depth += (c == '{') - (c == '}')
if depth == 0:
return s[1:i]
return ''
if s[:1] == '"':
return s[1:].split('"', 1)[0]
return re.match(r'[^,}\n]*', s).group(0).strip()
def plain(s): # TeX out: \"a is a, ``Boser'' is Boser
s = re.sub(r'\\[A-Za-z]+\s*', '', s)
s = re.sub(r'\\[^A-Za-z]', '', s)
s = re.sub(r"``|''|[{}$~]", ' ', s)
return ' '.join(s.split())
def norm(s):
s = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode()
return ' '.join(re.findall(r'[a-z0-9]+', s.lower()))
def first_page(p):
return re.split(r'[-ââ,\s]+', p.strip())[0].lstrip('0').lower() if p else ''
title, year, volume = plain(field('title')), field('year'), field('volume')
page = first_page(field('pages'))
first = plain(re.split(r'\s+and\s+', field('author') or field('editor'))[0])
family = first.split(',')[0] if ',' in first else (first.split() or [''])[-1]
if not title:
sys.exit(1)
q = urllib.parse.urlencode({'query.bibliographic': f'{title} {year}'.strip(),
'query.author': family, 'rows': 5})
req = urllib.request.Request('https://api.crossref.org/works?' + q,
headers={'User-Agent': 'showme (https://laussy.org/wiki/showme)'})
try:
with urllib.request.urlopen(req, timeout=15) as r:
items = json.load(r)['message']['items']
except Exception as e:
print(f'{crom}showme: Crossref did not answer ({e})', file=sys.stderr)
sys.exit(2)
best = None
for it in items:
ct = (it.get('title') or [''])[0]
sim = difflib.SequenceMatcher(None, norm(title), norm(ct)).ratio()
cv = str(it.get('volume', ''))
cp = first_page(it.get('page') or it.get('article-number') or '')
cy = ((it.get('issued') or {}).get('date-parts') or [[None]])[0][0]
same_place = bool(volume and page and cv == volume and cp == page)
same_title = sim >= 0.9 and (not year.isdigit() or not cy or abs(cy - int(year)) <= 1)
if (same_place or same_title) and (best is None or (same_place, sim) > best[0]):
best = ((same_place, sim), it, ct, cv, cp, cy, same_place)
if not best:
sys.exit(1)
_, it, ct, cv, cp, cy, same_place = best
doi = it['DOI']
for u in [((it.get('resource') or {}).get('primary') or {}).get('URL', '')] + \
[l.get('URL', '') for l in it.get('link') or []]:
i = u.lower().find(doi.lower())
if i >= 0:
doi = u[i:i + len(doi)]
break
authors = it.get('author') or []
who = (authors[0].get('family', '') + (' et al.' if len(authors) > 1 else '')) if authors else ''
journal = (it.get('short-container-title') or it.get('container-title') or [''])[0]
why = 'same volume and page' if same_place else 'same title'
print(f'{doi}\t{ct}. {who}, {journal} {cv}, {it.get("page") or cp} ({cy}); {why}')
PY
}
# The DOI written into the entry as doi2bib writes it, url = {doi:âŠ} as its
# last field (doi = {âŠ} when it has a url already), the file before kept as
# .bak like doi2bib's. Not while Emacs holds the file with unsaved changes
# (its lock .#sci.bib): then the line to add is only said.
add_doi() {
python3 - "$BIBFILE" "$KEY" "$1" "$CROM" "$CROM1" <<'PY'
import os, re, shutil, sys, tempfile
path, key, doi, crom, crom1 = sys.argv[1:6]
name = os.path.basename(path)
lock = os.path.join(os.path.dirname(path), '.#' + name)
data = open(path, 'rb').read()
lines = data.splitlines(keepends=True)
start = next((i for i, l in enumerate(lines)
if re.match(rb'@[A-Za-z]+[{(]\s*' + re.escape(key.encode()) + rb'\s*,', l, re.I)), None)
if start is None:
print(f'{crom}showme: {key} is not in {name} after all, nothing written', file=sys.stderr)
sys.exit(1)
end = next((i for i in range(start + 1, len(lines)) if lines[i][:1] in (b'}', b')')), None)
if end is None:
print(f'{crom}showme: the entry {key} in {name} has no end, nothing written', file=sys.stderr)
sys.exit(1)
if any(lines[i].lstrip()[:1] == b'@' for i in range(start + 1, end)):
print(f'{crom}showme: the entry {key} in {name} does not end on a line of its own, nothing written', file=sys.stderr)
sys.exit(1)
body = b''.join(lines[start:end])
if re.search(rb'doi:10\.|^\s*doi\s*=', body, re.I | re.M):
print(f'{crom}showme: {key} has a DOI in {name} already, nothing written', file=sys.stderr)
sys.exit(1)
new = (b' doi = {%s}\n' if re.search(rb'^\s*url\s*=', body, re.I | re.M) else b' url = {doi:%s}\n') % doi.encode()
if os.path.lexists(lock):
print(f'{crom}showme: {name} is open in Emacs with unsaved changes, so it is yours to add, in {key}:', file=sys.stderr)
print(f'{crom} {new.decode().strip()}', file=sys.stderr)
sys.exit(1)
last = max(i for i in range(start, end) if lines[i].strip())
eol = lines[last][len(lines[last].rstrip(b'\r\n')):] or b'\n'
text = lines[last].rstrip(b'\r\n')
if not text.rstrip().endswith(b','):
lines[last] = text + b',' + eol
lines.insert(end, new)
shutil.copy2(path, path + '.bak')
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path), prefix='.' + name + '.')
with os.fdopen(fd, 'wb') as f:
f.write(b''.join(lines))
shutil.copymode(path, tmp)
os.replace(tmp, path)
print(f'{crom1}showme: {new.decode().strip()} is in {key}, {name} (the file before kept as {name}.bak)')
PY
}
# The wiki's Template:<Key> made again from the bibliography, so that its
# citation carries the DOI link: bib2wiki's text, written by silent-lwedit,
# the page's date kept (a regenerated citation is no news). Only on azag,
# where the wiki is; not when there is no such template; not when it shows
# an uploaded pdf ({{pdf|File:âŠ}}), which bib2wiki knows nothing of.
wiki_template() {
local out title text cur tmp
out=$(bib2wiki -dry "$KEY" 2>/dev/null) || { echo "${CROM}showme: bib2wiki -dry $KEY failed, the wiki left alone" >&2; return 1; }
title=$(printf '%s\n' "$out" | head -1 | sed 's/\x1b\[[0-9;]*m//g')
text=$(printf '%s\n' "$out" | tail -n +2)
case "$title" in Template:*) ;; *) echo "${CROM}showme: bib2wiki said no template for $KEY, the wiki left alone" >&2; return 1 ;; esac
if [ ! -d /var/www/html/laussywiki/maintenance ]; then
echo "${CROM}showme: the wiki is on azag; there: bib2wiki -dry $KEY | tail -n +2 > t && silent-lwedit -f t '$title'" >&2
return 1
fi
cur=$(curl -s -G 'http://azag/laussywiki/api.php' --data-urlencode "titles=$title" \
--data 'action=query&prop=revisions&rvprop=content&rvslots=main&format=json&formatversion=2' |
python3 -c 'import json,sys
p=json.load(sys.stdin)["query"]["pages"][0]
print("" if p.get("missing") else p["revisions"][0]["slots"]["main"]["content"], end="")')
if [ -z "$cur" ]; then
echo "${CROM}showme: no $title on the wiki (bib2wiki $KEY makes it)" >&2
return 1
fi
if [[ "$cur" == *"{{pdf|"* ]]; then
echo "${CROM}showme: $title shows an uploaded pdf, which a remake would drop: left alone" >&2
return 1
fi
tmp=$(mktemp)
printf '%s\n' "$text" > "$tmp"
if out=$(silent-lwedit -f "$tmp" -s "DOI from Crossref (showme)" "$title" 2>&1); then
echo "${CROM1}showme: $title now links its DOI (date kept)"
else
echo "${CROM}showme: silent-lwedit could not write $title:" >&2
printf '%s\n' "$out" | tail -3 >&2
fi
rm -f "$tmp"
}
if [ -z "$DOI" ] && [ -t 0 ]; then
CR=$(crossref_doi)
case $? in
0)
DOI=${CR%%$'\t'*}
echo "${CROM1}showme: no DOI on record, but Crossref has $DOI:"
echo " ${CR#*$'\t'}"
read -r -p "${CROM}Add it to $KEY in ${BIBFILE##*/}, and to the wiki? [Y/n] " ANSWER
case "$ANSWER" in
[nN]*) ;;
*) add_doi "$DOI" && wiki_template ;;
esac
;;
1) echo "${CROM}showme: no DOI on record, and none at Crossref that fits $KEY" >&2 ;;
esac
fi
if [ -n "$DOI" ] && [ -t 0 ]; then
# The download will want the key's name: park it in the clipboard so the
# save box is a paste away.
read -r -p "${CROM}See sci huh? [Y/n] " ANSWER
case "$ANSWER" in
[nN]*) ;;
*)
if printf '%s' "$KEY.pdf" | xclip -selection clipboard &>/dev/null; then
echo "${CROM1}showme: $KEY.pdf is in the clipboard, for the save box"
fi
google-chrome "https://sci-hub.ru/$DOI" &>/dev/null &
;;
esac
elif [ -n "$DOI" ]; then
# No terminal to ask in (a "! showme" from Claude Code, a pipe): the
# address is shown instead, to be clicked
echo "${CROM1}showme: sci huh: https://sci-hub.ru/$DOI"
fi
exit 0
fi
# Several pdfs for a part of a key, showme laussy for laussy04b and the 19
# others: listed, numbered, as files are, and a number picks one. Copies of
# the very key.pdf, in sci and in transit, are one paper and go unlisted.
if [ ${#FILES[@]} -gt 1 ]; then
mapfile -t MTIME < <(stat -L -c %Y -- "${FILES[@]}")
mapfile -t SIZE < <(stat -L -c %s -- "${FILES[@]}")
q=$(typed "$1")
if [ -n "$PICK" ] && { [ "$PICK" -lt 1 ] || [ "$PICK" -gt ${#FILES[@]} ]; }; then
echo "${CROM}Error: ${#FILES[@]} pdfs match $q, no number $PICK:" >&2
p=$PICK; PICK=0
for ((i = 1; i <= ${#FILES[@]}; i++)); do list_line $i; done
exit 1
fi
SAME=true
for f in "${FILES[@]}"; do [ "${f##*/}" = "${FILES[0]##*/}" ] || SAME=false; done
if [ "$SAME" != true ] && [ "$PATH_ONLY" != true ]; then
if [ "${FILES[0]##*/}" = "$1.pdf" ]; then ORDER="the paper of that very key first"; else ORDER="by name"; fi
list_files "${#FILES[@]} pdfs in ~/bib match $q, $ORDER" "$q"
fi
elif [ -n "$PICK" ] && [ "$PICK" -ne 1 ]; then
echo "${CROM}Error: one pdf only matches '$1', no number $PICK: ${FILES[0]}" >&2
exit 1
fi
open_files
Bash autocompletion, since 23 October (2026), is in ~/.local/share/bash-completion/completions/showme: files and folders as ls completes them, a folder ending in / for the next Tab to go inside it, and the keys of the papers in ~/bib/sci and ~/bib/transit once a letter is typed (an empty Tab lists the folder, as with ls, not 2600 keys). It is read by ~/.bash_completion, which bash-completion reads last, so that it wins over the older showme-completion.bash of /etc/bash_completion.d/, which completed keys only and still does for bib2wiki.
# Tab completion for showme â v1.2 â Fri Oct 30 2026
#
# What showme takes, as ls would complete it: files and folders, a folder
# ending in / and no space so the next Tab goes inside it, ~ and spaces as
# bash handles them; and the keys of the papers filed in ~/bib/sci and
# ~/bib/transit (chen22 â chen22a), once at least a letter is typed: an empty
# Tab lists this folder, as ls does, not 2600 keys. -w takes a program.
#
# Read by ~/.bash_completion, which bash-completion sources last, so that this
# wins over /etc/bash_completion.d/showme-completion.bash (keys only, kept
# for bib2wiki).
# F.P. Laussy
_showme() {
local cur prev words cword
_init_completion -n = || return
case "$prev" in
-w|--with)
COMPREPLY=($(compgen -c -- "$cur"))
return ;;
esac
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "-w --with -2 --two -p --path -s --size -b --bib -v --version -h --help" -- "$cur"))
return
fi
_filedir
# Paper keys: a bare word, no path in it
if [[ -n "$cur" && "$cur" != */* && "$cur" != [~.]* ]]; then
local f
for f in "$HOME"/bib/sci/"$cur"*.[pP][dD][fF] "$HOME"/bib/transit/"$cur"*.[pP][dD][fF]; do
[ -e "$f" ] || continue
f=${f##*/}
COMPREPLY+=("${f%.*}")
done
fi
}
complete -F _showme showme
and ~/.bash_completion:
# Sourced last by bash-completion: completions here win over /etc/bash_completion.d
# showme: files and folders as ls, plus paper keys
. ~/.local/share/bash-completion/completions/showme