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, whereas realpath returns 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_dir is 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_name is passed into the module).

line 11:

The . (dot command) is a short writing for source and 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 -n test operator evaluates if the succeeding string is empty (can be omitted, if [[ "${__BMS_INCLUDED[$module]}" ]] also suffices). __BMS_INCLUDED is a associative array (stores key-value pairs), which includes all successfully loaded modules (this will be added to the try_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 on try_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 -gxr declares a global (-g) variable, which is exported after initialization (meaning it is available to every subsequent command / sub-shell in the environment) via the -x option and is read-only (-r option), so it cannot be accidentally overwritten.

line 9:

The -A option 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 from bms.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.

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_lines returns a new-line separated list of values, we can pipe this directly into sort (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 example if [[ i < 10 ]]; then).

line 29 - 34:

Sourcing the two modules bms.sh and array. The ladder uses the include() function from bms.sh so 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