3

I rsync the directory "Promotion" containing absolute symbolic links between two machines with different directory structures. Therefore absolute symbolic links don't work on both machines. To make them work, I would like to convert them to relative links. The directory structure is

Machine 1: /home/user/Privat/Uni Kram/Promotion/
Machine 2: /homes/user/Promotion/

Here are two example symlinks:

 4821      1 lrwxrwxrwx   1 manu  users         105 Nov 17  2014 ./Planung\ nach\ Priorit\303\244ten.ods -> /home/manu/Dokumente/Privat/Uni\ Kram/Promotion/Pl\303\244ne\ und\ Ideen/Pl\303\244ne/Planung\ nach\ Priorit\303\244ten.ods  
37675      1 lrwxrwxrwx   1 manu  users         102 Aug  3  2015 ./Kurs/Lab\ Course\ Somewhere -> /home/manu/Dokumente/Privat/Uni\ Kram/Promotion/Workshops\ &\ Fortbildungen/Kurs\ Lab\ Course\ Somewhere

My non-working try is (based on example this):

find * -type l -print | while read l; do 
ln -srf $(cut -c 24- < $(readlink $l)) $l;
done
oystermanu
  • 45
  • 1
  • 6
  • 2
    Given the number of `rsync` options relevant to links I'd be surprised if none handled that. I have no experience with them and can't test right now, but you should definitely look into them if you haven't already. – Aaron Oct 31 '18 at 09:56
  • Welp, according to [this 2014 answer on SuperUser](https://superuser.com/a/799362/545384), no there's no such option. It advises to change absolute links into relative ones. – Aaron Oct 31 '18 at 10:03
  • note that relative links are relative from where the link is – Nahuel Fouilleul Oct 31 '18 at 10:10
  • See [Convert absolute path into relative path given a current directory using Bash](https://stackoverflow.com/q/2564634/4154375). – pjh Oct 31 '18 at 19:24

6 Answers6

6

On machine that contains right sym-linked files, run this:

find . -type l -exec ln -sfr {} . \;

to only change the symlinks in the current directory and not traverse all subdirectories use the -maxdepth 1 flag as the above command will move symlinks in subdirectories into the current directory.

Also the native BSD ln command provided by macos does not have a -r flag, but the ln command provided by GNU core utils can be used instead, and if installed via homebrew prepend g to ln ie. gln -sfr

ipatch
  • 3,933
  • 8
  • 60
  • 99
Galaxy
  • 1,862
  • 1
  • 17
  • 25
  • How does this work? I gather that `ln -sr /d1/d2/d3/a.txt .` creates a relative symbolic link named `a.txt` in the current directory pointing to `/d1/d2/d3/a.txt`, but where is this special meaning of `.` documented? – dazedviper Jan 28 '23 at 13:42
1

Here is a solution for doing this using python3.

from pathlib import Path

d = Path("/home/user/Privat/Uni Kram/Promotion/")
all_symlinks = [p for p in d.rglob("*") if p.is_symlink()]

def make_symlink_relative(p):
    assert p.is_symlink()
    relative_target = p.resolve(strict=True).relative_to(p.absolute().parent)
    p.unlink() 
    # this while-loop protects against race conditions
    while True:
        try:
            p.symlink_to(relative_target)
            break
        except FileExistsError:
            pass

for symlink in all_symlinks:
    make_symlink_relative(symlink)

Michael Hall
  • 2,834
  • 1
  • 22
  • 40
0

Following may work.

Note that

  • links are be backuped, if not wanted change mv .. by rm
  • find argument must be absolute otherwise function will reject links

Example

find /absolute/path/to/root -type l -exec bash -c $'
    abs_to_relative() {
        l=$1
        t=$(readlink "$1")
        [[ $l = /* ]] && [[ $t = /* ]] && [[ $l != "$t" ]] || return
        set -f
        IFS=/
        link=($l)
        target=($t)
        IFS=$\' \\t\\n\'
        set +f
        i=0 f=\'\' res=\'\'
        while [[ ${link[i]} = "${target[i]}" ]]; do ((i+=1)); done
        link=("${link[@]:i}")
        target=("${target[@]:i}")
        for ((i=${#link[@]};i>1;i-=1)); do res+=../; done
        res=${res:-./}
        for f in "${target[@]}"; do res+=$f/; done
        res=${res%/}
        # rm "$1"
        mv "$1" "$1.bak"
        ln -s "$res" "$1"
    }
    for link; do abs_to_relative "$link"; done
' links {} +

Test that was done

mkdir -p /tmp/testlink/home{,s}/user/abc
touch /tmp/testlink/home/user/{file0,abc/file1}.txt
ln -s /tmp/testlink/home/user/abc/file1.txt /tmp/testlink/home/user/link1.txt
ln -s /tmp/testlink/home/user/file0.txt /tmp/testlink/home/user/abc/link0.txt
ln -s /tmp/testlink/home/user/    /tmp/testlink/home/user/abc/linkdir
# ... command
rm -rf /tmp/testlink
Nahuel Fouilleul
  • 18,726
  • 2
  • 31
  • 36
  • Thank you for your script. I didn't realize that my problem requires so much code. I still have some issues with your solution. it creates links pointing to "../../../home/manu/Studies/somefile". Where does the "../../../" comes from? Did I miss something? – oystermanu Oct 31 '18 at 12:12
  • I think the number of "../" corresponds with the number of spaces in the referenced path. So a path with 3 spaces gets 3 "../" in front of the referenced path in the symlink – oystermanu Oct 31 '18 at 12:30
  • it's difficult to say without having exact tree, can you give output of `find /path -type l -ls`? – Nahuel Fouilleul Oct 31 '18 at 13:08
  • I've got 130 symlinks in the folder structure. Here are two examples of the original symlinks: 124124 1 lrwxrwxrwx 1 manu users 121 Okt 31 13:38 Promotion/CLIB/Patent-\ &\ Innovationsmanagement\ Kurs -> /home/manu/Dokumente/Privat/Uni\ Kram/Promotion/Workshops\ &\ Fortbildungen/Kurs\ Patent-\ &\ Innovationsmanagement\ Kurs 123572 1 lrwxrwxrwx 1 manu users 113 Okt 31 13:07 Promotion/Planung\ nach\ Priorit\303\244ten.ods -> /home/manu/Dokumente/Privat/Uni\ Kram/Promotion/Pl\303\244ne\ und\ Ideen/Pl\303\244ne/Planung\ nach\ Priorit\303\244ten.ods – oystermanu Oct 31 '18 at 13:42
  • would be better to update the question with this, from this comment it seems you didn't use an absolute path (beginning by /) as argument of find as required, seems that Promotion is not a subdir of `/home` so there is not ohter solution than using `../..` until `/` – Nahuel Fouilleul Oct 31 '18 at 14:42
  • You're right. I added two example symlinks and the actual directory structure to the question. Regarding the "/absolute/path/to/root": I replaced it with "/homes/user/Promotion" as this is the structure on the machine I'm currently working on – oystermanu Oct 31 '18 at 15:38
0

Thank you all for your help. After some trying I came up with a solution based on your comments and code. Here is what solved my problem:

#!/bin/bash
# changes all symbolic links to relative ones recursive from the current directory
find * -type l -print | while read l; do 
cp -a "$l" "$l".bak
linkname="$l"
linktarget=$(readlink "$l")
echo "orig linktarget"
echo $linktarget
temp_var="${linktarget#/home/user/Privat/Uni Kram/Promotion/}"
echo "changed linktarget"
echo $temp_var;
ln -sfr "$temp_var" "$l"
echo "new linktarget in symlink"
readlink "$l";
done
oystermanu
  • 45
  • 1
  • 6
0

Here is a pair of functions to convert in both directions. I now have these in my ~/bin to use more generally. The links do not have to be located in the present directory. Just use as:

lnsrelative <link> <link> <link> ...
lnsabsolute <link> <link> <link> ...
#!/bin/bash
# changes absolute symbolic links to relative
# will work for links & filenames with spaces

for l in "$@"; do
    [[ ! -L "$l" ]] && echo "Not a link: $l" && exit 1
done
    
for l in "$@"; do
    # Use -b here to get a backup.  Unnecessary because reversible.
    ln -sfr "$(readlink "$l")" "$l"
done

and

#!/bin/bash
# changes relative symbolic links to absolute
# will work for links & filenames with spaces

for l in "$@"; do
    [[ ! -L "$l" ]] && echo "Not a link: $l" && exit 1
done
    
for l in "$@"; do
    # Use -b here to get a backup.  Unnecessary because reversible.
    ln -sf "$(realpath "$(readlink "$l")")" "$l"
done

For completeness, this will change symbolic links to hard links, used as:

lnstohard <link> <link> <link>
#!/bin/bash
# changes symbolic links to hard links
# This will work for filenames with spaces, 
# but only for regular files in the same filesystem as the link.
# Thorough discussion of correct way to confirm devices are the same:
# https://unix.stackexchange.com/questions/120810/check-if-2-directories-are-hosted-on-the-same-partition-on-linux

for l in "$@"; do
    [[ ! -L "$l" ]] && echo "Not a symbolic link: $l" && exit 1
    
    rl="$(readlink "$l")"
    rld="$(dirname "$l")"
    [[ ! -e "$rl" || -d "$rl" || "$(df --output=target "$rld")" != "$(df --output=target "$rl")" ]] && \
        echo "Target \"$rl\" must exist on same filesystem as link \"$l\", and may not be a directory" && \
        exit 1
done

for l in "$@"; do
    # Using -b here to get a backup, because it's not easy to revers a soft->hard link conversion
    ln -fb "$(readlink "$l")" "$l"
done
Diagon
  • 473
  • 5
  • 16
0

While @Diagon's answer is more complete, here's a quick and dirty one-liner that works most of the time but will probably die on paths with spaces:

find . -type l -exec bash -c  'ln -sfr {} $(dirname {})/' \;
arne
  • 4,514
  • 1
  • 28
  • 47