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.