Reputation: 55
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
Upvotes: 4
Views: 4312
Reputation: 16055
Here's an answer that can handle filenames with embedded line feeds and other special characters both in symlink names and symlink targets. This requires GNU find
and ln
:
find . -type l -exec printf "ln --no-target-directory -srf -- %q %q\n" {} {} \; | bash
Remove the | bash
from the end to see a preview about what it would do.
Basically it lists every symlink and passes the name twice to ln
with flag --no-target-directory
to allow the original symlink as both source and target as is. The -r
flag of GNU ln
will then automatically rewrite the symlink target as relative symlink regardless how convoluted the original symlink was. The printf %q
is used to encode all the special characters in the symlink names.
And you can give other parameter but .
to find
to search from some other directory downwards.
And if you only need to rewrite a single symlink you can do it like this:
LINK="/path/to/symlink/I want to fix"
ln --no-target-directory -srf -- "$LINK" "$LINK"
This will also correctly handle the case where the symlink name does not match the target name (e.g. a -> ../b
).
Upvotes: 2
Reputation: 4674
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 {})/' \;
Upvotes: 0
Reputation: 2022
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
Upvotes: 7
Reputation: 493
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
Upvotes: 0
Reputation: 3350
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)
Upvotes: 1
Reputation: 55
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
Upvotes: 0
Reputation: 19335
Following may work.
Note that
mv ..
by rm
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
Upvotes: 0