Looking for tested utility to get the absolute path to a relative symlink target

readlinksymlinkzsh

NB: In the original title of this post, I used the word standard in the everyday sense of "well-established" (and therefore time-tested, as a contrast to quick solutions I could roll myself). In the context of Unix-talk, however, the word standard has a very specific (and very different) technical meaning. This alternative, more correct, interpretation of the word standard in the title rendered the rest of my post inconsistent. (Thanks to Stéphane Chazelas for pointing this out.) Therefore, I've revised the title, replacing standard with tested.


As an example of the problem I refer to in the title, suppose I have the directory structure shown below

/tmp/example
├── a/
│   └── b/
│       └── c/
│           └── d/
│               └── target
└── A/
    └── B/
        ├── C/
        │   └── D/
        │       └── symlink-0 -> ../../symlink-1/b/c/d/target
        │
        └── symlink-1 -> /tmp/example/a

Note that /tmp/example/A/B/C/D/symlink-0 is a symbolic link whose immediate target is a relative path:

$ readlink /tmp/example/A/B/C/D/symlink-0
../../symlink-1/b/c/d/target

I want to get the absolute path corresponding to this immediate target. IOW, I want to perform the partial resolution

/tmp/example/A/B/C/D/symlink-0 -> /tmp/example/A/B/symlink-1/b/c/d/target

Is there a standard (or at least well-established and time-tested) Unix utility to do this?


Note that readlink -f resolves paths fully (to a symlink-free path); for the case discussed here, for example:

$ readlink -f /tmp/example/A/B/C/D/symlink-0
/tmp/example/a/b/c/d/target

This means that readlink -f is not the answer to the question I'm asking here.


I could roll my own, by adding adequate error-checking, etc., to something like the following zsh function:

canonicalize () {
    local abspath=$(dirname $1)/$(readlink $1)
    printf -- '%s\n' $abspath:a
}

…but I've learned (the hard way) not to underestimate the difficulty of implementing this sort of utility robustly, so I'd prefer to use existing tools, if possible.


FWIW, the script below generates this post's example:

mkdir -p /tmp/example/a/b/c/d /tmp/example/A/B/C/D
touch /tmp/example/a/b/c/d/target
ln -s /tmp/example/a /tmp/example/A/B/symlink-1
ln -s ../../symlink-1/b/c/d/target /tmp/example/A/B/C/D/symlink-0

Best Answer

I'm not aware of any such utility. However, your approach is flawed (and I agree it's very hard to get it right).

The :a modifier computes an absolute path without any access to the file system. In particular, it changes a/b/.. to a regardless of whether a/b is a symlink or not.

If you have a /a/b -> ../foo symlink, that is /foo only if /a is not a symlink itself. Even if you canonicalize the $(dirname $1), that would still not work for symlinks like /a/b -> x/../../foo if x itself is a symlink.

In your case:

/tmp/example/A/B/C/D/symlink-0 -> ../../symlink-1/b/c/d/target

resolves to /tmp/example/A/B/symlink-1/b/c/d/target only because neither /tmp/example/A/B/C/D nor /tmp/example/A/B/C are symlinks.

In other words, to get an absolute path to the target of a symlink that is free of .. components, you may have to resolve more than one symlink and you cannot do it alone by combining the path of the file and the target of the symlink.

If you want such a path, the easiest (and I'd say only reasonable and/or useful) approach is to get the canonical path of that target of the symlink, that is where all the path components are neither .., . nor symlinks except possibly for the last. For that, you could do:

zmodload -F zsh/stat b:zstat
canonicalize1() {
  if [ -L "$1" ]; then
    local link
    zstat -A link +link -- "$1" || return
    case $link in
      (/*) ;;
      (*)
        case $1:h in
          (*/) link=$1:h$link;;
          (*) link=$1:h/$link;;
        esac;;
    esac
    printf '%s\n' $link:h:A/$link:t
  else
    printf '%s\n' $1:A
  fi
}

On your /tmp/example/A/B/C/D/symlink-0 symlinks, that still gives /tmp/example/a/b/c/d/target, as the target of the symlink is not a symlink, but if that's not what you want, then I'd ask: what result would you want when doing:

canonicalize1 /tmp/x/y

Where /tmp/x/y is a symlink to ../foo and /tmp/x a symlink to /a/b/c and /a, /a/b, /a/b/c are also themselves symlinks?

Related Question