Skip to content

Instantly share code, notes, and snippets.

@snejus
Last active June 3, 2022 02:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save snejus/ec6edb43fc8bd7874313559d9a159d7e to your computer and use it in GitHub Desktop.
Save snejus/ec6edb43fc8bd7874313559d9a159d7e to your computer and use it in GitHub Desktop.
Find and print the changelog of a python package
#!/usr/bin/env bash
# Try finding the changelog of a Python package and render it with glow (markdown) or
# pygments (md and rst), if they are installed.
#
# Tested with bash and zsh. It isn't POSIX-compatible however.
#
# ### clone the gist
# $ chmod a+x changelog
# $ ln -is `realpath changelog` ~/.local/bin/changelog
#
# USAGE:
#
# $ changelog <package-name> [-p|--pager] [-P|--plain] [-s|--silent] [-h|--help]
#
# (or just source the functions to your environment)
#
# It depends on libraries providing links to their *github* repos in the 'Home-page'
# field on PyPI. For those which don't, there's a CHANGELOGS variable that allows to
# hard-code the mappings.
#
# It tries using Dephell to find the package details (since it doesn't need the package
# installed in order to `dephell show <pkg>`, unlike pip). If Dephell isn't available, it
# uses pip.
#
# If the changelog isn't found in the repo, it prints out the README, given it exists.
#
# If you want to access private repos and increase the Github API's rate limits,
#
# $ export GITHUB_TOKEN=<your github token>
#
declare -A CHANGELOGS
CHANGELOGS[attrs]="python-attrs/attrs/master/CHANGELOG.rst"
CHANGELOGS[isort]="PyCQA/isort/develop/CHANGELOG.md"
CHANGELOGS[pip]="pypa/pip/master/NEWS.rst"
CHANGELOGS[poetry]="python-poetry/poetry/master/CHANGELOG.md"
CHANGELOGS[psycopg2]="psycopg/psycopg2/master/NEWS"
CHANGELOGS[psycopg2-binary]="psycopg/psycopg2/master/NEWS"
good() {
printf "\e[1;3;32m$1\e[0m"
}
okay() {
printf "\e[1;33m$1\e[0m"
}
oh() {
printf "\e[1;31m$1\e[0m"
}
happy() {
printf "$(good "$1")"
[[ -n $2 ]] && printf " $2\n" || printf "\n"
}
hmmm() {
printf >&2 "$(okay WARNING:) $1\n"
}
uhoh() {
printf >&2 "$(oh ERROR:) $1\n"
}
parse_homepage() {
sed -n 's/.*\"\?Home-\?page\"\?: //pI' | tr -d '",'
}
get_homepage() {
pkg=$1
if which dephell &> /dev/null; then
details=$(dephell package show "$pkg" 2> /dev/null)
[[ -n $details ]] && echo "$details" | parse_homepage && return
hmmm "Dephell didn't find the package: Using pip"
else
hmmm "Dephell executable not found: Using pip"
fi
details=$(pip show "$pkg" 2> /dev/null)
[[ -n $details ]] && echo "$details" | parse_homepage
}
get() {
curl -s -u "username:$GITHUB_TOKEN" "$(echo "$1" | tr -d ' "')"
}
show() {
# Markdown: glow -> pygments -> plain
# Restructured text: pygments -> plain
url="$1"
plain=$2
[[ $plain -eq 1 ]] && get "$url" && return
format="rst"
echo "$url" | grep -vq ".rst" && format="md"
if [[ $format == "md" ]] && which glow &> /dev/null; then
get "$url" | glow -s dark - && return 0
fi
if which pygmentize &> /dev/null; then
get "$url" | pygmentize -l "$format" -O style=monokai
else
get "$url"
fi
}
render() {
url=$1
pager=$2
plain=$3
if [[ $pager -eq 0 ]]; then
show "$url" $plain 1>&2
else
printf "$(show "$url" $plain)" | less 1>&2
fi
}
changelog() {
pkg=$1
pager=$2
plain=$3
printf "Looking for $(good "$pkg") changelog\n"
[[ -z $GITHUB_TOKEN ]] && hmmm "GITHUB_TOKEN is not found in the environment"
if [ ${CHANGELOGS[$pkg]} ]; then
happy "Found it in the pre-defined changelogs list"
render "https://raw.githubusercontent.com/${CHANGELOGS[$pkg]}" $pager $plain
exit 0
fi
pat_projectname='s/^.*hub\.com\/\(.*\/\?\$\?\)/\1/pI'
pat_changelogurl='s/download_url.*\(https.*change.*\),/\1/pI'
pat_downloadurl='s/download_url.*\(https.*\),/\1/pI'
homepage="$(get_homepage $pkg)"
[[ -z $homepage ]] && uhoh "Pip failed to find the package" && exit 1
happy "Home-page:" "$homepage"
projectname=$(echo "$homepage" | sed -n "$pat_projectname")
[[ -z $projectname ]] && \
uhoh "Could not find $pkg github repository. Check out its home page instead: $homepage" && \
exit 1
happy "Github project:" "$projectname"
changelogurl=$(get "https://api.github.com/repos/$projectname/contents" | sed -n "$pat_changelogurl")
if [[ -n $changelogurl ]]; then
happy "Found the changelog"
url="$changelogurl"
else
hmmm "Failed to find the changelog in the repository root. Looking for a readme instead."
readmeurl=$(get "https://api.github.com/repos/$projectname/readme" | sed -n "$pat_downloadurl")
[[ -z $readmeurl ]] && uhoh "$pkg doesn't have a readme❓" && exit 1
happy 'Found the readme'
url="$readmeurl"
fi
render "$url" $pager $plain
exit 0
}
chill() {
printf "\e[1;34m$1\e[0m"
}
comment() {
printf "\e[1;3;90m$1\e[0m"
}
show_help() {
cat <<-EOF
» $(chill "changelog") $(okay "[options]") $(good "package") $(okay "[options]")
$(okay "-p, --pager") $(comment "# page the output")
$(okay "-P, --plain") $(comment "# do not apply post-processing")
$(okay "-s, --silent") $(comment "# only print errors and the output")
$(okay "-h, --help") $(comment "# display help")
EOF
exit 0
}
unhappy_input() {
uhoh "$1\n"
chill " changelog"
oh " package\n\n"
}
if test $# -eq 0; then
show_help
else
pager=0
plain=0
silent=0
pkg=""
while test $# -ne 0; do
case $1 in
-p|--pager) pager=1 ;;
-P|--plain) plain=1 ;;
-s|--silent) silent=1 ;;
-h|--help) show_help; exit ;;
*)
if [[ -z $pkg ]]; then
pkg="$1"
else
unhappy_input "Please provide a single package name"
exit 1
fi; ;;
esac
shift
done
[[ -z $pkg ]] && unhappy_input "Please provide a package name" && exit 1
if [[ $silent -eq 1 ]]; then
changelog $pkg $pager $plain 1> /dev/null
else
changelog $pkg $pager $plain
fi
fi
# vim:ft=bash
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment