Code Reusability [1]
Source other modules
The Shell family (to which Bash belongs) doesn’t know library, classes or modules.
Dynamically loading code can be done via source, but using
absolute paths aren’t possible since the script location isn’t fix
relative paths aren’t possible since those are relative by the current directory, not the location of the script file
This try_include() function is passed the module name, then attempts to find
its path and load it via source:
1try_include() {
2 local mod_name="$1"
3 local mod_dir
4 local mod_path
5 if ! mod_dir=$(realpath "${BASH_SOURCE[0]}"); then
6 echo "Cannot resolve ${BASH_SOURCE[0]}" 1>&2
7 return 1
8 fi
9 mod_dir="${mod_dir%/*}"
10 mod_path="$mod_dir/include/$mod_name.sh"
11 if ! . "$mod_path"; then
12 echo "Cannot load $mod_path" 1>&2
13 return 1
14 fi
15 return 0
16}
- line 5:
${BASH_SOURCE[0]}returns the relative path of the current script file towards the present working directory, whereasrealpathreturns its absolute path. If the resulting path cannot be resolved (throws an exit code other than 0), the conditional block is executed.- line 9 + 10:
The
mod_diris stripped by the final slash and the proceeding filename, resulting in the parent directory of the script, which is then expanded by/include/$mod_name.sh(where$mod_nameis passed into the module).- line 11:
The
.(dot command) is a short writing forsourceand attempts to load the module file, entering the conditional block in case of an error (exit code differs from 0).
Initialize sourced modules
Without an initialization function, the loaded module is executed as if it were
directly written inside try_include(). Bugs in the loaded module will impact
the function. To prevent that, let’s define that each module must contain a
constructor method, called __init, which returns 0 in case of success. This
method is then called by try_include().
In addition, it must be prevented that a module is loaded more than once, since this
would reset any changes done to the module’s state. A helper method called have()
is making sure of that.
1have() {
2 local module="$1"
3 if [[ -n "${__BMS_INCLUDED[$module]}" ]]; then
4 return 0
5 fi
6 return 1
7}
- line 3:
The
-ntest operator evaluates if the succeeding string is empty (can be omitted,if [[ "${__BMS_INCLUDED[$module]}" ]]also suffices).__BMS_INCLUDEDis a associative array (stores key-value pairs), which includes all successfully loaded modules (this will be added to thetry_include()method)
Hint
BMS stands for Bash Module System, the name of the framework.
Hint
There is no rule or effect using double underscores (‘__’) preceding in method or variable names, but it is commonly used across languages to mark something as private (in case the language itself does not feature access modifiers, like bash).
try_include() is now updated to store successfully loaded modules into the
__BMS_INCLUDED associative array. Also a wrapper method include(),
which enables loading multiple modules in one call is added:
1try_include() {
2 local module="$1"
3 local mod_path
4 local -i err
5 if have "$module"; then
6 return 0
7 fi
8 mod_path="$__BMS_PATH/$module.sh"
9 if ! . "$mod_path" &>/dev/null; then
10 echo "Cannot load $mod_path" 1>&2
11 return 1
12 fi
13 if __init; then
14 __BMS_INCLUDED["$module"]="$mod_path"
15 err=0
16 else
17 echo "Cannot initialize $module" 1>&2
18 err=1
19 fi
20 unset -f __init
21 return "$err"
22}
23
24include() {
25 local modules=("$@")
26 local module
27 for module in "${modules[@]}"; do
28 if ! try_include "$module"; then
29 return 1
30 fi
31 done
32 return 0
33}
- line 4:
declares a local variables of type integer
- line 5:
Calls the
have()method to verifies if specified module has already been located.- line 13:
Calling the module’s
__init()method, on success adding the module to__BMS_INCLUDED.- line 20:
Remove variable
__init(pointing to the module’s initialization method) to clear reference for the next call ontry_included().
Naturally those global variables used by try_included() must be declared
as well:
1__bms_init() {
2 local mod_dir
3 if ! mod_dir=$(realpath "${BASH_SOURCE[0]}"); then
4 echo "Cannot resolve ${BASH_SOURCE[0]}" 1>&2
5 return 1
6 fi
7 mod_dir="${mod_dir%/*}"
8 declare -gxr __BMS_PATH="$mod_dir/include"
9 declare -Axg __BMS_INCLUDED
10 readonly -f have
11 readonly -f try_include
12 readonly -f include
13 return 0
14}
- line 8:
declare -gxrdeclares a global (-g) variable, which is exported after initialization (meaning it is available to every subsequent command / sub-shell in the environment) via the-xoption and is read-only (-roption), so it cannot be accidentally overwritten.- line 9:
The
-Aoption marks that the name is an associative array.- line 10 - 12:
Mark functions as read-only, securing them from subsequent changes.
All these functions are saved to a script (e.g. bms.sh) and a new script is
created, which sources it:
1#!/bin/bash
2
3main() {
4 test_hello
5 return 0
6}
7
8{
9 if ! . bms.sh; then
10 exit 1
11 fi
12 if ! include "test"; then
13 exit 1
14 fi
15 main "$@"
16 exit "$?"
17}
- line 4:
Calling a function from a test module (included in line 12).
- line 9:
Attempt to source
bms.sh.- line 12:
Attempt to source the test module using the
include()function frombms.sh.
Setting up a test module
For convenience and general usability the bms.sh module, it should be placed
somewhere visible to all users, for example at /usr/local/share/bms/, also
featuring an include/ sub-folder.
In order to access the module from any bash script, this new directory needs to be
visible to the source command, hence it needs to be added to the PATH variable.
This can be done by placing a symbolic link:
$ ln -s /usr/local/share/bms /usr/local/bin/bms.sh
The test module is now created in test.sh. As previously defined, it must feature
a constructor method __init():
__init() {
declare -gxr __test_name="$USER"
return 0
}
test_hello() {
echo "Hello $__test_name"
}
Same namespace for all modules
In bash all sourced modules share the same namespace, which means that a variable or method name must be unique across all sourced modules. To avoid such conflicts when sourcing a new module.
This can be prevented when sticking to a set of rule within a module:
Element |
Naming Convention |
Example |
|---|---|---|
module |
lowercase letters and digits |
mod |
function |
module_name + “_” + … |
mod_functionA |
internal function |
“_” + module_name + “_” + … |
_mod_helper |
global variable |
“__” + module_name + “_” + … |
__mod_variable |
Because bash functions inherit the variables of its callee, by that being able
to change them. To prevent that all local variables must be declare using
local or declare.
Helper functions for arrays
Because arrays are used regularly a helper module to write, search and sort arrays
is useful and save it under /usr/local/share/bms/include/array.sh.
Print array, one line per item
1array_to_lines() {
2 local array=("$@")
3 if (( ${array[@]} > 0 )); then
4 printf "%s\n" "${array[@]}"
5 fi
6}
- line 2:
Define an array putting all passed in arguments (
$@) into it.- line 3 + 4:
${array[@]}expands each member of the array into a separate word, separated with a space, for example (“one” “two” “three”) -> “one” “two” “three”. If the array has no members it expands to 0, so the conditional block is entered, if the array has any members. Each word is then printed viaprintf, by declaring each value as string followed by a new line ("%sn") and passing all array words as arguments.
Use printf over echo, because the ladder accepts options, so
something like -o in the array is interpreted as option and won’t be printed.
It also saves the declaration of a loop, because printf is able to handle
arrays of various length in one call.
Sort array
To get the sorted elements of an array, the array_to_lines() function comes in
handy:
1array_sort() {
2 local array=("$@")
3 array_to_lines "${array[@]}" | sort -V"
4}
- line 3:
As
array_to_linesreturns a new-line separated list of values, we can pipe this directly intosort(the -V option is used to support version numbering within the text as well)
Search item in array
The function either returns an exit code 0, if the array contains a match or 1
if it doesn’t. Using a grep on the array isn’t as performant on longer
arrays as a regular loop is, so array_to_lines() will not be
utilized here, but the array’s elements are compared one by one:
1array_contains() {
2 local needle="$1"
3 local haystack=("${@:2}")
4 local cur
5 for cur on "${haystack[@]}"; do
6 if [[ "$needle" == "$cur" ]]; then
7 return 0
8 fi
9 done
10 return 1
- line 3:
The function expects the first argument to be the term to search for and all following arguments to be the array content (
${@:2}).- line 4 + 5:
Loop over all elements in the array (
$cur) and compare it to the searched term ($needle).
Add a constructor
Because it is here defined that each module must implement a __init() function,
even though this function isn’t used as nothing is required to be initialized by it,
so it only reports success:
__init() {
return 0
}
Testing the functions
In order to test each function created in this section, this test script is used:
1#!/bin/bash
2
3main() {
4 local array
5 local i
6 array=()
7 for (( i = 0; i < 5; i++ )); do
8 local num
9 num=$((RANDOM % 10))
10 array+=("$num")
11 done
12 echo "Unsorted:"
13 array_to_lines "${array[@]}"
14 echo "Sorted:"
15 array_sort "${array[@]}"
16 for (( i = 0; i < 3; i++)); do
17 local num
18 num=$((RANDOM % 10))
19 if array_contains "$num" "${array[@]}"; then
20 echo "array contains $num"
21 else
22 echo "array does not contain $num"
23 fi
24 done
25 return 0
26}
27
28{
29 if ! bms.sh: then
30 exit 1
31 fi
32 if ! include "array"; then
33 exit 1
34 fi
35 main "$@"
36 exit "$?"
37}
- line 7 - 11:
Add five random numbers (integer between 0 and 32767): the double brackets in
num=$((RANDOM % 10))enable the expression within to contain operators such as % (modulo) or other arithmetic expressions as opposed to single brackets. Same applies to square brackets as commonly used in if-statements (for exampleif [[ i < 10 ]]; then).- line 29 - 34:
Sourcing the two modules
bms.shandarray. The ladder uses theinclude()function frombms.shso expects the module name as an argument.
Executing the test script produces an output similar to this:
Unsorted:
2
5
3
4
9
Sorted:
2
3
4
5
9
array contains 3
array contains 9
array does not contain 1