Emmanuel Corels
← Blog
Linux & Server Operations

Make a Bash script find its own files from any directory

Stop confusing the caller’s working directory with the script’s location; handle spaces, sourced files, relative symlinks and physical paths deliberately.

By Emmanuel Corels

A deployment script works when launched from its own folder and fails from cron, systemd or /tmp. The usual bug is a relative path such as ./config/app.conf. That path is resolved from the process working directory—not the directory containing the script.

Reproduce the boundary before fixing it

cd /opt/myapp/scripts
./deploy.sh

cd /tmp
/opt/myapp/scripts/deploy.sh

Print both concepts during diagnosis:

printf 'working directory: %s
' "$PWD"
printf 'script source: %s
' "${BASH_SOURCE[0]}"

$PWD belongs to the caller’s current shell context. GNU Bash documents BASH_SOURCE as an array of source filenames corresponding to the current function call stack. At top level, element zero identifies the executing or sourced file.

The practical Bash-only answer

#!/usr/bin/env bash
set -Eeuo pipefail

SCRIPT_DIR="$(
  cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1
  pwd -P
)"
readonly SCRIPT_DIR

CONFIG_FILE="$SCRIPT_DIR/../config/app.conf"

Quoting preserves spaces and wildcard characters. -- prevents a path beginning with a dash from being interpreted as an option. The command substitution runs the cd in a subshell, so the caller’s working directory remains unchanged. pwd -P emits a physical path without directory symlinks.

Fail visibly if location cannot be resolved

script_source=${BASH_SOURCE[0]}
script_parent=$(dirname -- "$script_source") || {
  printf 'cannot parse script path: %s
' "$script_source" >&2
  exit 1
}
SCRIPT_DIR=$(CDPATH= cd -- "$script_parent" && pwd -P) || {
  printf 'cannot enter script directory: %s
' "$script_parent" >&2
  exit 1
}

Setting CDPATH= prevents a user’s search path from changing cd behavior or printing a directory name into captured output. Avoid suppressing errors permanently; suppression is convenient in a compact idiom but unhelpful in an unattended job.

Resolve a symlinked script entry point

The simple form resolves directory symlinks but not a final script filename that is itself a symlink. If /usr/local/bin/deploy points to ../../opt/myapp/scripts/deploy.sh, walk each link relative to the directory containing that link:

resolve_script_dir() {
  local source=${BASH_SOURCE[0]}
  local dir target

  while [[ -L $source ]]; do
    dir=$(CDPATH= cd -P -- "$(dirname -- "$source")" && pwd) ||
      return 1
    target=$(readlink -- "$source") || return 1
    if [[ $target == /* ]]; then
      source=$target
    else
      source=$dir/$target
    fi
  done

  CDPATH= cd -P -- "$(dirname -- "$source")" && pwd
}

SCRIPT_DIR=$(resolve_script_dir) || {
  printf 'failed to resolve script directory
' >&2
  exit 1
}

A relative symlink target is relative to the link’s directory, not the caller’s working directory. That detail is why blindly passing readlink output to dirname fails for chained relative links.

Decide whether “sourced file” or “main entry point” is intended

# lib/common.sh
LIB_DIR="$(
  CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P
)"

# The outermost script in this Bash source stack:
ENTRY_SOURCE=${BASH_SOURCE[${#BASH_SOURCE[@]} - 1]}

Inside a sourced library, BASH_SOURCE[0] means the library file. That is normally correct for locating assets owned by the library. If the requirement is the original entry script, inspect the array and document that contract rather than swapping indices by accident.

Do not use $0 as a universal replacement

For a directly executed script, $0 often contains the invocation path. When a file is sourced, it identifies the shell or outer script instead. Under bash -c, wrappers and tests, its meaning can be controlled by the caller. Use it only when you specifically need the command as invoked.

Test path edge cases automatically

test_root=$(mktemp -d)
trap 'rm -rf -- "$test_root"' EXIT

mkdir -p "$test_root/real dir/bin" "$test_root/links"
cp deploy.sh "$test_root/real dir/bin/"
ln -s "../real dir/bin/deploy.sh" "$test_root/links/run-deploy"

(
  cd /
  "$test_root/real dir/bin/deploy.sh"
  "$test_root/links/run-deploy"
)

Add tests for spaces, a launch directory unrelated to the application, a relative symlink, an absolute symlink and sourcing if supported. If portability to POSIX sh is required, BASH_SOURCE is unavailable; redesign the invocation contract or pass the resource root explicitly.

Use the resolved root without changing global state

source "$SCRIPT_DIR/lib/logging.sh"
template="$SCRIPT_DIR/templates/nginx.conf"

install -m 0644 -- "$template" /etc/nginx/conf.d/myapp.conf

Avoid cd "$SCRIPT_DIR" as a hidden global side effect unless every later command is intentionally relative to it. Absolute derived paths make the script easier to read, test and embed.

The working directory answers “where was I launched?” The script directory answers “where do my owned resources live?” Reliable automation never treats those as the same question.

Sources: the canonical Stack Overflow solution (question by Jiaaro; accepted answer by dogbane, CC BY-SA) and the GNU Bash Reference Manual for BASH_SOURCE, cd and pwd.

Primary source: Review the official reference ↗