Note: namei (see Anthony Geoghegan's answer) and chase (see Toby Speight's answer) are great Linux options; this answer offers:
* cross-platform solutions
* printing of absolute paths for every step of the chain, even if the symlinks are defined with relative paths.
- Consider the
typex utility (written by me), which prints the symlink chain of a given utility in the $PATH, using absolute paths in every step (typex also provides additional information, similar to, but more extensive than type).
- Simplest installation, with Node.js installed:
npm install typex -g
- Example (note how version information, obtained with
--version, is appended - wouldn't work for java, however, which uses -version):
$ typex awk
BINARY: /usr/bin/awk@ -> /etc/alternatives/awk@ -> /usr/bin/gawk [GNU Awk 4.0.1]
rreadlink is a lower-level utility (written by me) that prints the symlink chain as absolute paths for any given filesystem path.
Below is rreadlinkchain(), a fully POSIX-compliant script / function - it uses only POSIX shell language features and only POSIX-compliant utility calls. It is a POSIX-compliant variant of the bash function at the heart of the two utilities above, and was gratefully adapted from this answer; applied to your example: rreadlinkchain "$(which java)"
Compatibility notes:
typex and rreadlink, when installed from the npm registry, support both OS X and Linux, but they probably also run on BSD systems with bash, when manually installed.
As stated, the rreadlinkchain() function below is fully POSIX compliant and should work on most Unix-like platforms.
#!/bin/sh
## -------
# SYNOPSIS
# rreadlinkchain <symLink>
# DESCRIPTION
# Recursive readlink: prints the CHAIN OF SYMLINKS from the input
# file to its ultimate target, as ABSOLUTE paths, with each path on a separate
# line.
# Only the ultimate target's path is canonical, though.
# A broken symlink in the chain causes an error that reports the
# non-existent target.
# An input path that is not a symlink will print its own canonical path.
# LIMITATIONS
# - Won't work with filenames with embedded newlines or filenames containing
# the string ' -> '.
# COMPATIBILITY
# Fully POSIX-compliant.
# EXAMPLES
# # Print the symlink chain of the `git` executable in the $PATH.
# rreadlinkchain "$(which git)"
# # Ditto, using single-line `ls -l`-style format ('a@ -> b')
# rreadlinkchain "$(which git)" | sed -nE -e '$!{a\'$'\n''@ -> ' -e '}; p' | tr -d '\n'
# THANKS
# https://stackoverflow.com/a/1116890/45375
rreadlinkchain() ( # execute in *subshell* to localize the effect of `cd`, ...
target=$1 targetDir= targetName= CDPATH=
# Try to make the execution environment as predictable as possible:
# All commands below are invoked via `command`, so we must make sure that
# `command` itself is not redefined as an alias or shell function.
# (Note that command is too inconsistent across shells, so we don't use it.)
# `command` is a *builtin* in bash, dash, ksh, zsh, and some platforms do not
# even have an external utility version of it (e.g, Ubuntu).
# `command` bypasses aliases and shell functions and also finds builtins
# in bash, dash, and ksh. In zsh, option POSIX_BUILTINS must be turned on for
# that to happen.
{ \unalias command; \unset -f command; } >/dev/null 2>&1
[ -n "$ZSH_VERSION" ] && options[POSIX_BUILTINS]=on # make zsh find *builtins* with `command` too.
while :; do
# Unless the file is a symlink OR exists, we report an error - note that using `-e` with a symlink reports the *target*'s existence, not the symlink's.
[ -L "$target" ] || [ -e "$target" ] || { command printf '%s\n' "ERROR: '$target' does not exist." 1>&2; return 1; }
# !! We use `cd` to change to the target's folder
# !! so we can correctly resolve the full dir. path.
command cd "$(command dirname -- "$target")" # note: cd "" is the same as cd . - i.e., a no-op.
targetDir=$PWD
targetName=$(command basename -- "$target")
[ "$targetName" = '/' ] && targetName='' # !! curiously, `basename /` returns '/'
done=0
if [ ! -L "$targetName" ]; then
# We've found the ultimate target (or the input file wasn't a symlink to begin with).
# For the *ultimate* target we want use `pwd -P` to make sure we use the actual, physical directory,
# (not a symlink) to get the *canonical* path.
targetDir=$(command pwd -P)
done=1
fi
# Print (next) path - note that we manually resolve paths ending
# in /. and /.. to make sure we have a normalized path.
if [ "$targetName" = '.' ]; then
command printf '%s\n' "${targetDir%/}"
elif [ "$targetName" = '..' ]; then
# Caveat: something like /var/.. will resolve to /private (assuming
# /var@ -> /private/var), i.e. the '..' is applied AFTER canonicalization.
command printf '%s\n' "$(command dirname -- "${targetDir}")"
else
command printf '%s\n' "${targetDir%/}/$targetName"
fi
# Exit, if we've hit the non-symlink at the end of the chain.
[ "$done" = 1 ] && break
# File is symlink -> continue to resolve.
# Parse `ls -l` output, which, unfortunately, is the only POSIX-compliant
# way to determine a symlink's target. Hypothetically, this can break with
# filenames containig literal ' -> ' and embedded newlines.
target=$(command ls -l -- "$targetName")
target=${target#* -> }
done
)
rreadlinkchain "$@"