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 "$@"'