How to Get Absolute Script Path with Symlink Resolution in macOS Shell Scripts


1 views

When working with shell scripts on macOS, you'll quickly discover that the readlink command behaves differently than its Linux counterpart. The Linux version has useful flags like -m (canonicalize) and -n (no newline), but macOS's BSD-based implementation lacks these features.

Here's a function that works consistently across both Linux and macOS:

get_absolute_script_path() {
    # Try Linux-style readlink first
    if command -v readlink >/dev/null 2>&1 && \
       readlink --version 2>&1 | grep -q GNU; then
        readlink -mn "$0"
    else
        # Fallback for macOS/BSD
        perl -MCwd -e 'print Cwd::abs_path(shift)' "$0"
    fi
}

SCRIPT_PATH=$(get_absolute_script_path)
echo "Running script from: $SCRIPT_PATH"

If you prefer not to use Perl, consider these options:

# Using Python (macOS ships with Python)
python -c "import os; print(os.path.realpath('$0'))"

# Using Homebrew-installed coreutils (recommended for frequent use)
if brew list | grep -q coreutils; then
    greadlink -f "$0"
fi

Getting the correct script path is crucial when:

  • Your script needs to reference files relative to its location
  • You're running through symlinks in /usr/local/bin
  • You need to determine installation paths for dependencies

The solution accounts for:

# Multiple levels of symlinks
ln -s /path/to/script /tmp/link1
ln -s /tmp/link1 /tmp/link2

# Paths containing spaces
SCRIPT_WITH_SPACES="My Scripts/test.sh"

When writing shell scripts on macOS, developers often need to reliably determine the absolute path of the currently executing script, including resolving any symbolic links. While Linux systems provide straightforward solutions using readlink -f or realpath, macOS's BSD-based tools behave differently.

The common Linux solution:

$(readlink -f "$0")

won't work on macOS because:

  • BSD readlink doesn't support the -f flag
  • macOS lacks the realpath command entirely

Here are three reliable methods to get the absolute script path on macOS:

1. Using Python (Cross-Platform)

SCRIPT_PATH=$(python -c "import os; print(os.path.realpath('$0'))")

2. Pure Shell Solution

SCRIPT_PATH=$(
  cd "$(dirname "$0")" || exit
  pwd -P
)/$(basename "$0")

3. Using Homebrew-installed coreutils

If you have Homebrew:

brew install coreutils
SCRIPT_PATH=$(greadlink -f "$0")

For production scripts, consider these scenarios:

# Handle spaces in path
SCRIPT_PATH=$(
  cd "$(dirname "$0")" &> /dev/null || exit 1
  pwd -P
)/$(basename "$0")
SCRIPT_PATH=${SCRIPT_PATH%/*}  # If you just need the directory

The Python method is most reliable but incurs interpreter startup overhead. For frequently-called scripts, the pure shell solution offers better performance.