How can a bash script know the directory it is installed in when it is sourced with . operator?

I believe $(dirname "$BASH_SOURCE") will do what you want, as long as the file you are sourcing is not a symlink.

If the file you are sourcing may be a symlink, you can do something like the following to get the true directory:

PRG="$BASH_SOURCE"
progname=`basename "$BASH_SOURCE"`

while [ -h "$PRG" ] ; do
    ls=`ls -ld "$PRG"`
    link=`expr "$ls" : '.*-> \(.*\)$'`
    if expr "$link" : '/.*' > /dev/null; then
        PRG="$link"
    else
        PRG=`dirname "$PRG"`"/$link"
    fi
done

dir=$(dirname "$PRG")

Here is what might be an elegant solution:

script_path="${BASH_SOURCE[0]}"
script_dir="$(cd "$(dirname "${script_path}")" && pwd)"

This will not, however, work when sourcing links. In that case, one might do

script_path="$(readlink -f "$(readlink "${BASH_SOURCE[0]}")")"
script_dir="$(cd "$(dirname "${script_path}")" && pwd)"

Things to note:

  • arrays like ${array[x]} are not POSIX compliant - but then, the BASH_SOURCE array is only available in Bash, anyway
  • on macOS, the native BSD readlink does not support -f, so you might have to install GNU readlink using e.g. brew by brew install coreutils and replace readlink by greadlink
  • depending on your use case, you might want to use the -e or -m switches instead of -f plus possibly -n; see readlink man page for details

A different take on the problem - if you're using "." in order to set environment variables, another standard way to do this is to have your script echo variable setting commands, e.g.:

# settings.sh
echo export CLASSPATH=${CLASSPATH}:/foo/bar

then eval the output:

eval $(/path/to/settings.sh)

That's how packages like modules work. This way also makes it easy to support shells derived from sh (X=...; export X) and csh (setenv X ...)