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

9 Answers 9

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
    Commented 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). Commented 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
    Commented 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
    Commented 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
    Commented Jul 14, 2020 at 18:09
11

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. Commented 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
    Commented Dec 7, 2019 at 19:58
  • Please give this script a name, and then add an example usage of it, with example output. Commented May 14, 2020 at 6:07
  • 1
    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
    Commented Jul 9, 2023 at 12:44
  • 1
    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. Commented Jan 16 at 9: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.

3

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

0

I'm adding an answer to go a little farther than the other answers already present here. The symlinks -cr . part of my answer, therefore, is already present in two other answers here (see the "References" section below).

Convert absolute symlink to relative symlink with simple Linux command

Quick summary

cd path/to/some/directory

# Convert all absolute symlinks to relative symlinks
symlinks -cr .

All about symlinks: creating, listing, converting from absolute to relative, etc.

Here's a summary:

# Create an absolute symlink
ln -si /path/to/target /path/to/symlink

# Create a relative symlink
ln -sir /path/to/target /path/to/symlink


# Install `symlinks` on Linux Ubuntu
sudo apt update
sudo apt install symlinks

# Find all symlinks and see which are relative and which are absolute
symlinks -rsv .

# **Manually** replace any absolute symlink with a relative symlink. 
# This is also the best 'ln' command in my opinion when creating a brand new 
# symlink:
ln -svnri /path/to/target /path/to/symlink

# **Automatically** replace all functional absolute symlinks with relative 
# symlinks
symlinks -cr .

To fix broken symlinks, use my tool here: fix_broken_symlinks.sh, in my eRCaGuy_dotfiles repo.

References

  1. https://unix.stackexchange.com/a/100955/114401
  2. https://unix.stackexchange.com/a/698192/114401
  3. My fix_broken_symlinks.sh script, and my comments and help menu therein, in my eRCaGuy_dotfiles repo.
0

One-liner for those with ln -r and Bash:

find . -type l -exec printf "ln --no-target-directory -srf -- %q %q\n" {} {} \; | bash
1
  • 1
    Why not just find . -type l -exec ln --no-target-directory -srf {} {} \;? Commented May 18 at 11:45

You must log in to answer this question.

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