How to make BASH Script accept plugins

578 views Asked by At

I have a BASH script that's finished, but now I need to add the capability for it to accept plugins.

As an example, the script can move files from one filesystem location to another. It's currently written to utilize the mv command for this, but I want to know how to give the user the option to do two things:
Replace the mv command with something else, rsync for example
Redefine the entire "move" function with their improved version of it

I brainstormed for ways to do this after finding little online, but I'm not confident this is the right way.

Idea for "Replace the mv command with something else":
Provide a specification document that defines all the commands (and their associated switches) that can be replaced. I'd imagine it should also assert where/how these adjustments can be made.

Idea for "redefine the entire move function with their improved version of it":
Source a plugin file that contains a function with the same name as the one the user is redefining. I believe this would overwrite the default code for the given function.

I'd like to know the "right way" to implement plugin functionality on a conceptual level, and also how it might be achieved within BASH scripts specifically.

1

There are 1 answers

0
Socowi On

1. Define your interfaces

The most important part is not about the implementation, but about documentation and usage. Each and every tiny detail you rely on has to be communicated to the user.

Some examples for an overwriteable mv command. With the lazy specification mv: Move "$1" to "$2" your script might break in case ...

  • You use mv source1 source2 target/
  • You use mv -n source target to prevent overwriting an existing file
  • You assume that mv overwrites an existing target without asking and without errors.
  • You assume that mv does not print anything, for instance in var=$(mv a /tmp/; ls; mv /tmp/a ./).
  • You assume mv sourcedir/ targetdir/ and mv sourcedir targetdir to be the same. Hint: rsync behavior changes based on trailing slashes

When you carefully defined a standardized interface, you can think about the implementation.

2. Implement a plugin system

2.1 Using bash's default behavior (pretty hacky)

Technically, you don't have to do anything here, as most commands in bash are already "plugins", that is, executable binaries or script files as specified in the $PATH variable.

If a user wanted to write their own mv as a plugin for your_script.sh they could already run ...

mkdir /tmp/script_plugins
echo '#! /usr/bin/env bash
echo This is the mv plugin of the user
mv "$@"
' > /tmp/script_plugins/mv
chmod u+x /tmp/script_plugins/mv
export PATH="/tmp/script_plugins:$PATH"
./your_script.sh

... unless your_script.sh overwrites the PATH again or defines a function mv or stuff like that.

2.2 Using dedicated names (cleaner)

However, for a cleaner approach, I would define plugins as functions with dedicated names, to emphasize that those are OK to be overwritten:

#! /usr/bin/env bash
# Content of your_script.sh

if declare -pf USER_PLUGIN_MV &> /dev/null; then
  # user defined and exported a plugin function
  plugable_mv() { "$USER_PLUGIN_MV" "$@"; }
else
  # no plugin was specified, use default implementation
  plugable_mv() { mv "$@"; }
fi

for i in "just" "an" "example"; do
  plugable_mv "$i" target/
done

If the user defines and exports a function with the name USER_PLUGIN_MV, then that function will be used in instead of the default mv command.

USER_PLUGIN_MV() { echo "plugin received args: $*"; }
export -f USER_PLUGIN_MV
./your_script.sh

... prints ...

plugin received args: just target/
plugin received args: an target/
plugin received args: example target/

2.3 Using command strings

Alternatively, you could let the user specify command strings (as seen in xargs, bash -c, or GNU parallel), e.g. ...

./your_script.sh --replace-mv-with 'echo "plugin received args: $*"'

Those could be implemented as ...

# things starting with PSEUDO_ are pseudo code
if PSEUDO_user_specified_mv_plugin; then
  plugable_mv() {
    # either ...
    bash -c "$PSEUDO_mv_plugin_arg" user_plugin_mv "$@"
    # ... or if plugins have to access/define/alter your variables
    eval "$mv_plugin_arg"
  }
else
  plugable_mv() { mv "$@"; }
fi

However, nested quotes pose a serious problem to most users once the plugins become bigger. So, if you implemented this system, I still would recommend your users to go with exported functions ...

USER_PLUGIN_MV() { echo "plugin received args: $*"; }
export -f USER_PLUGIN_MV
./your_script.sh --replace-mv-with 'USER_PLUGIN_MV "$@"'