#!/bin/bash shellfu import sfdoc shellfu import pretty # # Shellfu plugin interface # # Discover, query, select and run plugins inside subshells so that they # can't interfere too easily. # # # EXAMPLE PLUGIN SETUP # ==================== # # Have a Shellfu module 'foo` wanting to support plugins `bar` and # `baz`. We'll need to implement 3 modules: `foo` itself and one # for each plugin: `_foop_bar` and `_foop_baz`. # # For example, the pluging `bar` might look like: # # _foop_bar__process() { # # # # Actual "payload" function # # # do_something_meaningful_with "$@" # } # # _foop_bar__sfpimeta() { # # # # Get self meta-data key $1 # # # local key=$1 # case $key in # desc) echo "We do meaningful thing in terms of bar." ;; # esac # } # # _foop_bar__sfpi_compat() { # # # # Check if we can function in this environment # # # test -d /etc/bar # } # # The useful function is _foop_bar__process(), which does the actual thing # that the plugin is supposed to do. You can have at least one such function, # for obvious reasons. # # There are two optional function helping with the 'sfpi' discovery: # # The _foop_bar__sfpimeta() function enables parent query plugin meta-data # using sfpi__key() function. Meanings of individual keys are not defined. # # The _foop_bar__sfpi_compat() function allows parent let plugin decide if # it's compatible with current environment and prevent it from being loaded. # In other words, if this function returns 1, the plugin would not be listed # by sfpi__ls(). # # The minimal foo.sh in this example could look like: # # SFPI__PREFIX=_foop # you NEED to set this ASAP # # foo() { # local stuff=$1 # for plugin in $(sfpi__ls); do # sfpi__call "$plugin" process "$stuff" # done # } # # # Plugin name prefix # # In order for your plugins to be discoverable, name them with this # prefix. Common naming scheme is '_p_', i.e. if # your application was named `foo`, final layout could look something # like this: # # foo # _foop_bar # _foop_baz # # where you could say that your module has plugins `foo` and `bar`. In # the above example, value of $SFPI__PREFIX would be `_foop` (starting # with underscore to remain hidden in normal listings by *sfdoc*. # SFPI__PREFIX= sfpi__call() { # # Call plugin $1 function $2 in subshell # __sfpi__ckpfx local plg=$1; shift local fun=$1; shift ( shellfu import "${SFPI__PREFIX}_${plg}" type -t "${SFPI__PREFIX}_${plg}__$fun" >/dev/null || { warn "plugin function not implemented: $fun in $plg" exit 3 } "${SFPI__PREFIX}_${plg}__$fun" "$@" ) } sfpi__import() { # # Import plugin $1 into current namespace # __sfpi__ckpfx local plg=$1 local mod="${SFPI__PREFIX}_${plg}" shellfu try_import "$mod" || { warn "module for plugin not found: $mod" return 3 } sfpi__is_compat "$plg" || { warn "plugin not compatible: $mod" return 3 } shellfu import "$mod" } sfpi__varname() { # # Print name of plugin $1 variable $2 # # Note that the variables are assumed to be named in ALL_CAPS, # so prefix is converted accordingly (not the variable name, # though). # # Example: # # SFPI__PREFIX=foo # sfpi__varname bar BAZ # prints 'FOO_BAR__BAZ' # __sfpi__ckpfx local plg=$1 local var=$2 echo "${SFPI__PREFIX^^}_${plg^^}__$var" } sfpi__varvalue() { # # Print value of plugin $1 variable $2 # # Note that the variables are assumed to be named in ALL_CAPS, # so prefix is converted accordingly (not the variable name, # though). # # Example: # # SFPI__PREFIX=foo # sfpi__varvalue bar BAZ # prints value of $FOO_BAR__BAZ # __sfpi__ckpfx local plg=$1 local var=$2 local varname="${SFPI__PREFIX^^}_${plg^^}__$var" echo "${!varname}" } sfpi__funname() { # # Print name of plugin $1 function $2 # __sfpi__ckpfx local plg=$1 local fun=$2 echo "${SFPI__PREFIX}_${plg}__$fun" } sfpi__modname() { # # Print name of module implementing plugin $1 # __sfpi__ckpfx local plg=$1 echo "${SFPI__PREFIX}_${plg}" } sfpi__has() { # # True if plugin $1 has function $2 # __sfpi__ckpfx local mod="${SFPI__PREFIX}_$1" local fun=$2 ( shellfu import "$mod" type -t "${mod}__$fun" >/dev/null; ) } sfpi__is_compat() { # # True if plugin $1 is compatible # # Load plugin in subshell and look for compatibility function. If # such function exists, it's called without arguments and if exit # status is zero, plugin is compatible. # # If plugin has no compatibility function, it's considered # compatible. # __sfpi__ckpfx local plg=$1 local mod="${SFPI__PREFIX}_$plg" local fun=${mod}__sfpi_compat ( shellfu import "$mod" || exit 3 sfpi__has "$plg" sfpi_compat || exit 0 "$fun"; es=$? test $es -gt 1 && warn "illegal exit status: $es from ${fun}" return $es ) } sfpi__key() { # # Safely get meta-data key $2 from plugin for item type $1 # __sfpi__ckpfx local plg=$1 # plugin name local key=$2 # meta-data key local value # ^^ value local fun="${SFPI__PREFIX}_${plg}__sfpimeta" # plg getter function name ( sfpi__import "$plg" if type -t "$fun" >/dev/null; then value=$("$fun" "$key") debug -v fun key value test -n "$value" || value="(none)" else value='(undefined)' fi echo "$value" ) } sfpi__ls_all() { # # List all available plugins # __sfpi__ckpfx SFDOC_SHOW_HIDDEN=true sfdoc__ls_m \ | grep "^$SFPI__PREFIX"'_[[:alpha:]][[:alnum:]]*$' \ | sed "s/^${SFPI__PREFIX}_//" } sfpi__ls() { # # List compatible plugins # __sfpi__ckpfx local plg for plg in $(sfpi__ls_all); do sfpi__is_compat "$plg" && echo "$plg" done true } __sfpi__ckpfx() { # # Check if $SFPI__PREFIX has been set # debug -v SFPI__PREFIX test -n "$SFPI__PREFIX" \ || die "bug: need to set SFPI__PREFIX first" }