Statements: Include

Inclusion of a file into a shell script is easy with . (or source which is the same thing). . will be searching along the contents of the PATH environment variable for the shell script of the given name. If you gave an absolute or relative path, it will not search PATH.

The bash allows you to pass arguments to the included code, something the sh can’t do.

cat <<EOF > foo.sh
printf "args: %s\n" "\$1"
EOF

cat <<EOF > bar.sh
#! /usr/bin/env bash
. "./foo.sh" "hello" || exit 1
EOF

chmod 755 foo.sh bar.sh
./bar.sh

The result here will be “hello”.

Lets turn . into a general purpose include facility. It should be able to include library script files of the executing script and shared script files that have already been installed by second or third parties.

Scheme 1

Search for global scripts in /usr/local/lib:/usr/lib:/lib like the system linker does. Search for local scripts in a “libexec” folder, that is close to the executing script (or include file):

# include appends .sh to the name and includes that file
include()
{
   local name="$1"; shift

   local execname

   execname="${0##*/}"           # /usr/local/bin/foo -> foo

   local libexec

   libexec="${0%/*}"             # /usr/local/bin/foo -> /usr/local/bin
   libexec="${libexec%/*}"       # /usr/local/bin -> /usr/local
   libexec="${libexec}/libexec"  # /usr/local + /libexec -> /usr/local/libexec

   local filename

   filename="${name}.sh"

   PATH="${libexec}:/usr/local/lib:/usr/lib:/lib" . "${filename}" "$@" || exit 1
}

Note

This code is a bit too simplistic. First it needs an absolute path for $0. Second the meaning of $0 depends on the shell and the location of where the include code actually is and is executed.

Scheme 2

Libraries are found by an executable placed into bin. Query the executable for its “libexec” directory. This has the advantage of being more independent of platform conventions:

include()
{
   local name="$1"; shift
   local executable="$1"; shift

   # use own executable as fallback
   executable="${executable:-$0}"

   local libexec

   # /usr/local/bin/foo-> /usr/local/libexec/foo
   libexec=$( "${executable}" libexec-dir) || exit 1

   local filename

   filename="${name}.sh"

   . "${libexec}/${filename}" "$@" || exit 1
}

This is simpler and more flexible.

Include file template

Taking cues from C headers, a shell library file that protects from double inclusion and that always returns true would look like this for a file “bar.sh” of an executable “foo:

# use `__` to separate executable and file, so foo-bar baz and foo bar-baz are
# distinguishable
if [ -z "${FOO__BAR_SH}" ]
then
   FOO__BAR_SH="included"

   # library code
fi

:  # ensure positive return value

Adhering to this convention, now one can test before the inclusion, if the file is already present:

if [ -v FOO__BAR_SH ]
then
   include "foo"
fi

The check for the inclusion guard can be moved into the “include” function itself, see mulle-bashloader.sh for a real life example. That way you can omit the if [ -z "${FOO__BAR_SH}" ] code in the library file entirely