showme is a bash script to visualize 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 (?!).
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 -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.
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.
Three things are looked at, in order, and the first that yields a match wins.
First locate, as always. 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.
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.
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.1 Wed Sep 17 2025
# @-::. : . . .. . :@ 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
# -@@+=:.....+@ laussy.org/wiki/showme
# F.P. Laussy
# Default program
PROGRAM="okular"
# Parse options with getopt
PARSED=$(getopt -o w:2pv -l with:,two,path,version -n 'showme' -- "$@")
if [ $? -ne 0 ]; then
echo "Error: Invalid option" >&2
exit 1
fi
eval set -- "$PARSED"
# Initialize two-files flag
TWO_FILES=false
# Initialize path-only flag
PATH_ONLY=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"
OPTS+=(-w "$2")
shift 2
;;
-2|--two)
TWO_FILES=true
OPTS+=(-2)
shift
;;
-p|--path)
PATH_ONLY=true
OPTS+=(-p)
shift
;;
-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 "Error: Invalid option" >&2
exit 1
;;
esac
done
# Check if a search term is provided
if [ -z "$1" ]; then
echo "Error: No search term provided" >&2
exit 1
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")
# 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 "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 "Error: no key in '$1'" >&2
exit 1
fi
if [ ${#KEYS[@]} -gt 1 ]; then
echo "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 "Error: no entry in the bibliography carries the DOI $LOOKUP (doi2bib -w $LOOKUP would add one)" >&2
exit 1
fi
echo "showme: $LOOKUP is $KEY" >&2
set -- "$KEY" "${@:2}"
fi
# Get matching PDF files
FILES=($(locate "$1" | grep -i ".pdf$" | grep -i bib))
# 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
# Still nothing: it may have been downloaded to the wrong place. Fetch it back.
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 "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 "showme: $DEST already exists, leaving $SRC alone" >&2
FILES=("$DEST")
elif mv -n "$SRC" "$DEST"; then
echo "showme: filed $NAME from Downloads into $TRANSITDIR"
FILES=("$DEST")
else
echo "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 ]; then
echo "Error: No matching PDF files found" >&2
exit 1
fi
ENTRY=
for BIBFILE in "${BIBFILES[@]}"; do
[ -r "$BIBFILE" ] || continue
ENTRY=$(awk -v key="$1" '
BEGIN { IGNORECASE = 1 }
!found && $0 ~ ("^@[A-Za-z]+[{(][ \t]*" key "[ \t]*,") { found = 1 }
found { print }
found && /^[})]/ { exit }
' "$BIBFILE")
[ -n "$ENTRY" ] && break
done
if [ -z "$ENTRY" ]; then
echo "Error: No matching PDF files found, and no BibTeX entry for '$1' either" >&2
exit 1
fi
echo "showme: no PDF for '$1', but the bibliography knows it (${BIBFILE##*/}):"
echo
printf '%s\n' "$ENTRY"
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:]]*$//'
}
# De-TeX the common accents and braces for terminal display
detex() {
sed -E 's/\\"([aouAOUei])/\1\xcc\x88/g; s/\\'"'"'([aeiouyAEIOU])/\1\xcc\x81/g;
s/\\`([aeiouAEIOU])/\1\xcc\x80/g; s/\\\^([aeiouAEIOU])/\1\xcc\x82/g;
s/\\~([nNaAoO])/\1\xcc\x83/g; s/\\c\{?c\}?/c\xcc\xa7/g; s/\\ss\{?\}?/\xc3\x9f/g;
s/\\\.\{?([a-zA-Z])\}?/\1\xcc\x87/g; s/\\u\{?([a-zA-Z])\}?/\1\xcc\x86/g;
s/\\v\{?([a-zA-Z])\}?/\1\xcc\x8c/g; s/\\H\{?([a-zA-Z])\}?/\1\xcc\x8b/g;
s/\\o\{?\}?([^a-zA-Z])/\xc3\xb8\1/g; s/\\l\{?\}?([^a-zA-Z])/\xc5\x82\1/g;
s/\\aa\{?\}?/\xc3\xa5/g; s/\\ae\{?\}?/\xc3\xa6/g;
s/\\([A-Za-z])/\1/g;
s/[{}]//g; s/~/ /g; s/---/\xe2\x80\x94/g; s/--/\xe2\x80\x93/g'
}
AUTHOR=$(bibfield author | detex | sed 's/ and /, /g')
TITLE=$(bibfield title | detex)
JOURNAL=$(bibfield journal)
YEAR=$(bibfield year)
VOLUME=$(bibfield volume)
PAGES=$(bibfield pages)
# journal = prl and friends are @string abbreviations defined in sci.bib
if [ -n "$JOURNAL" ]; then
EXPANDED=$(grep -i -m1 "^@string{[[:space:]]*$JOURNAL[[:space:]]*=" "$HOME/bib/sci.bib" 2>/dev/null \
| sed -E 's/^[^=]*=[[:space:]]*"//; s/"[[:space:]]*}.*$//')
[ -n "$EXPANDED" ] && JOURNAL="$EXPANDED"
fi
BOLD=$'\e[1m'; ITAL=$'\e[3m'; OFF=$'\e[0m'
printf ' %s,\n' "$AUTHOR"
printf ' %sâ%sâ,%s\n' "$ITAL" "$TITLE" "$OFF"
REF=" ${BOLD}${JOURNAL}${OFF}"
[ -n "$VOLUME" ] && REF="$REF ${BOLD}${VOLUME}${OFF}"
[ -n "$PAGES" ] && REF="$REF, $PAGES"
[ -n "$YEAR" ] && REF="$REF ($YEAR)"
printf '%s.\n\n' "$REF"
# 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\.[^},"]*')
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. 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"
read -r -p "See sci huh? [Y/n] " ANSWER
case "$ANSWER" in
[nN]*) ;;
*)
if printf '%s' "$KEY.pdf" | xclip -selection clipboard &>/dev/null; then
echo "showme: $KEY.pdf is in the clipboard, for the save box"
fi
google-chrome "https://sci-hub.ru/$DOI" &>/dev/null &
;;
esac
fi
exit 0
fi
if [ "$PATH_ONLY" = true ]; then
# Just print the location(s) of the matching file(s), don't open anything
printf '%s\n' "${FILES[@]}"
elif [ "$TWO_FILES" = true ]; then
# Open the first instance on the current desktop
$PROGRAM "${FILES[0]}" &>/dev/null &
# Open the same file again in a second instance
$PROGRAM "${FILES[0]}" &>/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
$PROGRAM "${FILES[0]}" &>/dev/null &
fi
Bash autocompletion is taken care of in showme-completion.bash in /etc/bash_completion.d/. This can work for other similar commands, such as bib2wiki by registering them on the last line.
#!/bin/bash
_showme_completion() {
local cur prev
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
# Directories to search for PDFs
local search_dirs=("$HOME/bib/transit" "$HOME/bib/sci")
# Handle options
if [[ "$cur" == -* ]]; then
COMPREPLY=( $(compgen -W "-w --with" -- "$cur") )
return 0
fi
# If previous word is -w or --with, suggest programs
if [[ "$prev" == "-w" || "$prev" == "--with" ]]; then
COMPREPLY=( $(compgen -W "okular evince acroread" -- "$cur") )
return 0
fi
# Complete PDF filenames from both ~/bib/transit and ~/bib/sci
local files=""
for dir in "${search_dirs[@]}"; do
if [ -d "$dir" ]; then
# Get list of PDFs in the directory, strip .pdf extension
files+=$(find "$dir" -maxdepth 1 -type f -iname "*.pdf" -printf "%f\n" | sed 's/\.pdf$//' | sed "s|^$dir/||")
files+=" "
fi
done
# Generate completions if files were found
if [ -n "$files" ]; then
COMPREPLY=( $(compgen -W "$files" -- "$cur") )
fi
return 0
}
# Register the completion function for multiple commands
complete -F _showme_completion showme
complete -F _showme_completion bib2wiki