Finding a Bash Script’s Directory Reliably
Getting the directory path of a running script is a common requirement in bash, whether you’re sourcing files, building paths to config directories, or navigating relative to the script itself. The naive approach often fails with symbolic links or when the script is sourced rather than executed directly.
The Problem
Using $0 alone is unreliable. If your script is a symlink, dirname $0 returns the symlink’s directory, not the actual script location. If the script is sourced, $0 refers to the shell itself. These edge cases break path resolution in production scripts.
The Solution
Use readlink -f to resolve the absolute path, then pass it to dirname:
script_dir=$(dirname "$(readlink -f "$0")")
Breaking this down:
readlink -f "$0"— resolves all symlinks and returns the absolute path to the actual script filedirname ...— extracts the directory portion of that pathscript_dir=...— stores the result in a variable
Handling Sourced Scripts
When a script is sourced (rather than executed), $0 points to the parent shell, not the sourced file. Use ${BASH_SOURCE[0]} instead:
script_dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
${BASH_SOURCE[0]} always references the currently executing file in the stack, so it works correctly whether the script is executed or sourced.
Practical Example
Here’s a complete script that sets up directories reliably:
#!/bin/bash
script_dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
config_dir="$script_dir/config"
data_dir="$script_dir/data"
echo "Script location: $script_dir"
echo "Config directory: $config_dir"
echo "Data directory: $data_dir"
if [[ ! -d "$config_dir" ]]; then
mkdir -p "$config_dir"
fi
Edge Cases to Consider
Symlinked script: If /usr/bin/myscript is a symlink to /opt/app/myscript.sh, readlink -f returns /opt/app/myscript.sh and script_dir becomes /opt/app/. This is the correct behavior for finding supporting files.
Script in PATH: If you run a script from PATH (e.g., just typing myscript), the absolute path resolution still works correctly.
Relative symlinks: Even complex chains of relative symlinks are resolved completely by readlink -f, unlike older -e flag implementations.
No readlink available: On some minimal systems, readlink -f may not be available. As a fallback:
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
This uses cd and pwd to normalize the path, though it doesn’t resolve symlinks. For most scripts, this is acceptable.
Best Practice
Use this as your standard:
script_dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
It handles symlinks, sourcing, relative paths, and works across different invocation methods. Store it near the top of your script before any file operations that depend on the script’s location.
