This can be tricky becayse in theory, file names can contain anything at all except /
and the NUL byte (\0
). This makes parsing tyhe output of ls
for things like ->
peroblematic since you can have a file named file -> foo
:
$ touch 'file -> foo'
$ ls
'file -> foo'
Plus, since names can contain newlines, the approach you suggested, ls -l | grep '^l'
doesn't help you easily collect the names:
$ ln -s 'file -> foo' 'some name'$'\n''with a newline'
$ ls -l
total 0
-rw-r--r-- 1 terdon terdon 0 Oct 15 12:36 'file -> foo'
lrwxrwxrwx 1 terdon terdon 11 Oct 15 12:37 'some name'$'\n''with a newline' -> 'file -> foo'
$ ls -l | grep '^l'
lrwxrwxrwx 1 terdon terdon 11 Oct 15 12:37 some name
As you can see above, that only catches the name of the file before the first newline. So we need better approaches. Luckily, find
has an option to look for symlinks only:
$ find . -type l
./some name?with a newline
Armed with that, we can get a list of all symlinks, read their target and change that to the updated user name:
find . -type l -print0 |
while IFS= read -r -d '' linkName; do
target=$(readlink -- "$linkName")
newTarget=$(sed 's|^/home/olduser/|/home/newuser/|' <<<"$target")
if [[ "$newTarget" != "$target" ]]; then
rm -- "$linkName" &&
ln -s -- "$newTarget" "$linkName"
fi
done
Let's break this down:
find . -type l -print0
: this finds all links in the current directory, and prints them as a NUL \0
separated list. This lets us handle weird file names such as those containing newline characters safely.
while IFS= read -r -d '' linkName; do ...; done
: we pass the output of the previous find
command to this while loop, which reads null-delimited iunput (-d ''
) and sets the IFS
variable to the empty string to avoid splitting on whitespace. The -r
ensures that any escape characters (e.g. \r
or \t
) found in the file names are not treated as escape character but as simple strings so that we can handle files with \
in their names. The loop itself iterates over ever link name returned by find
, saving it as $linkName
.
target=$(readlink "$linkName")
: this is the target the link points to. Note that I am not using -f
here because we don't want the final target, we just want the first level. It doesn't really matter here since your links will all be broken given that the target directory doesn't exist, but it is safer.
newTarget=$(sed 's|^/home/olduser/|/home/newuser/|' <<<"$target")
: we use sed
to replace /home/olduser/
found at the beginning of the target name with /home/newuser/
and save the resulting path as $newTarget
.
if [[ "$newTarget" != "$target" ]]; then
: if the new target name is not the same as the old one.
rm -- "$linkName" && ln -s -- "$newTarget" "$linkName"
: delete the existing link and, if the deletion was successful (&&
), make a new symlink with the same name but pointing to the new target.
This should work safely on arbitrary file names.
find
command. – vanadium Oct 14 '23 at 11:38