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


/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?

2 Answers 2


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

  1. Create a text file with the following content

    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"
  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
  • Thank you for this, however when I try to run the script as indicated, I simply get Run: Command not found
    – JVC
    Dec 8, 2013 at 17:28
  • Well, you only need to type the rest of the line (starting with the period)
    – nohillside
    Dec 8, 2013 at 17:43
  • 1
    I learned some new bash syntax from this one. So an upvote for that! Thanks!
    – Ian C.
    Dec 8, 2013 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.
    – JVC
    Dec 8, 2013 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
    – JVC
    Dec 8, 2013 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)
commands here to sort your links out, noting that the broken links are stored in the variable $BROKEN_LINKS

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

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)
> 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
  • 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, 2013 at 17:52
  • 1
    It's not a git to parse correctly in SE markdown either ;)
    – stuffe
    Dec 8, 2013 at 18:05

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .