How do you write, import, use, and test libraries in Bash?

7 minute read

Here’s a really beautiful, almost Python-like style I’ve come with over the years for writing and using Bash libraries. Bash is a beautiful “glue”-type language which allows you to easily tie together executables from many languages. Considering how long Bash has been around, I’m not sure why the below style isn’t more popular, but perhaps it hasn’t been thought of or used this way before. So, here goes. I think you’ll find it really useful.

You can also see a general starting point I use for all of my bash scripts in my hello_world_best.sh file in my eRCaGuy_hello_world repo.

You can see a full library example in floating_point_math.sh.

library_basic_example.sh:

#!/usr/bin/env bash

RETURN_CODE_SUCCESS=0
RETURN_CODE_ERROR=1

# Add your library functions here. Ex:

my_func1() {
    echo "100.1"
}

my_func2() {
    echo "200"
}

my_func3() {
    echo "hello world"
}

# Note: make "private" functions begin with an underscore `_`, like in Python,
# so that users know they are not intended for use outside this library.

# Assert that the two input argument strings are equal, and exit if they are not
_assert_eq() {
    if [ "$1" != "$2" ]; then
        echo "Error: assertion failed. Arguments not equal!"
        echo "  arg1 = $1; arg2 = $2"
        echo "Exiting."
        exit $RETURN_CODE_ERROR
    fi
}

# Run some unit tests of the functions found herein
_test() {
    printf "%s\n\n" "Running tests."

    printf "%s\n" "Running 'my_func1'"
    result="$(my_func1)"
    printf "%s\n\n" "result = $result"
    _assert_eq "$result" "100.1"

    printf "%s\n" "Running 'my_func2'"
    result="$(my_func2)"
    printf "%s\n\n" "result = $result"
    _assert_eq "$result" "200"

    printf "%s\n" "Running 'my_func3'"
    result="$(my_func3)"
    printf "%s\n\n" "result = $result"
    _assert_eq "$result" "hello world"

    echo "All tests passed!"
}

main() {
    _test
}

# Determine if the script is being sourced or executed (run).
# See:
# 1. "eRCaGuy_hello_world/bash/if__name__==__main___check_if_sourced_or_executed_best.sh"
# 1. My answer: https://stackoverflow.com/a/70662116/4561887
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
    # This script is being run.
    __name__="__main__"
else
    # This script is being sourced.
    __name__="__source__"
fi

# Code entry point. Only run `main` if this script is being **run**, NOT
# sourced (imported).
# - See my answer: https://stackoverflow.com/a/70662116/4561887
if [ "$__name__" = "__main__" ]; then
    main "$@"
fi

Run the library to run its unit tests

Now, make the file executable. Running it will run its internal unit tests:

# make it executable
chmod +x library_basic_example.sh

# run it
./library_basic_example.sh

Example run command and output:

eRCaGuy_hello_world$ bash/library_basic_example.sh 
Running tests.

Running 'my_func1'
result = 100.1

Running 'my_func2'
result = 200

Running 'my_func3'
result = hello world

All tests passed!

Import (source) the library

To import a Bash library, you “source” it with the source or . (better) command. Read more about that in my answer here: source (.) vs export (and also some file lock [flock] stuff at the end).

1. Use a manually-set import path

You can do this either in a bash terminal directly, or in your own bash script. Try it right now inside your own terminal!:

source "path/to/library_basic_example.sh"

# Or (better, since it's Posix-compliant)
. "path/to/library_basic_example.sh"

Once you source (import) the Bash library, you get access to call its functions directly. Here’s a full example run and output, showing that I can do function calls like my_func1, my_func2, etc, right inside my terminal once I’ve sourced (imported) this Bash library!:

eRCaGuy_hello_world$ . bash/library_basic_example.sh
eRCaGuy_hello_world$ my_func1
100.1
eRCaGuy_hello_world$ my_func2
200
eRCaGuy_hello_world$ my_func3
hello world
eRCaGuy_hello_world$ _test
Running tests.

Running 'my_func1'
result = 100.1

Running 'my_func2'
result = 200

Running 'my_func3'
result = hello world

All tests passed!

2. Use a BASHLIBS environment variable to make importing your Bash libraries easier

You can source your bash libraries from any path. But, an environment variable, such as BASHLIBS, makes it easier. Add this to the bottom of your ~/.bashrc file:

if [ -d "$HOME/libs_bash/libraries" ] ; then
    export BASHLIBS="$HOME/libs_bash/libraries"
fi

Now, you can symlink your Bash libraries into that directory, like this:

# symlink my `library_basic_example.sh` file into the `~/libs_bash/libraries`
# dir
mkdir -p ~/libs_bash/libraries
cd path/to/dir_where_my_library_file_of_interest_is_stored
# make the symlink
ln -si "$(pwd)/library_basic_example.sh" ~/libs_bash/libraries/

Now that a symlink to my library_basic_example.sh file is stored inside ~/libs_bash/libraries/, and the BASHLIBS environment variable has been set and exportted to my environment, I can import my library into any Bash terminal or script I run within the terminal, like this:

. "$BASHLIBS/library_basic_example.sh"

3. [My most-used technique!] Using relative import paths

This is really beautiful and powerful. Check this out!

Let’s say you have the following directory layout:

dir1/
    my_lib_1.sh

    dir2/
        my_lib_2.sh
        my_script.sh
    
        dir3/
            my_lib_3.sh

The above representation could easily exist in a large program or tool-chain you have set up with a scattered assortment of runnable scripts and libraries, especially when sharing (sourcing/importing) code between your various bash scripts, no matter where they lie.

Imagine you have these constraints/requirements:

  1. The entire directory structure above is all stored inside a single GitHub repo.
  2. You need anyone to be able to git clone this repo and just have all scripts and imports and things magically work for everybody!
    1. This means you need to use relative imports when importing your bash scripts into each other.
  3. You are going to run my_script.sh, for example.
  4. You must be able to call my_script.sh from anywhere, meaning: you should be able to be cded into any directory in your entire file system at the time you call this script to run.
  5. my_script.sh must use relative imports to import my_lib_1.sh, my_lib_2.sh, and my_lib_3.sh.

Here’s how! Basically, we are going to find the path to the script being run, and then use that path as a relative starting point to the other scripts around this script!

Read my full answer on this for more details: How to obtain the full file path, full directory, and base filename of any script being run OR sourced…even when the called script is called from within another bash function or script, or when nested sourcing is being used!.

Full example:

my_script.sh:

#!/usr/bin/env bash

# Get the path to the directory this script is in.
FULL_PATH_TO_SCRIPT="$(realpath "${BASH_SOURCE[-1]}")"
SCRIPT_DIRECTORY="$(dirname "$FULL_PATH_TO_SCRIPT")"

# Now, source the 3 Bash libraries of interests, using relative paths to this
# script!
. "$SCRIPT_DIRECTORY/../my_lib_1.sh"
. "$SCRIPT_DIRECTORY/my_lib_2.sh"
. "$SCRIPT_DIRECTORY/dir3/my_lib_3.sh"

# Now you've sourced (imported) all 3 of those scripts!

That’s it! Super easy if you know the commands!

Can Python do this? No, not natively at least. Bash is much easier than Python in this regard! Python doesn’t use filesystem paths directly when importing. It’s much more complicated and convoluted than that. But, I’m working on an import_helper.py Python module to make this type of thing easy in Python too. I’ll publish it shortly as well.

Going further

  1. See my full and really-useful Bash floating point library in my eRCaGuy_hello_world repo here: floating_point_math.sh.
  2. See some alternative notes of mine on Bash library installation and usage in my readme here: https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world/tree/master/bash/libraries

See also

  1. Testing a bash shell script - this answer mentions this assert.sh Bash repo, which looks really useful for more-robust Bash unit testing!
  2. Bash and Test-Driven Development
  3. Unit testing Bash scripts

I have also published this as an answer on Stack Overflow, here: Importing functions from a shell script.

Leave a comment

Comments are powered by Utterances. A free GitHub account is required. Comments are moderated. Be respectful. No swearing or inflammatory language. No spam.
I reserve the right to delete any inappropriate comments. All comments for all pages can be viewed and searched online here.

To edit or delete your comment: Option 1 (recommended): click the date just above your comment, ex: the just now or 5 minutes ago (or equivalent) part where it says YOUR_NAME commented just now or YOUR_NAME commented 5 minutes ago, etc., or Option 2: click the "Comments" link at the top of the comments section below where it says how many comments have been left. Option 1 will take you directly to your comment on GitHub. Option 2 will take you to a GitHub page with all comments for this page. Then: --> find your comment on this GitHub page and click the 3 dots in the top-right of your comment --> click "Edit" or "Delete". Editing or adding a comment from the GitHub page also gives you a nicer editor.