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
  • I didn't know the kernel included steam bash and python. Where did you get it from? – mikewhatever Oct 14 '23 at 09:56
  • If you referring to Bash and Python, they come included with any standard Ubuntu GNU/Linux installation, 22.04 LTS in this case. So the answer would be "From the official Ubuntu iso." I installed Steam via the Ubuntu Software Centre, but that doesn't matter so much, since it was just an example of a program creating tons of symlinks with a hard-coded username in my home directory. – winkmal Oct 14 '23 at 10:04
  • Not sure how you 'renamed the links", but to change the target of symbolic links requires deleting them and recreating them. So indeed you will need to script a bit to find symbolic links then delete and rename them. Could be done with a little bash script that then can be run on the links through a find command. – vanadium Oct 14 '23 at 11:38
  • Well, you've said "installing Linux", not Ubuntu. Any particular reason to be so vague? – mikewhatever Oct 14 '23 at 12:36
  • I always mount my data partition in /mnt/data. Some suggest just directly mounting into / (root) but not into /home. Then I can from /home run a simple command to link all folders into /home. You would have to change to that user to use same command. https://askubuntu.com/questions/1013677/storing-data-on-second-hdd-mounting – oldfred Oct 14 '23 at 14:15

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
  • 100,812
  • Funny because when I asked a popular bot to write me such a script, it came up with something very similar. I.e., the matching is almost identical find "$folder_path" -type l | while read symlink; do, but your replacement/update command works slightly better than ln -sf "$new_target" "$symlink". Kudos and thanks a lot for the detailed explanation. The only two symlinks that were caught by neither script were k: -> /media/olduser/thumbdrive1and l: -> /media/olduser/thumbdrive2. – winkmal Oct 16 '23 at 20:40
  • @winkmal the bot may be popular, but it is very often wrong. If something is a common error, then it will get it wrong too. And trying to parse the output of find as you show is a classic error. It looks like it works, and it does for simple file names, but it will fail for names with newlines or backslashes and, depending on how you quote, even on globbing characters or spaces. Be very, very careful with any answers from that bot! – terdon Oct 16 '23 at 21:35