39

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
2

6 Answers 6

25

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
' _ {} +
11
  • 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, 2013 at 7: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). Nov 13, 2013 at 8:21
  • Now the script runs, but does not as expected. Maybe I was not precise enough, I have updated the question.
    – Alex
    Nov 13, 2013 at 9:56
  • 1
    I strongly perfer a solution working out of the box, without the need to install something additional.
    – Alex
    Nov 13, 2013 at 10:02
  • 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, 2020 at 18:09
7

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
4
  • 1
    I'd suggest to add an if clause to skip broken links. Jul 15, 2019 at 16:41
  • 1
    Doesn't work. On every link, I get realpath: <path to symlink>: no such file or directory
    – whoKnows
    Dec 7, 2019 at 19:58
  • Please give this script a name, and then add an example usage of it, with example output. May 14, 2020 at 6: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. May 14, 2020 at 6:22
3

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.

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
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

0

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.

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.