45

I have a complete sub-filesystem inside a path /home/user/system containing the standard Linux structure with directories /bin, /home, /root, /usr, /var, /etc,...

This sub-filesystem contain symbolic links, either relative or absolute. The relative symlinks are just fine, they stay within the sub-filesystem under /home/user/system. But absolute symlinks are problematic, as they point to a target outside of the sub-filesystem.

As an example we assume an absolute symlink as follows (seen inside the sub-filesystem):

/usr/file1 -> /usr/lib/file1

In the overall filesystem we have a link at /home/user/system/usr/file1 that now point to a file /usr/lib/file1 outside the sub-filesystem, instead of a file /home/user/system/usr/lib/file1 inside the sub-filesystem.

I would like to have a simple script, preferably a single command line (rsync, chroot, find, ...) that converts every absolute symlink to a relative one.

In the given example, that relative link would become

/usr/file1 -> ../usr/lib/file1
Alex
  • 5,700

7 Answers7

30

With the symlinks utility by Mark Lord (offered by many distributions; if yours doesn't have it, build it from source):

chroot /home/user/system symlinks -cr .

Alternatively, on systems that have a readlink command and a -lname predicate to find (warning: untested code):

cd /home/user/system &&
find . -lname '/*' -exec ksh -c '
  for link; do
    target=$(readlink "$link")
    link=${link#./}
    root=${link//+([!\/])/..}; root=${root#/}; root=${root%..}
    rm "$link"
    ln -s "$root${target#/}" "$link"
  done
' _ {} +
h k
  • 103
  • 1
    This would be a nice solution! However, what does the expression _ {} + mean at the end of the find? Also, I get an error find: paths must precede expression: ksh which does not seem to make sense (as the path preceeds the ksh expression). – Alex Nov 13 '13 at 07:34
  • @Alex _ is $0 for the shell snippet, and {} + is replaced by the list of arguments which become $1, $2, etc. which for link; do … loops over (it's synonymous with for link in "$@"; do …). The error from find is due to a typo (I somehow managed to type backquotes instead of single quotes around the argument to -lname). – Gilles 'SO- stop being evil' Nov 13 '13 at 08:21
  • Now the script runs, but does not as expected. Maybe I was not precise enough, I have updated the question. – Alex Nov 13 '13 at 09:56
  • @Alex What's the problem? I think the principle is sound, but I may have made some coding mistakes. Did you try symlinks? It would solve your problem — that's basically what it was designed for (with the annoyance that you have to run it chrooted (fakechroot should do the trick)). – Gilles 'SO- stop being evil' Nov 13 '13 at 10:00
  • 1
    I strongly perfer a solution working out of the box, without the need to install something additional. – Alex Nov 13 '13 at 10:02
  • Also, symlinks does not compile out of the box. As I need to be able to run symlinks on different computers with a different setup, this will become a real mess. – Alex Nov 13 '13 at 12:03
  • chroot: failed to run command ‘symlinks’: No such file or directory. Running symlinks -cr without chroot worked though. – Marc.2377 Jul 22 '17 at 03:58
  • @Marc.2377 The symlinks executable has to be available inside the chroot, not just outside. Move or copy it to the chroot or install it in the chroot. – Gilles 'SO- stop being evil' Jul 23 '17 at 15:47
  • symlinks is C code full of strcpy, strcat and fixed-size PATH_MAX arrays on the stack. Ick! – Kaz Aug 30 '19 at 23:14
  • @Kaz That's no worse than the Linux kernel. – Gilles 'SO- stop being evil' Aug 31 '19 at 21:24
  • 1
    @Marc.2377 Get the source package (it has only one C file), add -static to CFLAGS , make clean and make, and you have a statically linked binary (check with ldd symlinks) that can be copied to and used standalone in the chroot. – Roadowl Jul 14 '20 at 18:09
10

Pure bash & coreutils, changes symlinks to relative without unnecessary ../s in path:

find . -type l | while read l; do
    target="$(realpath "$l")"
    ln -fs "$(realpath --relative-to="$(dirname "$(realpath -s "$l")")" "$target")" "$l"
done

You can change:

  • find . to find /path/to/directory to convert symlinks in that directory
  • ln -fs to echo ln -fs for a dry run

Explanation:

  • target="$(realpath "$l")" - finds absolute path to symlink target
  • ln -fs - creates symlink (-s), forcing (-f) rewrite of existing
  • realpath -s "$l" - finds absolute path to symlink itself
  • dirname "$(realpath -s "$l")" - finds absolute path to directory containing the symlink
  • realpath --relative-to="$(dirname "$(realpath -s "$l")")" "$target" - finds path of target relative to symlink, in other words: converts absolute to relative path
LUMIFAZA
  • 101
  • 1
    I'd suggest to add an if clause to skip broken links. – fuenfundachtzig Jul 15 '19 at 16:41
  • 1
    Doesn't work. On every link, I get realpath: : no such file or directory – whoKnows Dec 07 '19 at 19:58
  • Please give this script a name, and then add an example usage of it, with example output. – Gabriel Staples May 14 '20 at 06:07
  • Please also replace all unnecessary usages of the lower-case L (l) with a name which looks less like the number "one" (1). Seeing dollar L ($l) is extremely confusing since it looks like dollar one ($1), which is used to read a positional input parameter and really throws one off from reading this script properly. – Gabriel Staples May 14 '20 at 06:22
  • Nowadays you can also use ln -sr to directly create symbolic relative link. Not sure when it arrived, but is on GNU coreutils 8.32. – Zouppen Jul 09 '23 at 12:44
  • Pretty good but fails if the filename of some existing symbolic link contains a line feed. If you're running GNU tools and bash, you could probably do find -print0 and while read -d $'\0' and add a lot more escaping to above commands to handle all possible characters in link names. – Mikko Rantalainen Jan 16 '24 at 09:29
4

Transforming absolute in relative links is supported by sshfs which mounts remote directories via ssh.

The command is: There

sudo sshfs <remote_user>@<remote_ip_address>:/ /home/<host_user>/mntpoint/ -o transform_symlinks -o allow_other

The command, especially the <placeholders>, shall be adapted to the specific environment.

fra-san
  • 10,205
  • 2
  • 22
  • 43
2

The accepted answer is great, but the chroot is only required due to the specific requirement of this question that want to re-interpret differently the symlinks. For those of you that simply want to update absolute symlinks into a relative symlinks (for instance to allow copy/paste of folders without breaking the symlinks), just install the tiny program symlinks, and run:

symlinks -cr .

where . is the path of the directory, r is for recursive change, and c turns asbolute symlinks into normal symlinks. Without any option, it will list the absolute symlinks.

tobiasBora
  • 4,041
  • 4
  • 23
  • 35
1
find -lname "$PWD/*" | while read link ;do  
   target=$(readlink $link);
   relative_target=$(realpath $target --relative-to=$link/.. --no-symlinks --canonicalize-missing);
   ln --force --symbolic --no-target-directory $relative_target $link
done

"$PWD/*" stands to convert only absolute links which point inside current workdir

kyb
  • 420
  • Pretty good but fails if the filename of some existing symbolic link contains a line feed or a space. If you're running GNU tools and bash, you could probably do find -print0 and while read -d $'\0' and add a lot more escaping to above commands to handle all characters in link names. – Mikko Rantalainen Jan 16 '24 at 09:27
0

Here is a pure sh solution.

cd /home/user/system &&
find . -lname '/*' |
while read l ; do
  echo ln -sf $(echo $(echo $l | sed 's|/[^/]*|/..|g')$(readlink $l) | sed 's/.....//') $l
done |
sh
0

None of the answers seemed to do well without chroot for a fs that was not already root. I made this shell script that worked well:

#!/bin/sh

ROOTPATH="$1" shift if [ -n "$1" ]; then for LINK in "$@"; do ORIG="$(readlink "$LINK")" case "$ORIG" in /*) echo fixing $LINK '->' $ORIG NEWDEST="$(realpath --relative-to "$(dirname "${LINK}")" "${ROOTPATH}/${ORIG}")" ln -sf "$NEWDEST" "$LINK" ;; esac done else echo Finding and relativizing absolute symlinks under ${ROOTPATH} find "$ROOTPATH" -type l -exec "$0" "$ROOTPATH" '{}' + fi

Assuming you saved it as fixlinks.sh:

Usage: ./fixlinks.sh /root/of/path/to/fix