Published on December 2, 2023
Discuss: FreeBSD forum | reddit
View this article's example files
I like writing shell scripts, but one thing that has bugged me for years is that I don't reliably know how to include relative scripts. I run into three key problems:
. relative/script.sh
depends on the invocation dir.- Symlinking the command results in inconsistent command name and location.
- Almost all internet discussion about shell scripting is about Bash, whereas I'm using
sh(1)
on FreeBSD (aka Bourne shell).
tl;dr: The Solution
Use $(dirname $(realpath $0))
in your command files. realpath(1)
Problem 1: The invocation dir matters
The obvious way to include relative files is . relative/script.sh
.
We have the following file structure:
.
├── bin
│ └── hello.sh
└── lib
└── libhello.sh
bin/hello.sh:
#!/bin/sh
. lib/libhello.sh
hello
lib/libhello.sh:
hello() {
echo hello, world
}
Running ./bin/hello.sh
works as expected, whereas cd bin && ./hello.sh
fails:
./bin/hello.sh
hello, world
cd bin && ./hello.sh
.: cannot open lib/libhello.sh: No such file or directory
We need some way of sourcing relative to the script path, rather than the invocation directory.
Naive solution: Use $0
to find the script location
$0
returns the name of the invoked command, including any directory prefixing.
We can use it to require relative files:
bin/hello.sh:
#!/bin/sh
LIB=$(dirname $0)/../lib
. $LIB/libhello.sh
hello
Now the command succeeds, regardless of which dir we invoke it from:
./bin/hello.sh
hello, world
cd bin && ./hello.sh
hello, world
However, $0
is the name of the command passed to the shell - which may not be the actual command file when using symlinks.
Problem 2: Symlinks make the script name unreliable
Referencing $0
works, until we symlink the file.
This is the new file structure:
.
├── actual-hello
│ ├── actual-hello.sh
│ └── libhello.sh
└── bin
└── hello.sh -> ../actual-hello/actual-hello.sh
Now running ./bin/hello.sh
tries to include the file relative to bin/hello.sh
, when it should be relative to actual-hello/actual-hello.sh
.
Fortunately, FreeBSD gives us realpath(1)
to identify the real file referenced by a symlink.
Solution: Use realpath(1)
to source relative to the real file
actual-hello/actual-hello.sh:
#!/bin/sh
LIB=$(dirname $(realpath $0))
. $LIB/libhello.sh
hello
$(realpath $0)
returns the path of the real file, so now we can source relative to it.
Our symlink works, regardless the invocation dir:
./bin/hello.sh
hello, world
cd bin && ./hello.sh
hello, world
Bonus solution: A lib helper function
Now that we know the basic technique, we can define a BASE
var and write our shell library relative to it.
Here's the file structure:
.
├── actual-hello
│ ├── bin
│ │ └── actual-hello.sh
│ └── lib
│ ├── libhello.sh
│ └── libhelper.sh
└── bin
└── hello.sh -> ../actual-hello/bin/actual-hello.sh
actual-hello/bin/actual-hello.sh:
#!/bin/sh
BASE=$(dirname $(realpath $0))/..
. $BASE/lib/libhelper.sh
require_lib "hello"
hello
actual-hello/lib/libhelper.sh:
: ${BASE:?}
LIB=$BASE/lib
require_lib() {
. $LIB/lib${1}.sh
}
Conclusion
It's possible to write extremely powerful tools using shell scripts, and modularize your code. Bourne shell doesn't provide a clear mechanism for referencing relative files. By establishing a command file's base dir, you can reliably reference relative shell scripts.