1

When installing Linux on a new SSD, I created a user newuser with a different name than on the old system olduser. I successfully rsynced all the files from the old to the new SSD's home partition. But especially in the .steam folder, there are tons of symlinks (softlinks) which still use /home/olduser, rather than ~or $HOME. I started renaming some of them in Bash, but then realized its just too many, like > 6000!

So my question is: Is there a Bash/Python/Whatever script to

  1. List all symlinks in a folder, including subfolders (ls -lR /path/to/folder | grep '^l'), and
  2. Rename all the /home/olduser to /home/newuser symlinks
winkmal
  • 136

1 Answers1

1

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.

terdon
  • 104,119