5

Finding the java binaries can be painful:

  • which java gives /usr/bin/java
  • lh $(which java) gives /usr/bin/java -> /etc/alternatives/java
  • lh /etc/alternatives/java gives /etc/alternatives/java -> /usr/lib/jvm/jdk1.8.0_66/bin/java

Is there a way to automatically follow the symlink-chain and print all members? e.g. whichfollow or follow /usr/bin/java could give:

/usr/bin/java
-> /etc/alternatives/java
-> /usr/lib/jvm/jdk1.8.0_66/bin/java
Toby Speight
  • 23,550
  • 47
  • 57
  • 84
Edward
  • 3,883
  • 7
  • 32
  • 76
  • 1
    @AnthonyGeoghegan: It's not a duplicate, because this question is about printing the _chain_ of symlinks. – mklement0 Oct 21 '15 at 12:11
  • 1
    @mklement0 I figured that out after (and then provided an alternative answer) but I don't think it's possible to unflag. In any case, I left the comment as the answers in the related question would still be useful for people interested in seeing how symlinks resolve. Great answer, yourself. – Anthony Geoghegan Oct 21 '15 at 13:00

4 Answers4

9

In addition to the readlink command, GNU/Linux users can use the namei command from the util-linux package. According to its man page:

namei uses its arguments as pathnames to any type of Unix file (symlinks, files, directories, and so forth). namei then follows each pathname until an endpoint is found (a file, a directory, a device node, etc). If it finds a symbolic link, it shows the link, and starts following it, indenting the output to show the context.

Its output isn’t as pretty as you would like but it shows each path components and shows if its a directory, symbolic link, socket, block device, character device, FIFO (named pipe) or regular file.

Example usage:

$ namei  /usr/bin/java

f: /usr/bin/java
 d /
 d usr
 d bin
 l java -> /etc/alternatives/java
   d /
   d etc
   d alternatives
   l java -> /usr/lib/jvm/jre-1.7.0-openjdk/bin/java
     d /
     d usr
     d lib
     d jvm
     l jre-1.7.0-openjdk -> java-1.7.0-openjdk-1.7.0.85/jre
       d java-1.7.0-openjdk-1.7.0.85
       d jre
     d bin
     - java
Anthony Geoghegan
  • 10,032
  • 4
  • 42
  • 50
  • 2
    Great stuff; looks like `namei` is preinstalled on many distros and can list additional info, such as mode bits. The one thing I wished it did was to resolve relative target paths to absolute ones; without it, it's not always obvious what path is being referred to, especially with `..` components. – mklement0 Oct 21 '15 at 19:52
5

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.

    • Simplest installation, with Node.js installed: npm install rreadlink -g
    • Example:

      $ rreadlink -1 "$(which awk)"
      /usr/bin/awk
      /etc/alternatives/awk
      /usr/bin/gawk
      
  • 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 "$@"
Community
  • 1
  • 1
mklement0
  • 245,023
  • 45
  • 419
  • 492
2

Consider installing chase:

Example output:

$ chase --verbose /usr/bin/java
/usr/bin/java
-> /etc/alternatives/java
-> /usr/lib/jvm/java-7-openjdk-amd64/jre/bin/java
/usr/lib/jvm/java-7-openjdk-amd64/jre/bin/java

Package description:

Package: chase
State: installed
Automatically installed: no
Version: 0.5.2-4build2
Priority: optional
Section: universe/utils
Maintainer: Ubuntu Developers
Architecture: amd64
Uncompressed Size: 61.4 k
Depends: libc6 (>= 2.3.4), libgc1c2 (>= 1:7.2d)
Conflicts: chase
Description: Follow a symlink and print out its target file
Chase is a small utility for tracking down the actual file that a symbolic link points to - chasing the symlink, if you will. The result of a successful run is guaranteed to be an existing file which is not a symbolic link.

Toby Speight
  • 23,550
  • 47
  • 57
  • 84
  • Good to know; also good to see that `chase` has symlink-loop detection. Unlike `namei`, `chase` commendably always prints the _ultimate_ target path as an _absolute_ path, but I wish it also did it for _intermediate_ ones whose targets are defined as _relative_ paths (possibly in _addition_ to the relative path), as it's not always obvious what path is being referred to, especially with `..` – mklement0 Oct 21 '15 at 20:02
1

You can use readlink with a while loop. The function below would work:

function follow {
  path="$1"
  echo "$path"
  while path=$(readlink "$path"); do
    echo "-> $path"
  done
}

follow "/usr/bin/java"
arco444
  • 19,715
  • 10
  • 57
  • 59
  • This doesn't work with relative links, unless they happen to be in the current working directory (or, sometimes, a cousin directory if there's one or more `..` components at the beginning. – Toby Speight Oct 21 '15 at 12:16