How to recursively edit path of symlinks?

This is an additional question that began with this one - How to edit Symbolic Links in OS X?

Now that I know how to edit the path symlinks point to, I'm trying to figure out how to do this recursively for any broken symlink. I'm a PHP guy, so this CLI stuff is somewhat foreign to me...

There seems to be a few specific elements to this, namely:

  1. Identify a broken symlink and retrieve its location
  2. Get the old, now-incorrect, destination for the symlink
  3. Construct the new correct path for the symlink to point to
  4. Edit the symlink

The first is fairly easy:

find /path/to/search -type l | while read f; do if [ ! -e "$f" ]; then ls -l "$f"; fi; done

This gets me a nice list of all broken symlinks found below the supplied directory. But I believe I would need to store the location of each in a variable (array?) to work with it.

Next, it seems that changing the action supplied in the "then" portion of the logic, would yield the solution.

Item 2 can be retrieved using:

readlink /path/to/broken/symlink

So it seems we need to shove that into a variable. I'm unclear on how to do this in terminal.

Number 3 would be a simple edit to the path retrieved during step 2. I need to replace the old drive name, with the new. So changing:

/Volumes/Old\ Drive/path/to/symlink

to

/Volumes/New\ Drive/path/to/symlink

Also unclear on exactly how to do this within a CLI script. Some sort of string replacement appears to be needed. Something like str_replace in the PHP world.

Finally step 4 can be done via:

ln -f -s /path/to/new/location/of/original /path/to/location/of/broken/symlink/

as detailed in my other question, previously linked above.

How exactly would I string these concepts together, to accomplish my desired outcome of fixing all my symlinks in one shot?


Solution 1:

From the top of my head (and without any testing at all):

  1. Create a text file with the following content

    #!/bin/bash
    
    find "$1" -type l | while read symlink; do
        if [ ! -e "$symlink" ]; then
            old_path=$(readlink "$symlink")
            new_path=${old_path/Old Drive/New Drive}
            ln -f -s "$new_path" "$symlink"
        fi
    done
    
  2. Make it executable by running chmod +x name-of-file

  3. Run ./name-of-file /path/to/search

This is untested so try it with a sample directory first.


To add some explanations

  • old_path=$(readlink "$symlink") runs the command in $(...) and assigns the result to $old_path
  • ${old_path/Old Drive/New Drive} does text substitution on $old_path

Solution 2:

In a bash shell, to set a variable, simply use set NAME=bob or set VITAL_SIGNS=none.

You can also set a variable using the output of a command by calling the bash-builtin function read to assign the output to a named variable. This works well in a pipe stream like so:

ls -l | wc -l | read NUMBER_OF_LINES

Or you can assign the output directly to a variable like so:

LICENSE_KEY=$(cat ~/software/key.txt | grep KEY | awk '{print $1}')

A great way to recursively read variables is in a loop as follows:

for BROKEN_LINK in $(commands to produce a list of files)
do
commands here to sort your links out, noting that the broken links are stored in the variable $BROKEN_LINKS
done

With the above in mind, something like the following should work:

prove a folder doesn't exist

StuffeMac:dan stuffe$ ls ~/Desktop/broken_links
ls: /Users/stuffe/Desktop/broken_links: No such file or directory

prove a new target folder does exist

StuffeMac:dan stuffe$ ls ~/Desktop/working_links
StuffeMac:dan stuffe$

create some invalid and valid links

StuffeMac:dan stuffe$ ln -s ~/Desktop/brokenlinks/dan1
StuffeMac:dan stuffe$ ln -s ~/Desktop/brokenlinks/dan2
StuffeMac:dan stuffe$ ln -s ~/Desktop/working_links/dan3
StuffeMac:dan stuffe$ ln -s ~/Desktop/outofscopedeadlinks/dan4
StuffeMac:dan stuffe$ ls -l
total 32
lrwxr-xr-x  1 stuffe  staff  38  8 Dec 10:06 dan1 -> /Users/stuffe/Desktop/brokenlinks/dan1
lrwxr-xr-x  1 stuffe  staff  38  8 Dec 10:06 dan2 -> /Users/stuffe/Desktop/brokenlinks/dan2
lrwxr-xr-x  1 stuffe  staff  40  8 Dec 10:06 dan3 -> /Users/stuffe/Desktop/working_links/dan3
lrwxr-xr-x  1 stuffe  staff  46  8 Dec 10:21 dan4 -> /Users/stuffe/Desktop/outofscopedeadlinks/dan4

grab a list of dead links into a file for input

StuffeMac:dan stuffe$ find . -type l | while read f; do if [ ! -e "$f" ]; then ls "$f" >> deadlinks.txt; fi; done
StuffeMac:dan stuffe$ more deadlinks.txt
./dan1
./dan2
./dan4

run a loop against each dead link

StuffeMac:dan stuffe$ for DEAD_LINK in $(cat deadlinks.txt)
> do
> DESTINATION_IN_SCOPE=$(readlink $DEAD_LINK | grep brokenlinks | wc -l)
> NEW_DESTINATION="~/Desktop/working_links/"
> if [ $DESTINATION_IN_SCOPE = "1" ]
> then
> NEW_LINK=$(echo $DEAD_LINK | colrm 1 2)
> ln -f -s $NEW_DESTINATION$NEW_LINK $DEAD_LINK
> else
> echo "Link $DEAD_LINK not in target folder"
> fi
> done
Link ./dan4 not in target folder
StuffeMac:dan stuffe$

check out the symlinks after edits

StuffeMac:dan stuffe$ ls -l
total 32
lrwxr-xr-x  1 stuffe  staff  28  8 Dec 10:08 dan1 -> ~/Desktop/working_links/dan1
lrwxr-xr-x  1 stuffe  staff  28  8 Dec 10:08 dan2 -> ~/Desktop/working_links/dan2
lrwxr-xr-x  1 stuffe  staff  40  8 Dec 10:06 dan3 -> /Users/stuffe/Desktop/working_links/dan3
lrwxr-xr-x  1 stuffe  staff  46  8 Dec 10:21 dan4 -> /Users/stuffe/Desktop/outofscopedeadlinks/dan4