Browse Source

First implementation

Alois Mahdal 6 years ago
parent
commit
6b801d01f6
1 changed files with 215 additions and 4 deletions
  1. 215
    4
      src/cached.sh.skel

+ 215
- 4
src/cached.sh.skel View File

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