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
2

7 Answers 7

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
' _ {} +
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
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
6
  • 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
  • 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 9, 2023 at 12:44
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.

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.

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

1
  • 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. Jan 16 at 9: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

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .