Take the 2-minute tour ×
Ask Different is a question and answer site for power users of Apple hardware and software. It's 100% free, no registration required.

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?

share|improve this question

2 Answers 2

up vote 4 down vote accepted

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
share|improve this answer
    
Thank you for this, however when I try to run the script as indicated, I simply get Run: Command not found –  Jonathan van Clute Dec 8 '13 at 17:28
    
Well, you only need to type the rest of the line (starting with the period) –  patrix Dec 8 '13 at 17:43
1  
I learned some new bash syntax from this one. So an upvote for that! Thanks! –  Ian C. Dec 8 '13 at 17:49
    
Oh! OK now it ran. Didn't appear to do anything though... the symlinks do have a new timestamp, but they're still broken as before. In trying to understand your code, I'm not clear on how it's doing the string replace on the old name to change it to the new. –  Jonathan van Clute Dec 8 '13 at 17:51
1  
Ohhhh I'm supposed to change that bit. Thought somehow it was dynamic LOL I'll adjust and post back if I still have any trouble –  Jonathan van Clute Dec 8 '13 at 18:32

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
share|improve this answer
    
The more modern bash way to grab output from commands is to use the $(command) syntax like for MYVAR in $(ls | grep somefilter) instead of using backticks. It has some advantages over backticks like this. I made edits based on this. –  Ian C. Dec 8 '13 at 17:52
1  
It's not a git to parse correctly in SE markdown either ;) –  stuffe Dec 8 '13 at 18:05

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.