Created
August 2, 2019 17:15
-
-
Save jpbochi/c7971a0f3d7a3b9fdb71c5621c8e41c5 to your computer and use it in GitHub Desktop.
Run speficied (presumably expensive) command with specified arguments and cache result. If cache is fresh enough, don't run command again but return cached output.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
set -o errexit | |
set -o nounset | |
set -o noglob | |
set -o pipefail | |
# | |
# Purpose: run speficied command with specified arguments and cache result. If cache is fresh enough, don't run command again but return cached output. | |
# Also cache exit status and stderr. | |
# License: GPLv3 | |
# Adapted from zsh version at https://gist.github.com/akorn/51ee2fe7d36fa139723c851d87e56096 | |
# Use silly long variable names to avoid clashing with whatever the invoked program might use | |
RUNCACHED_MAX_AGE=${RUNCACHED_MAX_AGE:-300} | |
RUNCACHED_IGNORE_ENV=${RUNCACHED_IGNORE_ENV:-0} | |
RUNCACHED_IGNORE_PWD=${RUNCACHED_IGNORE_PWD:-0} | |
[ -n "$HOME" ] && RUNCACHED_CACHE_DIR=${RUNCACHED_CACHE_DIR:-$HOME/.runcached} | |
RUNCACHED_CACHE_DIR=${RUNCACHED_CACHE_DIR:-/var/cache/runcached} | |
function usage() { | |
echo "Usage: runcached [--ttl <max cache age>] [--cache-dir <cache directory>]" | |
echo " [--ignore-env] [--ignore-pwd] [--help] [--prune-cache]" | |
echo " [--] command [arg1 [arg2 ...]]" | |
echo | |
echo "Run 'command' with the specified args and cache stdout, stderr and exit" | |
echo "status. If you run the same command again and the cache is fresh, cached" | |
echo "data is returned and the command is not actually run." | |
echo | |
echo "Normally, all exported environment variables as well as the current working" | |
echo "directory are included in the cache key. The --ignore options disable this." | |
echo "The OLDPWD variable is always ignored." | |
echo | |
echo "--prune-cache deletes all cache entries older than the maximum age. There is" | |
echo "no other mechanism to prevent the cache growing without bounds." | |
echo | |
echo "The default cache directory is ${RUNCACHED_CACHE_DIR}." | |
echo "Maximum cache age defaults to ${RUNCACHED_MAX_AGE}." | |
echo | |
echo "CAVEATS:" | |
echo | |
echo "Side effects of 'command' are obviously not cached." | |
echo | |
echo "There is no cache invalidation logic except cache age (specified in seconds)." | |
echo | |
echo "If the cache can't be created, the command is run uncached." | |
echo | |
echo "This script is always silent; any output comes from the invoked command. You" | |
echo "may thus not notice errors creating the cache and such." | |
echo | |
echo "stdout and stderr streams are saved separately. When both are written to a" | |
echo "terminal from cache, they will almost certainly be interleaved differently" | |
echo "than originally. Ordering of messages within the two streams is preserved." | |
} | |
while [ -n "${1:-}" ]; do | |
case "$1" in | |
--ttl) RUNCACHED_MAX_AGE="$2"; shift 2;; | |
--cache-dir) RUNCACHED_CACHE_DIR="$2"; shift 2;; | |
--ignore-env) RUNCACHED_IGNORE_ENV=1; shift;; | |
--ignore-pwd) RUNCACHED_IGNORE_PWD=1; shift;; | |
--prune-cache) RUNCACHED_PRUNE=1; shift;; | |
--help) usage || exit 0;; | |
--) shift; break;; | |
*) break;; | |
esac | |
done | |
# This is racy, but the race is harmless; at worst, the program is run uncached | |
# because the cache is unusable. Testing for directory existence saves an | |
# mkdir(1) execution in the common case, improving performance infinitesimally; | |
# it could matter if runcached is run from inside a tight loop. | |
# Hide errors so that runcached itself is transparent (doesn't mix new messages | |
# into whatever the called program outputs). | |
[ -d "$RUNCACHED_CACHE_DIR" ] || mkdir -p "$RUNCACHED_CACHE_DIR" >/dev/null 2>/dev/null | |
if [ -n "${RUNCACHED_PRUNE:-}" ]; then | |
find "$RUNCACHED_CACHE_DIR/." -maxdepth 1 -type f \ | |
\! -newermt "$(date -r "$(echo "$(date '+%s')" - "$RUNCACHED_MAX_AGE" | bc)" '+%Y-%m-%d %H:%M:%S')" \ | |
-delete -print | |
exit 0 | |
fi | |
if [ -z "$*" ]; then | |
usage | |
exit 1 | |
fi | |
# Almost(?) nothing uses OLDPWD, but taking it into account potentially reduces cache efficency. | |
# Thus, we ignore it for the purpose of coming up with a cache key. | |
unset OLDPWD | |
if [ -n "$RUNCACHED_IGNORE_PWD" ]; then unset PWD; fi | |
if hash md5sum &> /dev/null; then | |
RUNCACHED_MD5="md5sum" | |
else | |
RUNCACHED_MD5='md5' | |
fi | |
RUNCACHED_CACHE_KEY=$(([ -z $"RUNCACHED_IGNORE_ENV" ] || env; echo -E "$@" ) | $RUNCACHED_MD5 | cut -f1 -d' ') | |
RUNCACHED_CACHE_PATH="$RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY" | |
# Look for cached outputs | |
if [ -f "${RUNCACHED_CACHE_PATH}.stdout" ]; then | |
if (( $(date -r "${RUNCACHED_CACHE_PATH}.stdout" '+%s') > "$(date '+%s')" - "$RUNCACHED_MAX_AGE" )); then | |
# echo "¢¢¢ ${RUNCACHED_CACHE_PATH} ¢¢¢" >&2 | |
# echo "$(date '+%s')" - $(date -r "${RUNCACHED_CACHE_PATH}.stdout" '+%s') | bc >&2 | |
/bin/cat "${RUNCACHED_CACHE_PATH}.stdout" & | |
/bin/cat "${RUNCACHED_CACHE_PATH}.stderr" >&2 & | |
wait | |
exit $(<"${RUNCACHED_CACHE_PATH}.exitstatus") | |
else | |
rm -f "${RUNCACHED_CACHE_PATH}."{cmd,stdout,stderr,exitstatus} 2>/dev/null | |
fi | |
fi | |
RUNCACHED_tempdir=$(mktemp -d 2>/dev/null) | |
RUNCACHED_CACHE_TEMPPATH="$RUNCACHED_tempdir/$RUNCACHED_CACHE_KEY" | |
# RUNCACHED_CACHE_TEMPPATH="$RUNCACHED_CACHE_PATH" | |
echo "$@" > "${RUNCACHED_CACHE_TEMPPATH}.cmd" | |
set +o errexit | |
$@ \ | |
1> >(tee "${RUNCACHED_CACHE_TEMPPATH}.stdout") \ | |
2> >(tee "${RUNCACHED_CACHE_TEMPPATH}.stderr" >&2) | |
RUNCACHED_ret=$? | |
echo $RUNCACHED_ret > "${RUNCACHED_CACHE_TEMPPATH}.exitstatus" | |
mv "${RUNCACHED_CACHE_TEMPPATH}."{cmd,stdout,stderr,exitstatus} "${RUNCACHED_CACHE_DIR}/" | |
rmdir "$RUNCACHED_tempdir" | |
exit "$RUNCACHED_ret" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment