123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- #!/bin/bash
-
- shellfu import pretty
-
- #
- # Root of the cache.
- #
- # All cache objects are created or looked up under this directory. The path
- # is validated against common dangerous scenarios; see $CACHED__ROOT_VALID.
- #
- CACHED__ROOT=${CACHED__ROOT:-}
-
- #
- # Consider $CACHED__ROOT always valid
- #
- # To minimize chance of damage, value of $CACHED__ROOT is validated using
- # heuristic based on common caching paths such as /tmp, /var/cache or
- # $HOME/.cache.
- #
- # However, if you insist on using non-standard caching location, set this
- # to 'always' to turn off this validation.
- #
- # Note that this has no effect on cases when $CACHED__ROOT is empty,
- # unset or exactly '/'; in these cases validation will always fail.
- #
- CACHED__ROOT_VALID=${CACHED__ROOT_VALID:-}
-
-
- cached() {
- #
- # Get cached or new output of command $@
- #
- # Usage:
- #
- # CACHED__ROOT=$HOME/.cache/myapp
- # cached [-m] [-w] [-a ATTR] CMD [ARG]
- #
- # Look up CMD with any ARGs in local cache and return result on hit.
- # In case of miss, run command to create the cache first.
- #
- # Cache objects are identified by computing a MD5 hash from combination
- # of several attributes. By default, only CMD and ARGs are included;
- # that is, same CMD+ARGs combination may match, no matter what is current
- # working directory. Optionally, you can specify -w if current directory
- # should be included in the identifier. (This is useful for commands like
- # 'ls'.) You can also add arbitrary string by providing ATTR parameter.
- #
- # For example, following set of commands would either hit or miss cache:
- #
- # CACHED__ROOT=$(mktemp -d)
- # cached ls /etc # miss (first run)
- # cached ls /etc # hit
- # cached -w ls /etc # miss (first run with $PWD consideration)
- # pushd /tmp
- # cached -w ls /etc # miss (different $PWD)
- # pushd /tmp
- # cached -w ls /etc # hit (back to previous $PWD)
- # cached -a foo ls /etc # miss (first run with 'foo')
- # cached -a bar ls /etc # miss (first run with 'bar')
- # cached -a foo ls /etc # hit (second run with 'foo')
- #
- # All cache objects are queried or created under directory specified by
- # global variable $CACHED__ROOT, which must be specified beforehand.
- #
- # NOTE: Caching of commands that process standard input is not supported.
- # (I.e. cached() will close standard input immediately.)
- #
- local Cache # local cache root
- local Workdir="$PWD" # current workdir
- local Command # command to run
- local es=2 # exit status of this function
- local Miss=false # force cache miss?
- local ObjPath # cache object path
- local MatchWD=false # does workdir matter?
- local Attr # custom attribute
- while true; do case $1 in
- --) shift; break ;;
- -a) Attr=$2; shift 2 || return 2 ;;
- -w) MatchWD=true; shift ;;
- -m) Miss=true; shift ;;
- -*) warn "bad argument: $1"; return 2 ;;
- *) break ;;
- esac done
- __cached__validroot || {
- warn "cache root (\$CACHED__ROOT) is invalid: '$CACHED__ROOT'"
- return 2
- }
- Cache=$CACHED__ROOT
- Command=$(printf '%q ' "$@")
- Command=${Command% }
- bash -n <<<"$Command" || {
- warn "command is not a valid Bash command: $Command"
- return 2
- }
- ObjPath=$(__cached__objpath)
- debug -v Command ObjPath Attr Miss MatchWD
- exec 0<&-
- if $Miss; then
- debug FORCED_MISS
- __cached__run; es=$?
- elif __cached__hit; then
- debug HIT
- else
- debug MISS
- __cached__run; es=$?
- fi
- __cached__pull
- return $es
- }
-
- cached__kill() {
- #
- # Kill whole cache
- #
- __cached__validroot || {
- warn "cache root (\$CACHED__ROOT) is invalid: '$CACHED__ROOT'"
- return 2
- }
- rm -rf "$CACHED__ROOT"
- }
-
- cached__prune() {
- #
- # Remove items older than age $1
- #
- # Age must be in format:
- #
- # N[d]
- #
- # where N is an integer meaning age in minutes, unless suffix 'd' is
- # added, in which case N means age in days (ie. N * 24 hours).
- #
- # Examples:
- #
- # cached__prune 15 # remove items older than 15 minutes
- # cached__prune 5d # remove items older than 24*5 hours
- #
- local age=$1
- local item
- local scancmd
- __cached__validroot || {
- warn "cache root (\$CACHED__ROOT) is invalid: '$CACHED__ROOT'"
- return 2
- }
- scancmd="find $CACHED__ROOT/cached -mindepth 1 -maxdepth 1"
- case $age in
- *d) scancmd+=" -mtime +${age%d}" ;;
- *) scancmd+=" -mmin +$age" ;;
- esac
- for item in $(eval "$scancmd"); do
- rm -r "$item"
- done
- }
-
- __cached__describe() {
- #
- # Create command call description
- #
- echo "Command=$Command"
- $MatchWD && echo "Workdir=$(readlink -m "$Workdir")"
- test -n "$Attr" && echo "Attr=$Attr"
- }
-
- __cached__hit() {
- #
- # True if $Command has cache hit
- #
- $Miss && return 1
- test -d "$ObjPath"
- }
-
- __cached__objid() {
- #
- # Describe command $Command called from directory $Workdir
- #
- __cached__describe | md5sum | cut -d\ -f1
- }
-
- __cached__objpath() {
- #
- # Print cache object path
- #
- printf %s "$Cache/cached/$(__cached__objid)"
- }
-
- __cached__pull() {
- #
- # Pull result from cache object
- #
- cat "$ObjPath/out"
- cat "$ObjPath/err" >&2
- return "$(<"$ObjPath/es")"
- }
-
- __cached__run() {
- #
- # Run command, creating cache object
- #
- local es # command exit status
- rm -rf "$ObjPath"
- mkdir -p "$ObjPath"
- __cached__describe >"$ObjPath/desc"
- eval "$Command" \
- >"$ObjPath/out"\
- 2>"$ObjPath/err"; es=$?
- echo $es>"$ObjPath/es"
- return $es
- }
-
- __cached__validroot() {
- #
- # True if $CACHED__ROOT is valid
- #
- test -n "$CACHED__ROOT" || return 1
- test "$CACHED__ROOT" == / && return 1
- test "$CACHED__ROOT_VALID" == 'always' && return 0
- case $CACHED__ROOT in
- /var/cache/[[:word:].-]*) return 0 ;;
- /tmp/[[:word:].-]*) return 0 ;;
- /var/tmp/[[:word:].-]*) return 0 ;;
- $HOME/.cache/[[:word:].-]*) return 0 ;;
- esac
- return 1
- }
-
- __cached__validcmd() {
- #
- # True if $Command is a valid Bash command
- #
- bash -n <<<"$Command"
- }
-
- #shellfu module-version=__MKIT_PROJ_VERSION__
|