0

I have morass of chained symlinks like this scattered around:

A (symlink) -> B (symlink) -> C (file)

Some may even involve longer chains, I'm not sure yet.

When manually examining a single file it's easy to see what's going on but I'm looking for an automated way to find these and (preferably) clean them up.

In the example above I would want the B->C symlink to stay in place but the A->B symlink would become A->C

Identifying them if the main goal; the quantity might end up being sufficiently low that fixing them manually wouldn't be a big deal. But finding them all manually isn't feasible.

I'm aware of the "symlinks" utility and it is useful in many scenarios such as finding dangling symlinks, but it doesn't seem to have the ability to detect "chains" like this

I'm aware of "find -type l" as well as the -L option for find but they don't seem to work together as "find -L -type l" always returns an empty response.

I make use of "find -type l -exec ls -l {} +" (which works better than find's -ls option) and tried to use it to obtain a list of symlink destinations which I could then check to see if they're symlinks or not, however, all the symlinks are relative rather than absolute so the output is a bit messy

for example I get outputs like this:

lrwxrwxrwx 1 username 50 Nov  5 01:00  ./a/b/c/d -> ../../e/f/g/h

From that I have to think a bit to figure out that the actual symlink destination is ./a/e/f/g/h (and I can then check to see if it's a symlink or not), not really ideal for automation

If the symlinks were (temporarily) absolute rather than relative this would be easier but the "symlinks" utility can only convert absolute -> relative; as far as I can tell it can't convert relative -> absolute.

1 Answers1

1

Following the entire link chain

realpath

realpath(1) gets you the absolute destination of a link. But it only gives you the final destination, so if you have a link chain, it will only show you the path of the last real file/folder that the chain eventually points to.

So in your case, if ../../e/f/g/h is real file or folder, you will see the absolute path. But if one of the components is also a symlink, it will keep recursively until it resolves all the symlinks in the way.

readlink

You could also use the readlink(1) command with the -f or -e flags, which will give you similar result to realpath.

  -f, --canonicalize            canonicalize by following every symlink in
                                every component of the given name recursively;
                                all but the last component must exist
  -e, --canonicalize-existing   canonicalize by following every symlink in
                                every component of the given name recursively,
                                all components must exist

Avoid following the entire link chain

If I understand correctly, you don't want to follow the chain to the last component, but to only get the absolute path of the actual link. It's a little ugly, but you can do something such as:

LINK=./a/b/c/d
/usr/bin/realpath --no-symlinks "$(dirname ${LINK})/$(readlink ${LINK})"

Explanation:

$ dirname $LINK # dirname ./a/b/c/d
./a/b/c

$ readlink $LINK # readlink ./a/b/c/d
../../e/f/g/h

$ echo "$(dirname ${LINK})/$(readlink ${LINK})" 
./a/b/c/../../e/f/g/h

$ /usr/bin/realpath --no-symlinks "$(dirname ${LINK})/$(readlink ${LINK})"
/home/a/e/f/g/h

If you only want to see the link relative to your CWD, you can add the --relative-to argument:

$ /usr/bin/realpath --no-symlinks --relative-to ./ "$(dirname ${LINK})/$(readlink ${LINK})"
a/e/f/g/h

Using find

You can get the argument for the realpath command I've shown before by using the find command:

$ find ./ -type l -printf "%h/%l\n"
./a/b/c/../../e/f/g/h

From man find:

%h     Leading directories of file's name (all but the last element).   If
       the  file  name  contains  no  slashes  (since it is in the current
       directory) the %h specifier expands to ".".

%l     Object of symbolic link (empty string if file is not a symbolic link).

Then you could combine it with the realpath command from above.

$ find ./ -type l -printf "%h/%l\n" | xargs /usr/bin/realpath -s
/home/a/e/f/g/h

If you only want to see the link relative to your CWD:

$ find ./ -type l -printf "%h/%l\n" | xargs /usr/bin/realpath -s --relative-to ./
a/e/f/g/h
aviro
  • 131
  • 3
  • Interesting... the fact that it follows the chain to the end isn't ideal but I might be able to cross-reference the realpath output with the target information on "ls -l" to see if they match – Displayname71 Nov 08 '21 at 13:01
  • I've edited my answer and added another option, the last one might be what you're looking for. – aviro Nov 08 '21 at 15:59
  • @Displayname71, I have added some more options to my answer, hope one of them will fit your needs – aviro Nov 08 '21 at 18:32