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"
shift
eval "$fn () { unset -f $fn ; eval \"\$( $@ )\" ; $fn \$@ ; }"
fi
}
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/nvm.sh'
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
v12.2.0
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.
investigation
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)
__nvm_installed_nodes
;;
alias | unalias)
__nvm_alias
;;
*)
__nvm_commands
;;
esac;
return 0
}
Top comments (0)