One Script, Several Files: source

A library is a file of functions that several scripts share: each script loads it with source, and finds it from any folder with here=$(dirname "${BASH_SOURCE[0]}"), the folder the script itself is saved in.

Journeyman · Unix · Linux · Updated

After this lesson you can

  • Share functions between scripts by keeping them in a library file and loading it with source
  • Find a script's own folder with dirname and ${BASH_SOURCE[0]}, so it can load its library from any folder

Key takeaways

  • A library is a file of functions only. Scripts load it with source, so a fix made in the library reaches every script that uses it.
  • A library is never run on its own, so it needs no #! line and no chmod +x.
  • source mix-lib.sh looks in the folder you're in, not the script's folder, so the script breaks when it's run from anywhere else.
  • here=$(dirname "${BASH_SOURCE[0]}") keeps the folder the script is saved in, so source "$here/mix-lib.sh" works from any folder.

The shuffler is turning into two scripts: mix.sh prints the mix, and a new stats.sh will say how long it is. Both need banner, and the stats need sum_minutes too. Copying the functions into both works for a week, until you fix a bug in one copy and the other keeps it. A garage has a better habit: one shared toolbox, and every project borrows the hammer from it instead of buying its own. In a script, the shared toolbox is a library, a file of functions any script can load. It's how bigger tools stay organized: spend will keep its functions in a library, and so do the scripts that set up servers and deploy websites at work.

A library: a file of functions

In the shuffler folder from Functions: Naming a Job (mkdir -p ~/terminal-practice/shuffler && cd ~/terminal-practice/shuffler makes it), write mix-lib.sh with nano. It holds banner and the sum_minutes from Local Variables and Getting a Value Back:

# mix-lib.sh: functions for the playlist scripts.
# Not run on its own: a script loads it with source.

banner() {
  echo "------------------------------"
  echo "  $1"
  echo "------------------------------"
}

sum_minutes() {
  local total=0 m
  for m in "$@"; do
    total=$((total + m))
  done
  echo "$total"
}

A library has no #! line and needs no chmod +x, because nobody runs it: scripts load it. Keep it to definitions. A library that also did something, such as printing a line or deleting a file, would do it in every script that loads it.

source: load the library into a script

Write stats.sh, and make it runnable with chmod +x stats.sh:

#!/usr/bin/env bash
# stats.sh: how long is the road-trip mix?
source mix-lib.sh

banner "Mix stats"
echo "Total: $(sum_minutes 4 3 5 4 6 3) minutes"
./stats.sh
------------------------------
  Mix stats
------------------------------
Total: 25 minutes

source is the same command you used in Startup Files: .bashrc, .zshrc and source: it reads a file into the shell that's running, here the script's own, so the functions it defines stay. . (read "dot") is its short name: . mix-lib.sh does the same.

Run it from somewhere else, and it breaks

Now run the script from your home folder, by its full path:

cd ~
~/terminal-practice/shuffler/stats.sh
/home/sam/terminal-practice/shuffler/stats.sh: line 3: mix-lib.sh: No such file or directory
/home/sam/terminal-practice/shuffler/stats.sh: line 5: banner: command not found
/home/sam/terminal-practice/shuffler/stats.sh: line 6: sum_minutes: command not found
Total:  minutes

source mix-lib.sh looked for the library in the folder you're in, your home folder. It's the same trap as cat todo.txt in Run It from Anywhere: ~/bin on Your PATH. A full path such as ~/terminal-practice/shuffler/mix-lib.sh would fix it until you move the folder or give it to a friend. The script needs to know where it lives.

Where the script lives: ${BASH_SOURCE[0]}

BASH_SOURCE is a variable bash fills in with the path of the file it's reading. It holds a list, and [0] picks the first item, the file running now (the next topic, Arrays, explains lists like it). dirname cuts the file's name off a path and leaves its folder. Write where.sh, make it runnable, and run it two ways from your home folder:

#!/usr/bin/env bash
# where.sh: where does this script live?
echo "This file: ${BASH_SOURCE[0]}"
echo "Its folder: $(dirname "${BASH_SOURCE[0]}")"
cd ~
terminal-practice/shuffler/where.sh
~/terminal-practice/shuffler/where.sh
This file: terminal-practice/shuffler/where.sh
Its folder: terminal-practice/shuffler
This file: /home/sam/terminal-practice/shuffler/where.sh
Its folder: /home/sam/terminal-practice/shuffler

However you start the script, the folder is right. So change the top of stats.sh to keep it in here, and load the library from there:

here=$(dirname "${BASH_SOURCE[0]}")
source "$here/mix-lib.sh"
cd /tmp
~/terminal-practice/shuffler/stats.sh
------------------------------
  Mix stats
------------------------------
Total: 25 minutes

One case still trips it: a link made with ln -s in ~/bin (Shortcuts to Files: ln -s). Then ${BASH_SOURCE[0]} is the link's path, and here is ~/bin. readlink -f follows a link to the real file first, so the line becomes here=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")"). On a Mac, readlink -f needs macOS 12.3 or newer.

BASH_SOURCE is bash's own; zsh doesn't have it, which is one more reason every script in this course starts with #!/usr/bin/env bash.

Check yourself

2 questions to try on the lesson page.

So what

A library and two lines at the top of each script let several tools share one set of functions and run from any folder, which is how a pile of scripts grows into an application.

Key terms

Cheat sheets

Related