summaryrefslogtreecommitdiffstats
path: root/examples/functions/autoload.v4
blob: 850c614857ac6f1d9c2cea93752369257ab95b62 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
## -*- sh -*-

# The psuedo-ksh autoloader.

# How to use:
# o One function per file.
# o File and function name match exactly.
# o File is located in a directory that is in FPATH.
# o This script (autoload) must be sourced in as early as possible. This
#   implies that any code in this script should NOT rely on any library of local
#   or self-defined functions having already been loaded.
# o autoload must be called for each function before the function can be used. If
#   autoloads are in directories where there are nothing but autoloads, then
#   'autoload /path/to/files/*' suffices (but see options -a and -f).
# o The call must be made in the current environment, not a subshell.
# o The command line suffices as "current environment". If you have autoload
#   calls in a script, that script must be dotted into the process.

# The first cut of this was by Bill Trost, trost@reed.bitnet.
# The second cut came from Chet Ramey, chet@ins.CWRU.Edu
# The third cut came from Mark Kennedy, mtk@ny.ubs.com.  1998/08/25
# The fourth cut came from Matthew Persico, matthew.persico@gmail.com 2017/August

autoload_calc_shimsize ()
{
    echo $((AUTOLOAD_SHIM_OVERHEAD + 3 * ${#1}))
}

_autoload_split_fpath ()
{
    (IFS=':'; set -- ${FPATH}; echo "$@")
}

_aload()
{
    local opt OPTIND
    local doexport=0
    local doreload=0
    local doverbose=0
    local doevalshim=0
    local loadthese
    local optimize=0
    local loaded=0
    local exported=0
    local optimized=0
    local summary=0
    local dofpath=0
    while getopts xrvla:oyf opt; do
        case $opt in
            x) doexport=1;;
            r) doreload=1;;
            v) doverbose=1;;
            l) doevalshim=1;;
            a) loadthese=$(find $OPTARG -maxdepth 1 -type f -printf '%f ');;
            o) optimize=1;;
            y) summary=1;;
            f) loadthese=$(find $(_autoload_split_fpath) -maxdepth 1 -type f -printf '%f ');;
            *) echo "_aload: usage: _aload [-xrvlyf] [-a dir] [function ...]" >&2; return;;
        esac
    done

    shift $(($OPTIND-1))

    [ -z "$loadthese" ] && loadthese="$@"

    local func
    for func in $loadthese; do
        local exists_fn
        exists_fn=$(declare -F $func)
        if [ -n "$exists_fn" ] && ((doreload==0)) && ((doevalshim==0))
        then
            if ((doverbose))
            then
                echo "autoload: function '$func' already exists"
            fi
        else
            local andevaled=''
            local andexported=''
            local evalstat=0
            local doshim=1
            local funcfile
            funcfile=$(_autoload_resolve $func)
            if [[ $funcfile ]] ; then
                ## The file was found for $func. Process it.

                if ((optimize)); then
                    ## For the first function loaded, we will not know
                    ## AUTOLOAD_SHIM_OVERHEAD. We can only calculate it after
                    ## we have loaded one function.
                    if [[ $AUTOLOAD_SHIM_OVERHEAD ]]; then
                        local size=$(wc -c $funcfile| sed 's/ .*//')
                        local shimsize=$(autoload_calc_shimsize $func)
                        if (( size <= shimsize)); then
                            doshim=0
                            andevaled=', optimized'
                            ((optimized+=1))
                        fi
                    fi
                fi

                if ((doevalshim)); then
                    doshim=0
                    andevaled=', evaled'
                fi

                ## 'brand' as in branding a cow with a mark. We add a local
                ## variable to each function we autoload so that we can tell
                ## later on it is an autoloaded function without having to
                ## maintain some bash array or hash that cannot be passed to
                ## and used by subshells.
                local brandtext
                brandtext="eval \"\$(type $func | sed -e 1d -e 4ilocal\\ AUTOLOADED=\'$func\')\""
                if ((doshim)); then
                    ## Don't bother trying to save space by shoving all the
                    ## eval text below onto one unreadable line; new lines will
                    ## be added at your semicolons and any indentation below
                    ## seems to be ignored anyway if you export the function;
                    ## look at its BASH_FUNCTION representation.
                    eval $func '()
                    {
                    local IS_SHIM="$func"
                    local file=$(_autoload_resolve '$func')
                    if [[ $file ]]
                    then
                        . $file
                        '$brandtext'
                        '$func' "$@"
                        return $?
                    else
                        return 1;
                    fi
                    }'
                else
                    . $funcfile
                    eval "$brandtext"
                fi
                evalstat=$?
                if((evalstat==0))
                then
                    ((loaded+=1))
                    ((doexport)) && export -f $func && andexported=', exported' && ((exported+=1))
                    ((doverbose)) && echo "$func autoloaded${andexported}${andevaled}"
                    if [[ ! $AUTOLOAD_SHIM_OVERHEAD ]] && ((doshim)); then
                        ## ...we have just loaded the first function shim into
                        ## memory. Let's calc the AUTOLOAD_SHIM_OVERHEAD size
                        ## to use going forward. In theory, we could check
                        ## again here to see if we should optimize and source
                        ## in this function, now that we now the
                        ## AUTOLOAD_SHIM_OVERHEAD. In practice, it's not worth
                        ## duping that code or creating a function to do so for
                        ## one function.
                        AUTOLOAD_SHIM_OVERHEAD=$(type $func | grep -v -E "^$1 is a function" | sed "s/$func//g"| wc -c)
                        export AUTOLOAD_SHIM_OVERHEAD
                    fi
                else
                    echo "$func failed to load" >&2
                fi
            fi
        fi
    done
    ((summary)) && echo "autoload: loaded:$loaded exported:$exported optimized:$optimized overhead:$AUTOLOAD_SHIM_OVERHEAD bytes"
}

_autoload_dump()
{
    local opt OPTIND
    local opt_p=''
    local opt_s=''
    while getopts ps opt
    do
        case $opt in
            p ) opt_p=1;;
            s ) opt_s=1;;
        esac
    done

    shift $(($OPTIND-1))

    local exported=''
    local executed=''
    local func
    for func in $(declare | grep -E 'local\\{0,1} AUTOLOADED' | sed -e "s/.*AUTOLOADED=//" -e 's/\\//g' -e 's/[");]//g' -e "s/'//g")
    do
        if [ -n "$opt_p" ]; then echo -n "autoload "; fi
        if [ -n "$opt_s" ]
        then
            exported=$(declare -F | grep -E "${func}$" | sed 's/declare -f\(x\{0,1\}\).*/\1/')
            [ "$exported" = 'x' ] && exported=' exported' || exported=' not exported'
            executed=$(type $func | grep 'local IS_SHIM')
            [ -z "$executed" ] && executed=' executed' || executed=' not executed'
        fi
        echo "${func}${exported}${executed}"
    done
}

_autoload_resolve()
{
    if [[ ! "$FPATH" ]]; then
        echo "autoload: FPATH not set or null" >&2
        return
    fi

    local p # for 'path'. The $() commands in the for loop split the FPATH
            # string into its constituents so that each one may be processed.

    for p in $( _autoload_split_fpath ); do
        p=${p:-.}
        if [ -f $p/$1 ]; then echo $p/$1; return; fi
    done

    echo "autoload: $1: function source file not found" >&2
}

_autoload_edit()
{
    [ -z "$EDITOR" ] && echo "Error: no EDITOR defined" && return 1
    local toedit
    local func
    for func in "$@"
    do
        local file=$(_autoload_resolve $func)
        if [[ $file ]]
        then
            toedit="$toedit $file"
        else
            echo "$funcname not found in FPATH funcfile. Skipping."
        fi
    done

    [ -z "$toedit" ] && return 1

    local timemarker=$(mktemp)

    $EDITOR $toedit

    local i
    for i in $toedit
    do
        if [ $i -nt $timemarker ]
        then
            local f=$(basename $i)
            echo Reloading $f
            autoload -r $f
        fi
    done
}

_autoload_page()
{
    [ -z "$PAGER" ] && echo "Error: no PAGER defined" && return 1
    local topage
    local func
    for func in "$@"
    do
        local file=$(_autoload_resolve $func)
        if [[ $file ]]
        then
            topage="$topage $file"
        else
            echo "$funcname not found in FPATH funcfile. Skipping."
        fi
    done

    [ -z "$topage" ] && return 1

    $PAGER $topage
}

_autoload_remove()
{
    unset -f "$@"
}

_autoload_help()
{
    cat <<EOH
NAME
        autoload

SYNOPSIS
        autoload [-ps]
        autoload [-xuremloyv] [function ...]
        autoload -a directory [-oyv]
        autoload -f [-oyv]
        autoload [-h]

        autoreload [function ...]

DESCRIPTION

        An implementation of the 'autoload' functionality built into other
        shells, of which 'ksh' is the most prominent.  It allows for a keeping
        the process environment small by loading small 'shim' functions into
        memory that will, on first call, load the full text of the given
        function and run it. Subsequent calls to the function just run the
        function.

        'autoreload' is a synonym for 'autoload -r'. See below.

USAGE

        o Each function to be autoloaded should be defined in a single file,
          named exactly the same as the function.

        o In order to avoid side effects, do NOT put code other than the
          function definition in the file. Unless of course you want to do some
          one-time initialization. But beware that if you reload the function
          for any reason, you will rerun the initialization code. Make sure
          your initialization is re-entrant. Or, better yet,

          *** do NOT put code other than the function definition in the file ***

        o These function definition files should be placed in a directory that
          is in the FPATH environment variable. Subdirectories are NOT scanned.

        o The autoload script should be sourced into the current process as
          early as possible in process start up. See NOTES below for
          suggestions.

        o The calls to the autoload function must be made in the current
          process. If your calls are in their own script, that script must be
          sourced in. Command line invocations are also sufficient. (But see
          '-l' below.)

        o The first time the function is called, the shim function that was
          created by the 'autoload' call is what is executed. This function
          then goes and finds the appropriate file in FPATH, sources it in and
          then calls the actual function with any arguments you just passed in
          to the shim function. Subsequent calls just run the function.

OPTIONS

        -a Autoload (a)ll the functions found in the given directory.

        -f Autoload all the functions found in all the directories on the
           FPATH.

        -p Print all the autoloaded functions.

        -s Print all the autoloaded functions and add their export status.

        -x Export the specified functions to the environment for use in
           subshells.

        -u Unset the function, so it can be reloaded.

        -r Reload the shims of the specified functions, even if the functions
           have been already been executed.  This will allow you to modify the
           functions' source and have the new version executed next time the
           function is called.

           It would be very easy to modify a function's script, run the
           function and scratch your head for a long time trying to figure out
           why your changes are not being executed. That's why we provide the
           '-e' flag described below for modifications.

           Reloads, of course, only apply in the context of the current session
           and any future subshell you start from the current session. Existing
           sessions will need to have the same 'autoload -r' command run in
           them.

        -e Find the scripts in which the specified functions are defined and
           start up \$EDITOR on those scripts. Reload the ones that were
           modified when you exit \$EDITOR. (Note: If you use 'autoload -e foo'
           to edit function 'foo', and then in your editor you separately load
           up function 'bar', 'autoload' has no way of knowing that you edited
           'bar' and will NOT reload 'bar' for you.)

           Reloads, of course, only apply in the context of the current session
           and any future subshell you start from the current session. Existing
           sessions will need to have the same 'autoload -r' command run in
           them.

        -m Find the scripts in which the specified functions are defined and
           run \$PAGER on them ('m' is for 'more', because 'p' (page) and 'l'
           (load) are already used as options in 'autoload').

        -l When autoloading a function, eval the shim immediately in order to
           load the true function code. See "Using '-l'" in the NOTES below for
           details.

        -o Optimize. When autoloading, take the time to execute

               'theCharCount=\$(wc -c \$theFuncFile)'

           for each function and

                if \$theCharCount < \$AUTOLOAD_SHIM_OVERHEAD

           don't shim it, just eval directly.

        -y Summar(y). Print the number of loaded, exported and optimized
           functions.

        -v Turns up the chattiness.

NOTES

        o Calling 'autoload' on a function that already exists (either shimmed
          or expanded) silently ignores the request to load the shim unless it
          has been previously removed (-u) or you force the reload (-r).

        o Changing and reloading a function that has been exported does not
          require it be re-exported; the modifications will appear in
          subsequent subshells.

        o Using '-1'

          If you are running under set -x and/or set -v, you may see that the
          shim does not appear to "work"; instead of seeing the shim first and
          the real code subsequently, you may see the shim evaluated multiple
          times.

          This may not be an error; review your code. What is most likely
          happening is that you are calling the function in subshells via
          backticks or $(), or in a script that is not being sourced into the
          current environment. If you have not previously called the function
          in question at your command line or in a script that was sourced into
          the current environment, then the various subshells are going to
          encounter the shim and replace with the real code before executing.

          Remember, however, that environment modifications that occur in a
          subshell are NOT propagated back to the calling shell or over to any
          sibling shells. So, if you call an autoloaded function in a very
          tight loop of very many subshells, you may want to make an 'autoload
          -l' call before you start your loop. '-l' will instruct 'autoload' to
          bypass the shim creation and just source in the function's file
          directly. For a few calls, the overhead of repeatedly running the
          shim is not expensive, but in a tight loop, it might be. Caveat
          Programmer.

        o Although the number of functions in the environment does not change
          by using 'autoload', the amount of memory they take up can be greatly
          reduced, depending on the size of your functions. If you have a lot
          of small functions, then it is possible that the shim text will be
          larger than your actual functions, rendering the memory savings moot.

          'small' in this case can be determined by calling the function
          'autoload_calc_shimsize' with the name of the function to determine
          its shim size.

        o In order to support the -p and -s options, we need a way to determine
          if a function 'func' has been autoloaded or if it was loaded
          diredctly. In order to do that, we modify the function's code by
          adding the text

              local  AUTOLOADED='func';

          to the shim and to the actual function text, just after the opening
          brace. Then supporting -p and -s is just a matter of grepping through
          all the function text in memory. Even though grepping through the
          environment may not be the most efficient way to support this, it is
          the simplest to implement for -p and -s operations that are not
          heavily used.

          As a consequence of this (and other reasons), the AUTOLOAD* namespace
          is reserved for autoloading. Make sure you check any functions that
          you bring under autoload for use of variables or functions that start
          with AUTOLOAD and change them.

        o The easiest way to load shims for all functions on the FPATH is to run

               autoload -f -x

          in the profile that gets run for login shells.

          When called in the profile of a login shell where no definitions
          exist, -f will load all functions it can find on FPATH and -x will
          export all of those functions to be available in subshells when this
          is called in a login shell. Using this option will relieve you of the
          need to call 'autoload' after Every Single Function Definition, nor
          will you need to call it in subshells.

          The only thing left to do is to load up the autoload function itself
          and its helper functions. That needs to happen in your profile:

            export FPATH=~/functions       # or wherever you stash them
            if [ -z $(declare -F autoload) ]
            then
              . ~/bin/autoload             # or wherever you've put it
            fi

          The 'if' statement is used to make sure we don't reload autoload
          needlessly. Sourcing in the autoload script loads the 'autoload'
          function and all of its support functions. Additionally, we export
          all of these functions so that they are available in subshells; you
          do not have to re-source the autoload file in '.bashrc'.

        o Even with all of these shenanigans, you will find cases where no
          matter how hard you try, your autoloaded functions will be
          unavailable to you, even if you run 'autoload -x -f'. The typical
          condition for this is starting up not a subshell, but a brand new
          DIFFERENT shell. And the typical example of this is git extensions.

          At the time of this writing, git extensions work by taking a command
          'git foo' and looking for a file 'git-foo' on the path. 'git' then
          executes 'git-foo' in a new shell - it executes your command in
          /bin/sh. That's not a subshell of your process. It will not get your
          exported shell functions. Ballgame over.

          If you find that you want your functions to be available in such
          circumstances, convert them back to plain old scripts, make sure they
          are 'sh' compliant and take the read/parse hit every time they are
          run.

EOH
}

autoload()
{
    if (( $# == 0 )) ; then _autoload_dump; return; fi

    local opt OPTIND OPTARG
    local passthru
    local dumpopt
    while getopts psuema:yxrvlohf opt
    do
        case $opt in
            p|s) dumpopt="$dumpopt -${opt}";;
            u) shift $((OPTIND-1)); _autoload_remove "$@"; return;;
            e) shift $((OPTIND-1)); _autoload_edit "$@"; return;;
            m) shift $((OPTIND-1)); _autoload_page "$@"; return;;
            x|r|v|l|y|f|o) passthru="$passthru -$opt";;
            a) passthru="$passthru -$opt $OPTARG";;
            h) _autoload_help; return;;
            *) echo "autoload: usage: autoload [-puUx] [function ...]" >&2; return;;
        esac
    done

    shift $(($OPTIND-1))
    if [ -n "$dumpopt" ]
    then
        _autoload_dump $dumpopt
    else
        _aload $passthru "$@"
    fi
}

autoreload ()
{
    autoload -r "$@"
}

## When we source in autoload, we export (but NOT autoload) the autoload
## functions so that they are available in subshells and you don't have to
## source in the autoload file in subshells.
export -f _aload \
       _autoload_dump \
       _autoload_edit \
       _autoload_help \
       _autoload_page \
       _autoload_resolve \
       _autoload_split_fpath \
       autoload \
       autoload_calc_shimsize \
       autoreload