123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 |
- #!/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] [-e ES_EXPR] CMD [ARG]
- #
- # Look up CMD with any ARGs in local cache and return result on hit.
- # In case of miss, run command and if exit status is zero (can be changed
- # by -e argument; see below), create the cache and return it.
- #
- # 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.
- #
- # Exit status has to match ES_EXPR in order to be matched. ES_EXPR has
- # form of comma-separated list of exit statuses or simple exit status
- # ranges. Valid ranges are: `N` which matches exactly `N` or `M-N`,
- # which matches any status from `M` to `N` inclusively.
- #
- # For example, following commands do or do not create cache (assuming
- # no cache hit):
- #
- # cached false # no: by default ES_EXPR is only 0
- # cached -e 0,1 false # yes: 1 matches `0,1`
- # cached -e 0-3,9 exit_4 # no: 4 does not match `0-3,9`
- # cached -e 0-3,9 exit_3 # yes: 3 matches `0-3,9`
- #
- # 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
- local EsExpr=0 # allowed exit status
- while true; do case $1 in
- --) shift; break ;;
- -a) Attr=$2; shift 2 || return 2 ;;
- -e) EsExpr=$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__es_match() {
- #
- # True if exit status $1 matches expression $EsExpr
- #
- local es=$1
- local part
- for part in ${EsExpr//,/ }; do
- test -n "$part" || continue
- #FIXME: a rather funny implementation (works, though...)
- eval "echo {${part/-/..}}" | grep -qwF "$es" && return 0
- done
- return 1
- }
-
- __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 if exit status matches $EsExpr
- #
- local es # command exit status
- rm -rf "$ObjPath"
- mkdir -p "$ObjPath"
- __cached__describe >"$ObjPath/desc"
- eval "$Command" \
- >"$ObjPath/out.tmp"\
- 2>"$ObjPath/err.tmp"; es=$?
- echo $es>"$ObjPath/es.tmp"
- if __cached__es_match $es; then
- mv "$ObjPath/out.tmp" "$ObjPath/out"
- mv "$ObjPath/err.tmp" "$ObjPath/err"
- mv "$ObjPath/es.tmp" "$ObjPath/es"
- else
- warn "bad exit status; skipping cache creation: $es does not match $EsExpr"
- cat "$ObjPath/out.tmp"
- cat "$ObjPath/err.tmp" >&2
- rm "$ObjPath/out.tmp"
- rm "$ObjPath/err.tmp"
- rm "$ObjPath/es.tmp"
- fi
- 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__
|