## -*- 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
