cached.sh.skel 8.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. #!/bin/bash
  2. shellfu import pretty
  3. #
  4. # Root of the cache.
  5. #
  6. # All cache objects are created or looked up under this directory. The path
  7. # is validated against common dangerous scenarios; see $CACHED__ROOT_VALID.
  8. #
  9. CACHED__ROOT=${CACHED__ROOT:-}
  10. #
  11. # Consider $CACHED__ROOT always valid
  12. #
  13. # To minimize chance of damage, value of $CACHED__ROOT is validated using
  14. # heuristic based on common caching paths such as /tmp, /var/cache or
  15. # $HOME/.cache.
  16. #
  17. # However, if you insist on using non-standard caching location, set this
  18. # to 'always' to turn off this validation.
  19. #
  20. # Note that this has no effect on cases when $CACHED__ROOT is empty,
  21. # unset or exactly '/'; in these cases validation will always fail.
  22. #
  23. CACHED__ROOT_VALID=${CACHED__ROOT_VALID:-}
  24. cached() {
  25. #
  26. # Get cached or new output of command $@
  27. #
  28. # Usage:
  29. #
  30. # CACHED__ROOT=$HOME/.cache/myapp
  31. # cached [-m] [-w] [-a ATTR] [-e ES_EXPR] CMD [ARG]
  32. #
  33. # Look up CMD with any ARGs in local cache and return result on hit.
  34. # In case of miss, run command and if exit status is zero (can be changed
  35. # by -e argument; see below), create the cache and return it.
  36. #
  37. # Cache objects are identified by computing a MD5 hash from combination
  38. # of several attributes. By default, only CMD and ARGs are included;
  39. # that is, same CMD+ARGs combination may match, no matter what is current
  40. # working directory. Optionally, you can specify -w if current directory
  41. # should be included in the identifier. (This is useful for commands like
  42. # 'ls'.) You can also add arbitrary string by providing ATTR parameter.
  43. #
  44. # For example, following set of commands would either hit or miss cache:
  45. #
  46. # CACHED__ROOT=$(mktemp -d)
  47. # cached ls /etc # miss (first run)
  48. # cached ls /etc # hit
  49. # cached -w ls /etc # miss (first run with $PWD consideration)
  50. # pushd /tmp
  51. # cached -w ls /etc # miss (different $PWD)
  52. # pushd /tmp
  53. # cached -w ls /etc # hit (back to previous $PWD)
  54. # cached -a foo ls /etc # miss (first run with 'foo')
  55. # cached -a bar ls /etc # miss (first run with 'bar')
  56. # cached -a foo ls /etc # hit (second run with 'foo')
  57. #
  58. # All cache objects are queried or created under directory specified by
  59. # global variable $CACHED__ROOT, which must be specified beforehand.
  60. #
  61. # Exit status has to match ES_EXPR in order to be matched. ES_EXPR has
  62. # form of comma-separated list of exit statuses or simple exit status
  63. # ranges. Valid ranges are: `N` which matches exactly `N` or `M-N`,
  64. # which matches any status from `M` to `N` inclusively.
  65. #
  66. # For example, following commands do or do not create cache (assuming
  67. # no cache hit):
  68. #
  69. # cached false # no: by default ES_EXPR is only 0
  70. # cached -e 0,1 false # yes: 1 matches `0,1`
  71. # cached -e 0-3,9 exit_4 # no: 4 does not match `0-3,9`
  72. # cached -e 0-3,9 exit_3 # yes: 3 matches `0-3,9`
  73. #
  74. # NOTE: Caching of commands that process standard input is not supported.
  75. # (I.e. cached() will close standard input immediately.)
  76. #
  77. local Cache # local cache root
  78. local Workdir="$PWD" # current workdir
  79. local Command # command to run
  80. local es=2 # exit status of this function
  81. local Miss=false # force cache miss?
  82. local ObjPath # cache object path
  83. local MatchWD=false # does workdir matter?
  84. local Attr # custom attribute
  85. local EsExpr=0 # allowed exit status
  86. while true; do case $1 in
  87. --) shift; break ;;
  88. -a) Attr=$2; shift 2 || return 2 ;;
  89. -e) EsExpr=$2; shift 2 || return 2 ;;
  90. -w) MatchWD=true; shift ;;
  91. -m) Miss=true; shift ;;
  92. -*) warn "bad argument: $1"; return 2 ;;
  93. *) break ;;
  94. esac done
  95. __cached__validroot || {
  96. warn "cache root (\$CACHED__ROOT) is invalid: '$CACHED__ROOT'"
  97. return 2
  98. }
  99. Cache=$CACHED__ROOT
  100. Command=$(printf '%q ' "$@")
  101. Command=${Command% }
  102. bash -n <<<"$Command" || {
  103. warn "command is not a valid Bash command: $Command"
  104. return 2
  105. }
  106. ObjPath=$(__cached__objpath)
  107. debug -v Command ObjPath Attr Miss MatchWD
  108. exec 0<&-
  109. if $Miss; then
  110. debug FORCED_MISS
  111. __cached__run; es=$?
  112. elif __cached__hit; then
  113. debug HIT
  114. else
  115. debug MISS
  116. __cached__run; es=$?
  117. fi
  118. __cached__pull
  119. return $es
  120. }
  121. cached__kill() {
  122. #
  123. # Kill whole cache
  124. #
  125. __cached__validroot || {
  126. warn "cache root (\$CACHED__ROOT) is invalid: '$CACHED__ROOT'"
  127. return 2
  128. }
  129. rm -rf "$CACHED__ROOT"
  130. }
  131. cached__prune() {
  132. #
  133. # Remove items older than age $1
  134. #
  135. # Age must be in format:
  136. #
  137. # N[d]
  138. #
  139. # where N is an integer meaning age in minutes, unless suffix 'd' is
  140. # added, in which case N means age in days (ie. N * 24 hours).
  141. #
  142. # Examples:
  143. #
  144. # cached__prune 15 # remove items older than 15 minutes
  145. # cached__prune 5d # remove items older than 24*5 hours
  146. #
  147. local age=$1
  148. local item
  149. local scancmd
  150. __cached__validroot || {
  151. warn "cache root (\$CACHED__ROOT) is invalid: '$CACHED__ROOT'"
  152. return 2
  153. }
  154. scancmd="find $CACHED__ROOT/cached -mindepth 1 -maxdepth 1"
  155. case $age in
  156. *d) scancmd+=" -mtime +${age%d}" ;;
  157. *) scancmd+=" -mmin +$age" ;;
  158. esac
  159. for item in $(eval "$scancmd"); do
  160. rm -r "$item"
  161. done
  162. }
  163. __cached__describe() {
  164. #
  165. # Create command call description
  166. #
  167. echo "Command=$Command"
  168. $MatchWD && echo "Workdir=$(readlink -m "$Workdir")"
  169. test -n "$Attr" && echo "Attr=$Attr"
  170. }
  171. __cached__es_match() {
  172. #
  173. # True if exit status $1 matches expression $EsExpr
  174. #
  175. local es=$1
  176. local part
  177. for part in ${EsExpr//,/ }; do
  178. test -n "$part" || continue
  179. #FIXME: a rather funny implementation (works, though...)
  180. eval "echo {${part/-/..}}" | grep -qwF "$es" && return 0
  181. done
  182. return 1
  183. }
  184. __cached__hit() {
  185. #
  186. # True if $Command has cache hit
  187. #
  188. $Miss && return 1
  189. test -d "$ObjPath"
  190. }
  191. __cached__objid() {
  192. #
  193. # Describe command $Command called from directory $Workdir
  194. #
  195. __cached__describe | md5sum | cut -d\ -f1
  196. }
  197. __cached__objpath() {
  198. #
  199. # Print cache object path
  200. #
  201. printf %s "$Cache/cached/$(__cached__objid)"
  202. }
  203. __cached__pull() {
  204. #
  205. # Pull result from cache object
  206. #
  207. cat "$ObjPath/out"
  208. cat "$ObjPath/err" >&2
  209. return "$(<"$ObjPath/es")"
  210. }
  211. __cached__run() {
  212. #
  213. # Run command, creating cache object if exit status matches $EsExpr
  214. #
  215. local es # command exit status
  216. rm -rf "$ObjPath"
  217. mkdir -p "$ObjPath"
  218. __cached__describe >"$ObjPath/desc"
  219. eval "$Command" \
  220. >"$ObjPath/out.tmp"\
  221. 2>"$ObjPath/err.tmp"; es=$?
  222. echo $es>"$ObjPath/es.tmp"
  223. if __cached__es_match $es; then
  224. mv "$ObjPath/out.tmp" "$ObjPath/out"
  225. mv "$ObjPath/err.tmp" "$ObjPath/err"
  226. mv "$ObjPath/es.tmp" "$ObjPath/es"
  227. else
  228. warn "bad exit status; skipping cache creation: $es does not match $EsExpr"
  229. cat "$ObjPath/out.tmp"
  230. cat "$ObjPath/err.tmp" >&2
  231. rm "$ObjPath/out.tmp"
  232. rm "$ObjPath/err.tmp"
  233. rm "$ObjPath/es.tmp"
  234. fi
  235. return $es
  236. }
  237. __cached__validroot() {
  238. #
  239. # True if $CACHED__ROOT is valid
  240. #
  241. test -n "$CACHED__ROOT" || return 1
  242. test "$CACHED__ROOT" == / && return 1
  243. test "$CACHED__ROOT_VALID" == 'always' && return 0
  244. case $CACHED__ROOT in
  245. /var/cache/[[:word:].-]*) return 0 ;;
  246. /tmp/[[:word:].-]*) return 0 ;;
  247. /var/tmp/[[:word:].-]*) return 0 ;;
  248. $HOME/.cache/[[:word:].-]*) return 0 ;;
  249. esac
  250. return 1
  251. }
  252. __cached__validcmd() {
  253. #
  254. # True if $Command is a valid Bash command
  255. #
  256. bash -n <<<"$Command"
  257. }
  258. #shellfu module-version=__MKIT_PROJ_VERSION__