6

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
  • What do you mean by standard? Neither readlink nor zsh are standard utilities for instance. – Stéphane Chazelas Mar 19 '17 at 22:11
  • @StéphaneChazelas: sorry for my sloppy wording; since reading your comment, I've made multiple attempts to come up with a better wording, but I've rejected them because I'm sure that someone can point out how that wording too is wrong in one way or another. Let's say that if I can get the utility as part of a stable package from http.us.debian.org/debian, it's "standard" enough for me. I.e. by "standard" I meant "well-established and time-tested". – kjo Mar 19 '17 at 22:21
2

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?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.