cached.sh.skel 6.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  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] 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 to create the cache first.
  35. #
  36. # Cache objects are identified by computing a MD5 hash from combination
  37. # of several attributes. By default, only CMD and ARGs are included;
  38. # that is, same CMD+ARGs combination may match, no matter what is current
  39. # working directory. Optionally, you can specify -w if current directory
  40. # should be included in the identifier. (This is useful for commands like
  41. # 'ls'.) You can also add arbitrary string by providing ATTR parameter.
  42. #
  43. # For example, following set of commands would either hit or miss cache:
  44. #
  45. # CACHED__ROOT=$(mktemp -d)
  46. # cached ls /etc # miss (first run)
  47. # cached ls /etc # hit
  48. # cached -w ls /etc # miss (first run with $PWD consideration)
  49. # pushd /tmp
  50. # cached -w ls /etc # miss (different $PWD)
  51. # pushd /tmp
  52. # cached -w ls /etc # hit (back to previous $PWD)
  53. # cached -a foo ls /etc # miss (first run with 'foo')
  54. # cached -a bar ls /etc # miss (first run with 'bar')
  55. # cached -a foo ls /etc # hit (second run with 'foo')
  56. #
  57. # All cache objects are queried or created under directory specified by
  58. # global variable $CACHED__ROOT, which must be specified beforehand.
  59. #
  60. # NOTE: Caching of commands that process standard input is not supported.
  61. # (I.e. cached() will close standard input immediately.)
  62. #
  63. local Cache # local cache root
  64. local Workdir="$PWD" # current workdir
  65. local Command # command to run
  66. local es=2 # exit status of this function
  67. local Miss=false # force cache miss?
  68. local ObjPath # cache object path
  69. local MatchWD=false # does workdir matter?
  70. local Attr # custom attribute
  71. while true; do case $1 in
  72. --) shift; break ;;
  73. -a) Attr=$2; shift 2 || return 2 ;;
  74. -w) MatchWD=true; shift ;;
  75. -m) Miss=true; shift ;;
  76. -*) warn "bad argument: $1"; return 2 ;;
  77. *) break ;;
  78. esac done
  79. __cached__validroot || {
  80. warn "cache root (\$CACHED__ROOT) is invalid: '$CACHED__ROOT'"
  81. return 2
  82. }
  83. Cache=$CACHED__ROOT
  84. Command=$(printf '%q ' "$@")
  85. Command=${Command% }
  86. bash -n <<<"$Command" || {
  87. warn "command is not a valid Bash command: $Command"
  88. return 2
  89. }
  90. ObjPath=$(__cached__objpath)
  91. debug -v Command ObjPath Attr Miss MatchWD
  92. exec 0<&-
  93. if $Miss; then
  94. debug FORCED_MISS
  95. __cached__run; es=$?
  96. elif __cached__hit; then
  97. debug HIT
  98. else
  99. debug MISS
  100. __cached__run; es=$?
  101. fi
  102. __cached__pull
  103. return $es
  104. }
  105. cached__kill() {
  106. #
  107. # Kill whole cache
  108. #
  109. __cached__validroot || {
  110. warn "cache root (\$CACHED__ROOT) is invalid: '$CACHED__ROOT'"
  111. return 2
  112. }
  113. rm -rf "$CACHED__ROOT"
  114. }
  115. cached__prune() {
  116. #
  117. # Remove items older than age $1
  118. #
  119. # Age must be in format:
  120. #
  121. # N[d]
  122. #
  123. # where N is an integer meaning age in minutes, unless suffix 'd' is
  124. # added, in which case N means age in days (ie. N * 24 hours).
  125. #
  126. # Examples:
  127. #
  128. # cached__prune 15 # remove items older than 15 minutes
  129. # cached__prune 5d # remove items older than 24*5 hours
  130. #
  131. local age=$1
  132. local item
  133. local scancmd
  134. __cached__validroot || {
  135. warn "cache root (\$CACHED__ROOT) is invalid: '$CACHED__ROOT'"
  136. return 2
  137. }
  138. scancmd="find $CACHED__ROOT/cached -mindepth 1 -maxdepth 1"
  139. case $age in
  140. *d) scancmd+=" -mtime +${age%d}" ;;
  141. *) scancmd+=" -mmin +$age" ;;
  142. esac
  143. for item in $(eval "$scancmd"); do
  144. rm -r "$item"
  145. done
  146. }
  147. __cached__describe() {
  148. #
  149. # Create command call description
  150. #
  151. echo "Command=$Command"
  152. $MatchWD && echo "Workdir=$(readlink -m "$Workdir")"
  153. test -n "$Attr" && echo "Attr=$Attr"
  154. }
  155. __cached__hit() {
  156. #
  157. # True if $Command has cache hit
  158. #
  159. $Miss && return 1
  160. test -d "$ObjPath"
  161. }
  162. __cached__objid() {
  163. #
  164. # Describe command $Command called from directory $Workdir
  165. #
  166. __cached__describe | md5sum | cut -d\ -f1
  167. }
  168. __cached__objpath() {
  169. #
  170. # Print cache object path
  171. #
  172. printf %s "$Cache/cached/$(__cached__objid)"
  173. }
  174. __cached__pull() {
  175. #
  176. # Pull result from cache object
  177. #
  178. cat "$ObjPath/out"
  179. cat "$ObjPath/err" >&2
  180. return "$(<"$ObjPath/es")"
  181. }
  182. __cached__run() {
  183. #
  184. # Run command, creating cache object
  185. #
  186. local es # command exit status
  187. rm -rf "$ObjPath"
  188. mkdir -p "$ObjPath"
  189. __cached__describe >"$ObjPath/desc"
  190. eval "$Command" \
  191. >"$ObjPath/out"\
  192. 2>"$ObjPath/err"; es=$?
  193. echo $es>"$ObjPath/es"
  194. return $es
  195. }
  196. __cached__validroot() {
  197. #
  198. # True if $CACHED__ROOT is valid
  199. #
  200. test -n "$CACHED__ROOT" || return 1
  201. test "$CACHED__ROOT" == / && return 1
  202. test "$CACHED__ROOT_VALID" == 'always' && return 0
  203. case $CACHED__ROOT in
  204. /var/cache/[[:word:].-]*) return 0 ;;
  205. /tmp/[[:word:].-]*) return 0 ;;
  206. /var/tmp/[[:word:].-]*) return 0 ;;
  207. $HOME/.cache/[[:word:].-]*) return 0 ;;
  208. esac
  209. return 1
  210. }
  211. __cached__validcmd() {
  212. #
  213. # True if $Command is a valid Bash command
  214. #
  215. bash -n <<<"$Command"
  216. }
  217. #shellfu module-version=__MKIT_PROJ_VERSION__