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

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")
    root=${link//+([!\/])/..}; root=${root#/}; root=${root%..}
    rm "$link"
    ln -s "$root${target#/}" "$link"
' _ {} +
  • 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 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). – Gilles 'SO- stop being evil' Nov 13 '13 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 '13 at 9: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

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"

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


  • 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
  • 1
    I'd suggest to add an if clause to skip broken links. – fuenfundachtzig Jul 15 '19 at 16:41
  • Doesn't work. On every link, I get realpath: <path to symlink>: no such file or directory – whoKnows Dec 7 '19 at 19:58

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.


This bash snippet should do it. The first form will print what it would do; the second will do it. I would carefully review the output as it is destructive -- the ln -f overwrites the existing link.

cd $TOP
find * -type l -print | while read l; do echo ln -sf $TOP$(readlink $l) $l; done
find * -type l -print | while read l; do      ln -sf $TOP$(readlink $l) $l; done
  • Besides the fact that this will fail for names with spaces, isn't this the wrong way round? It prefixes the absolute path? – fuenfundachtzig Jul 15 '19 at 16:34

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

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

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.