42

Given this example:

mkdir a
ln -s a b
ln -s b c
ln -s c d

If I execute:

ls -l d

It will show:

d -> c

Is there a way for ls or any other linux command to show d -> c -> b -> a instead?

Kalecser
  • 523
  • 1
  • 4
  • 5
  • I cross posted this as http://stackoverflow.com/questions/2320277/detect-loops-in-a-hash-representation-of-file-system-symbolic-links I hope you don't mind. – Paul Feb 23 '10 at 17:30

4 Answers4

82

Just use namei:

$ namei d
f: d
 l d -> c
   l c -> b
     l b -> a
       d a
peterh
  • 4,914
  • 13
  • 29
  • 44
Mike
  • 836
  • 1
  • 7
  • 2
21

readlink -e <link>

readlink [OPTION]... FILE

  • -e, --canonicalize-existing
    canonicalize by following every symlink in every component of the given name recursively, all components must exist
$ mkdir testlink
$ cd testlink
pjb@pjb-desktop:~/testlink$ ln -s c b
pjb@pjb-desktop:~/testlink$ ln -s b a
pjb@pjb-desktop:~/testlink$ ls -l 
total 0
lrwxrwxrwx 1 pjb pjb 1 2010-02-23 08:48 a -> b
lrwxrwxrwx 1 pjb pjb 1 2010-02-23 08:48 b -> c
pjb@pjb-desktop:~/testlink$ echo foo > c
pjb@pjb-desktop:~/testlink$ cat a
foo
pjb@pjb-desktop:~/testlink$ readlink -e a
/home/pjb/testlink/c

note: readlink a by itself returns b

note #2: together with find -l, a utility to list the chains could easily be written in perl, but also has to be smart enough to detect loops

readlink will not output anything if you have a loop. This is better than getting stuck, I suppose.

pjb@pjb-desktop:~/testlink$ ln -sf a c
pjb@pjb-desktop:~/testlink$ ls -l 
total 0
lrwxrwxrwx 1 pjb pjb 1 2010-02-23 08:48 a -> b
lrwxrwxrwx 1 pjb pjb 1 2010-02-23 08:48 b -> c
lrwxrwxrwx 1 pjb pjb 1 2010-02-23 09:03 c -> a
pjb@pjb-desktop:~/testlink$ readlink -e a
pjb@pjb-desktop:~/testlink$ # (note: no output)
czerny
  • 235
  • 1
  • 2
  • 8
Paul
  • 1,626
  • 15
  • 19
4

Here is a recursive function in Bash:

chain() { export chain; local link target; if [[ -z $chain ]]; then chain="$1"; fi; link=$(stat --printf=%N $1); while [[ $link =~ \-\> ]]; do target="${link##*\`}"; target="${target%\'}"; chain+=" -> $target"; chain "$target"; return; done; echo "$chain"; unset chain; }

On multiple lines:

chain() {
    export chain
    local link target
    if [[ -z $chain ]]
    then
        chain="$1"
    fi
    link=$(stat --printf=%N "$1")
    while [[ $link =~ \-\> ]]
    do
        target="${link##*\`}"
        target="${target%\'}"
        chain+=" -> $target"
        if [[ ! $target =~ / && $1 =~ / ]]
        then
            target="${1%/*}/$target"
        fi
        chain "$target"
        return
    done
    echo "$chain"
    unset chain
}

Examples:

$ chain d
d -> c -> b -> a
$ chain c
c -> b -> a
$ chain a
a

It requires stat(1) which may not be present on some systems.

It will fail if names contain backticks, single quotes, or "->". It gets stuck in a loop with symlink loops (this could be solved using an associative array in Bash 4). It exports a variable called "chain" without regard to whether it's already in use.

There may be other problems with it.

Edit:

Fixed a problem with some relative symlinks. Some still don't work, but the version below doesn't require the target of the link to exist.

Added a version that uses readlink:

chain ()
{
    export chain;
    local target;
    if [[ -z $chain ]]; then
        chain="$1";
    fi;
    target=$(readlink "$1");
    while [[ $target ]]; do
        chain+=" -> $target";
        if [[ ! $target =~ / && $1 =~ / ]]
        then
            target="${1%/*}/$target"
        fi
        chain "$target";
        return;
    done;
    echo "$chain";
    unset chain
}
Dennis Williamson
  • 60,515
  • 14
  • 113
  • 148
  • I've tested your script and it really works but I prefer something simpler so I've accepted the other answer even if incomplete. – Kalecser Feb 23 '10 at 21:02
  • 1
    Nice script. Sometimes I want to see the entire chain, and `readlink` doesn't seem to show that. Java on Ubuntu is: `/usr/bin/java -> /etc/alternatives/java -> /usr/lib/jvm/java-6-openjdk/jre/bin/java` – Stefan Lasiewski Oct 20 '10 at 19:33
0

You could just postprocess the output of namei with something like awk or grep to get just the lines you want:

namei d | awk '$1=="l"'

or

namei d | egrep -e "->"
Andrew Williams
  • 667
  • 8
  • 20
Tony
  • 1