DEV Community


Posted on

bash - lazy completion evaluation.

lazy completion evaluation in bash

tl;dr: loading completions lazily can help with terminal init speed, reduce memory usage (if only slightly), and use expensive functions (eg generators) once only.

lazy evaluation comes with a lot of positives-- less memory, less parsing, less evaluation, and you only pay for what you use. So, what can we do with it in bash?

well, here's a stupid (well, requires some jumps) means of setting it up:

function __lazyfunc {
  if [[ -z "$(type -t $1)" || $LAZYFUNC_FORCE_REDEFINE ]]; then
    local fn="$1"
    eval "$fn () { unset -f $fn ; eval \"\$( $@ )\" ; $fn \$@ ; }"

so what does it do?

  • only define if not declared. If you rerun . .bashrc on this, it doesn't override the values.
  • (locally) take the first argument, shift it off the front
  • evaluate a string defining a function

that's it

of course, there are some downsides--

  • most declarations are not intended to be done like this
  • it takes some editing to rewrite the available source

for example, with this, rather than just

eval "$(pandoc --bash-completion)"

it now takes

__lazyfunc _pandoc pandoc --bash-completion

and it gets worse for others. eg, nvm:

export NVM_DIR="$HOME/.nvm"
__lazyfunc __nvm 'cat $NVM_DIR/bash_completion'
__lazyfunc nvm 'cat $NVM_DIR/'
complete -o default -F __nvm nvm

cargo and rustup?:

__lazyfunc _rustup rustup completions bash rustup
__lazyfunc _cargo 'cat $(rustc --print sysroot)/etc/bash_completion.d/cargo'
complete -F _rustup -o bashdefault -o default rustup
complete -F _cargo cargo

in use

using the above settings, with a few moments of loading (and adding time before eval call),

$ nvm a
real    0m0.007s
user    0m0.006s
sys 0m0.001s
lias ^C
$ cargo a
real    0m0.142s
user    0m0.089s
sys 0m0.042s
dd ^C
$ rustup 
real    0m0.023s
user    0m0.012s
sys 0m0.011s

completions     dump-testament  man             show            -v
component       -h              override        target          -V
default         --help          run             toolchain       --verbose
doc             help            self            uninstall       --version
docs            install         set             update          which
$ rustup ^C
$ pandoc 
real    0m1.221s
user    0m0.302s
sys 0m0.202s

$ pandoc
$ npm 
real    0m0.954s
user    0m0.535s
sys 0m0.351s

real    0m2.186s
user    0m1.153s
sys 0m1.058s
$ node

real    0m0.993s
user    0m0.524s
sys 0m0.404s
Welcome to Node.js v12.2.0.
Type ".help" for more information.

(node and npm are loaded through nvm and have their own macros, so this takes a bit longer than usual)

function node {
        unset -f node npm
        local which=`nvm which | tail -n 1`
        NODE_ICU_DATA="$(dirname "$(dirname "$which")")/lib/node_modules/full-icu"
        PATH="$PATH:$(dirname "$which")"
        $which $@
function npm {
        >&2 node -v
        npm $@
__lazyfunc _npm_completion npm completion
complete -F _npm_completion npm

now, what good is it?

  • on a computer with slow disk access, this greatly decreases time between starting and having access to a terminal
  • on a phone (termux), this also decreases time between startup

of course, there are some downsides

  • initiation of a function can add time to initial completion
  • this initial setup can be annoying

now, I doubt very much that I'm the first to post about it; but it is worth the time.


as you can guess, this means it hot-swaps the functions. so, what's the difference?

$ type _cargo
_cargo is a function
_cargo () 
    unset -f _cargo;
    eval "$( cat $(rustc --print sysroot)/etc/bash_completion.d/cargo )";
    _cargo $@
$ type __nvm
__nvm is a function
__nvm () 
    unset -f __nvm;
    eval "$( cat $NVM_DIR/bash_completion )";
    __nvm $@
$ nvm alias ^C
$ type __nvm
__nvm is a function
__nvm () 
    declare previous_word;
    previous_word="${COMP_WORDS[COMP_CWORD - 1]}";
    case "${previous_word}" in 
        use | run | exec | ls | list | uninstall)
        alias | unalias)
    return 0

Top comments (0)