12

I would like to learn, how to find broken symbolic links, recursively given a path.

Just as a precise reference of what I am looking for, the Linux equivalent (that does not work in macOS) would be find -xtype:

find /path/to/directory -xtype l
8
  • 1
    Try this command: find . -type l | xargs stat -FL {}. That will find all of the symbolic links in the current path and if the link is broken, it will error out, if not, you’ll get info on the link itself
    – Allan
    Apr 20 at 18:41
  • wc * | grep such will show all the broken links in $pwd but unfortunately it is not recursive.
    – WGroleau
    Apr 21 at 15:07
  • @WGroleau: Did you mean wc * 2>&1 | grep such? I think you could make it recursive by changing to find . -exec wc {} \; 2>&1 | grep such. Apr 21 at 20:34
  • 1
    See the same question on Unix&Linux, where many answers cover non-GNU systems including macOS. Apr 21 at 23:45
  • 1
    @DavidAnderson, good one—I've used that a lot, but none of the times had any subdirectories.
    – WGroleau
    Apr 22 at 3:50

4 Answers 4

13
find /path/to/directory -type l ! -exec [ -e {} ] \; -print

-exec executes a command for each result.
[ -e … ] tests whether the destination exists.
! negates the result of the exec (where the destination does not exist).
-print prints the results.

0
4

With the tools that come with macOS, the easiest way is to use zsh: its glob qualifiers allow filtering by file type. The glob qualifier @ is the analog of -type l for the find command, and the glob qualifier - is the analog of using -xtype instead of -type.

print -lr -- /path/to/directory/**/*(-@)

Also of interest if you're going to work on symbolic links in zsh:

  • Run zmodload -F zsh/stat b:zstat to load the zstat builtin. This lets you print the content of a symbolic link (the value returned by readlink) with zstat +link -- $some_path. This is the same as readlink -- $somepath, but faster and lets you store the result in a variable with zstat -A target +link -- $somepath without having to worry about edge cases with targets containing newlines.
  • …(:A) applies the history modifier A to a path, which resolves all symlinks in the path.
0
3

Not tested on macOS, but this should work, returning all links that cannot be followed:

find -L /path/to/directory -type l -print

If not, then this finds all links, and for each one runs a shell command to check whether the target exists, and if not prints the path:

find /path/to/directory -type l -exec sh -c 'test -e {} || echo {}' \;
1
2

I want to give an answer, based on grg's answer above, on how to take all the broken symlinks found and transform them into shell-reusable form, portably* in bash and zsh (*first see David Anderson's comments below and my edited entry on "${pathnames[@]:"$i":1}").

I also want to mention, the two following equivalances, they will function the same:

  • find ... ! -exec test -e {} \; means if the pathname (resolving symlinks) exists, don't find it. "!" is used here by "find". Can also substitute "-not" in place of "!".
  • find ... -exec test ! -e {} \; means if the pathname (resolving symlinks) doesn't exist, find it. "!" is used here by "test". I will use this in the following example.

Example to (1) store, (2) print out, and (3) count the broken symlinks

pathnames=()
while IFS= read -r -d $'\0' pathname; do
  pathnames+=( "$pathname" )
done < <(find . -type l -exec test ! -e {} \; -print0)

for (( i=0; i<${#pathnames[@]}; i++ )); do
  printf "%q\n" "${pathnames[@]:"$i":1}"
done

printf "\nBroken symlinks found: %s\n" "${#pathnames[@]}"
  • pathnames is an array to store the pathnames found, in this case broken symlinks.
  • find . -type l -exec test ! -e {} \; means search in and under current directory for broken symlinks. -type l finds symlinks and test ! -e tests if the symlink-resolved pathname does not exist.
  • -print0 means delimit the pathnames found from find using NUL bytes, and print.
  • < <(...) means process substitution in bash/zsh and redirecting into stdin of read.
  • while ... read means run read until it encounters end-of-file or experiences an error.
  • IFS= means don't truncate whitespace at end/start of pathname.
  • -r means don't allow backslashes to escape characters; do not interpret backslashes.
  • -d $'\0', or -d '', means read in input delimited by NUL bytes.
  • printf "%q\n" leverages bash's/zsh's builtin printf to print out pathnames in shell-reusable form.
  • "${pathnames[@]:"$i":1}" uses 0-indexed array indexing in bash/zsh.
    • As noted below in David Anderson's comments, changing "$i" to $i (without the quotation marks) will work more portably and not cause this piece of code to fail for older releases of bash/zsh, given that $i does not expand into anything more than an integer index. I want to point out it is a bug in older releases to not be able to include quotes for preventing unwanted expansions within array-like parameter expansions.
    • I tested my code in bash v5.2.26(1) and zsh v5.9, but forgot to test in bash v3.2.57(1), which is the very old bash version* Apple still retains in Macs* (I usually test this bash version against code I have) (*also see great info on Apple's bash version and on related Macs in comments section below by David Anderson). "${pathnames[@]:"$i":1}" will fail in this old bash version, and it will also fail in zsh 5.7.1 as noted below by David Anderson.
    • In summary, for portable indexing, use "${pathnames[@]:$i:1}" instead.

More info on the while ... read loop: https://mywiki.wooledge.org/BashFAQ/001

Sample setup

Create some broken symlinks and broken directory symlinks with some newlines and carriage returns:

ln -s nofile 'broken symlink'
ln -s nofile $'broken symlink\nwith\nnewlines'
ln -s nofile $'broken symlink with\r carriage return'
mkdir tempdir
ln -s tempdir 'broken dir symlink'
ln -s tempdir $'broken \r dir \n_symlink'
sleep 1; rm -d tempdir
  • sleep 1 is used for Finder to catch up displaying symlink-folder icons for the two broken symlink directories.

Output in zsh, and then in bash

./broken\ symlink
./broken\ symlink\ with$'\r'\ carriage\ return
./broken\ $'\r'\ dir\ $'\n'_symlink
./broken\ dir\ symlink
./broken\ symlink$'\n'with$'\n'newlines

Broken symlinks found: 5
./broken\ symlink
$'./broken symlink with\r carriage return'
$'./broken \r dir \n_symlink'
./broken\ dir\ symlink
$'./broken symlink\nwith\nnewlines'

Broken symlinks found: 5
11
  • I tested you answer using zsh 5.9 on Ventura without issues. When testing using zsh 5.7.1 on Catalina, I needed to replace printf "%q\n" "${pathnames[@]:"$i":1}" with printf "%q\n" "${pathnames[@]:$i:1}". Apr 23 at 9:44
  • When testing using bash 3.2.57(1)-release on both Ventura and Catalina, I needed to make the same replacement. Apr 23 at 9:53
  • I believe OrangeDog's answer can be modified to accomplish the same output, except for the count of broken links. For example: find /path/to/directory -type l -exec sh -c "test -e '{}' || printf '%q\n' '{}'" \; Apr 23 at 13:59
  • (1) Your replacement is much better to enable for older versions of bash/zsh, since $i only expands to an integer. It is a bug though in older releases to not be able to include quotes for preventing unwanted expansions within array-like parameter expansions. (2) POSIX doesn't specify what happens if the two characters {} are part of a larger string and not standing alone (it's implementation-defined), an alternative can be find /path/to/directory -type l -exec sh -c 'test -e "$1" || printf "%q\n" "$1"' sh1 {} \;
    – Aeronautix
    Apr 24 at 21:44
  • Aeronautix: I agree with your previous comment. However, questions and answers posted here at Ask Different are supposed to be for Apple hardware and software. The OP's question explicitly states "macOS". Your answer will work in a bash shell under the latest macOS only if one first installs over 10 GB of additional software some of which is third party. You might want to mention that somewhere in your answer. Apr 24 at 22:51

You must log in to answer this question.

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