#! /usr/bin/env bash
#
# rfc
#
# Author: Baptiste Fontaine
# License: MIT
# Version: 1.0.0 (2024/03/26)
#
# URL: https://github.com/bfontaine/rfc
#
__rfc() {
local VERSION='1.0.0'
local PAGER=${PAGER:-less}
local fetch_cmd=${CURL:-curl}
local rfc_dir
local default_rfc_dir="$HOME/.cache/RFCs"
local code
# Error codes
local NOT_FOUND=1
local UNRECOGNIZED_CMD=2
local NETWORK_ERROR=3
local NO_CURL_WGET=4
# URLs
local REPO_HOME='https://github.com/bfontaine/rfc'
local ISSUES_URL='https://github.com/bfontaine/rfc/issues'
local RFCS_TGZ_BASEURL='https://www.rfc-editor.org/in-notes/tar/'
local RFCS_BASEURL='https://www.rfc-editor.org/rfc/'
local DRAFTS_BASEURL='https://www.ietf.org/id/'
if [ -n "$RFC_DIR" ]; then
rfc_dir="$RFC_DIR"
elif [ -n "$XDG_CACHE_HOME" ]; then
rfc_dir="$XDG_CACHE_HOME/RFCs"
else
rfc_dir="$default_rfc_dir"
fi
# $rfc_dir must be absolute
[ "${rfc_dir:0:1}" != "/" ] && rfc_dir="$PWD/$rfc_dir"
print_rfc() {
$PAGER "$rfc_dir/$1"
}
fetch_url() {
case $fetch_cmd in
curl)
curl -fs "$1" >| "$2";
case "$?" in
6|7) return $NETWORK_ERROR ;;
22) return $NOT_FOUND ;;
*) return 0 ;;
esac ;;
wget)
wget -cq "$1" -O "$2";
case "$?" in
4) return $NETWORK_ERROR ;;
8) return $NOT_FOUND ;;
*) return 0 ;;
esac ;;
esac
}
get_ftp_url() {
local tmp
tmp=$(mktemp)
# $1 = pattern of the file we're looking for
local pattern="$1"
local fetch_exit_code=
fetch_url "$RFCS_TGZ_BASEURL" "$tmp"
fetch_exit_code=$?
if [ "$fetch_exit_code" -ne 0 ]; then
return $fetch_exit_code
fi
# Lines have the following format:
#
RFCs0001-0500.tar.gz
# We want this __^^^^^^^^^^^^^^^^^^^^
< "$tmp" grep "$pattern" | cut -d'"' -f2
}
get_rfc() {
fetch_url "$RFCS_BASEURL/rfc$1.txt" "$rfc_dir/$1"
return $?
}
get_draft() {
fetch_url "$DRAFTS_BASEURL/$1.txt" "$rfc_dir/$1"
return $?
}
migrate_rfc_dir() {
local previous_rfc_dir="$HOME/.RFCs"
if [ -f "$previous_rfc_dir/_404s" ] && [ "$rfc_dir" != "$previous_rfc_dir" ]; then
# Migrate _404s
cat "$previous_rfc_dir/_404s" >> "$rfc_dir/_404s"
rm "$previous_rfc_dir/_404s"
# Migrate the RFC files
mv -f "$previous_rfc_dir"/* "$rfc_dir/"
echo "In 1.0.0 the RFC cache directory moved from $previous_rfc_dir to $rfc_dir." >&2
echo "All files were migrated; you can now run \`rmdir '$previous_rfc_dir'\`" >&2
fi
}
init_rfc_dir() {
mkdir -p "$rfc_dir"
touch "$rfc_dir"/_404s
if [ -z "$RFC_TEST" ]; then
migrate_rfc_dir
fi
}
print_not_found_error() {
if [[ "$1" == draft* ]]; then
echo "There's no such draft."
else
echo "There's no such RFC."
fi
}
## Subcommands ##
# rfc list
list() {
\ls -1 "$rfc_dir" | grep '^[0-9]\{1,\}$' | sort -n | sed 's/^/RFC /'
}
# rfc search
search() {
if [ -z "$1" ]; then
echo 'Usage: rfc search ""'
return $UNRECOGNIZED_CMD;
fi
grep -RIs --exclude _404s $@ "$rfc_dir" | sed "s%$rfc_dir/%RFC %"
return 0
}
# rfc sync [week|month|all]
# shellcheck disable=SC2164
sync_rfcs() {
local name=RFC-all.tar.gz
case "$1" in
month|30|30days) name=$(get_ftp_url "30daysTo.*gz") ;;
week|7|7days) name=$(get_ftp_url "7daysTo.*gz") ;;
all) ;;
*)
# Support 'sync' without any argument as an alias to 'sync all'
if [ ! -z "$1" ]; then
echo "Unrecognized 'sync' target: $1"
return $UNRECOGNIZED_CMD
fi ;;
esac
# shellcheck disable=SC2181
if [ $? -ne 0 ]; then
return $?
fi
# This shouldn't happen.
if [ -z "$name" ]; then
echo "I couldn't find the requested archive." >&2
echo "Please report this: $ISSUES_URL" >&2
return $NOT_FOUND
fi
mkdir -p "$rfc_dir/_tmp"
if fetch_url "$RFCS_TGZ_BASEURL$name" "$rfc_dir/_tmp/$name"; then
:
else
exit $NETWORK_ERROR
fi
cd "$rfc_dir/_tmp"
tar -xzf "$name"
rm -f "$name"
[ -d in-notes ] && cd in-notes
for f in rfc*.txt; do
echo "$f" | grep -q '^rfc[0-9]\{1,\}\.txt$'
if [ "$?" -eq "0" ]; then
n=${f##rfc};
n=${n%%.txt};
mv -f "$f" "$rfc_dir/$n"
fi
done
rm -rf "$rfc_dir/_tmp"
}
# rfc help
print_usage() {
# shellcheck disable=SC2016
echo 'Usage:
rfc --version # display the version number and exit
rfc --help # display this text and exit
rfc # display the RFC
rfc search [OPTS] X # Search for X in local RFCs using grep with OPTS
passed through
rfc list # List locally available RFCs
rfc sync [week|month|all] # batch download RFCs. `week` and `month`
respectively download only RFCs that were
added/updated during the last week/month.
`all` (default) downloads all RFCs. It might
take some time, be patient.
rfc clear # clear the cache
The default cache directory is '"$default_rfc_dir"'.
The current one is '"$rfc_dir"'.'
}
# rfc version
print_version() {
echo "rfc v$VERSION - $REPO_HOME"
}
## /commands ##
if [ $# -eq 0 ]; then
print_usage
return 0
fi
# Prefix everything with --debug to enable the debugging output on STDERR.
if [ "$1" == "--debug" ]; then
shift ;
echo "Enabling debug mode." >&2 ;
set -x ;
fi
if which curl > /dev/null 2>&1; then
if which wget > /dev/null 2>&1; then
fetch_cmd='wget'
else
echo "Error: You need Wget or cURL!"
return $NO_CURL_WGET;
fi
fi
init_rfc_dir
case "$1" in
-v|--version|version)
print_version
return 0;;
-h|--help|-help|help)
print_usage
return 0;;
-*)
echo "Unrecognized option: $1"
print_usage
return $UNRECOGNIZED_CMD;;
clear|clean|clr)
rm -rf "$rfc_dir"
return 0;;
ls|list)
list
return 0;;
search|find)
shift;
search "$@"
return $?;;
sync|download)
shift;
sync_rfcs "$@"
return $?;;
_*)
print_usage
return 1;;
*)
if grep -q "^$1\$" "$rfc_dir/_404s" 2> /dev/null; then
print_not_found_error "$1"
return $NOT_FOUND
elif [ ! -f "$rfc_dir/$1" ]; then
if [[ "$1" == draft* ]]; then
get_draft "$1"
else
get_rfc "$1"
fi
code=$?
if [ $code -eq $NETWORK_ERROR ]; then
echo "Unable to connect to the network."
[ ! -s "$rfc_dir/$1" ] && rm -f "$rfc_dir/$1"
return $NETWORK_ERROR
fi
if [ $code -eq $NOT_FOUND ]; then
echo "$1" >> "$rfc_dir/_404s"
print_not_found_error "$1"
[ ! -s "$rfc_dir/$1" ] && rm -f "$rfc_dir/$1"
return $NOT_FOUND
fi
fi
print_rfc "$@" ;;
esac
}
__rfc "$@"