4

find -type l and ls -l | grep '^l' show symbolic links but do not distinguish between symlinks pointing to directories and those pointing to files. A very kludgy way to do something like this is find -type l -exec file -L '{}' ';' | grep directory but that is very inefficient.

Is there a better way to find only symlinks that point to directories?

6 Answers6

14

According to the POSIX specification of test, the -d test is:

True if pathname resolves to an existing directory entry for a directory.

So once you have a symlink, use [ -d ... ] on it:

find . -type l -exec test -d {} \; -print

Or, to avoid executing an external command for each link:

find . -type l -exec sh -c 'for l; do [ -d "$l" ] && printf "%s\n" "$l"; done' _ {} +
muru
  • 72,889
  • Note that whether [ or printf is builtin in sh or not is unspecified by POSIX, [ generally is, printf not always. Running find -exec test -d {} \; also may not fork and exec a separate utility like with busybox find. – Stéphane Chazelas Oct 19 '23 at 19:23
12

You can use -type l with -xtype d. (I don't know if -xtype is POSIX though.)

Tom Yan
  • 731
3

For zsh users, this can easily be achieved using the glob qualifiers (@-/):

for F in **/*(N@-/); do the_thing "$F"; done

Breaking down that expression: The **/* glob matches all files/directories recursively, excluding hidden ones. (expr) after the glob limits the match to files satisfying certain criteria. The criteria here are:

  • N: enable nullglob far that one expansion so that if there's no matching file, it expands to nothing rather than causing an error. You generally want to use that qualifier when the glob is used in a for loop or in an array assignment.
  • @: match only symbolic links
  • -: switch from testing the links themselves to testing the link targets
  • /: match only directories

Example output, run in /etc/ on my system:

% ls -ldF -- **/*(@-/)
lrwxrwxrwx 1 root root 46 Sep 22  2022 alternatives/desktop-plasma5-wallpaper -> /usr/share/desktop-base/active-theme/wallpaper/
lrwxrwxrwx 1 root root 39 Sep 22  2022 alternatives/desktop-theme -> /usr/share/desktop-base/homeworld-theme/
lrwxrwxrwx 1 root root 36 Sep 22  2022 alternatives/vendor-logos -> /usr/share/desktop-base/debian-logos/
lrwxrwxrwx 1 root root 11 Sep 22  2022 runit/runsvdir/default/ssh -> /etc/sv/ssh/
lrwxrwxrwx 1 root root 18 Aug  7  2022 xdg/systemd/user -> ../../systemd/user/

Note that this also matches links that point to links pointing to directories, not just direct links.

marcelm
  • 2,485
2

If you just want something that works on (GNU) Linux:

find -type l -xtype d -print

This has the advantage that you can specify some action other than -print.

If you need POSIX then perhaps something like this:

(
export LC_COLLATE=C
find    . -type l -print | sort > ListOfLinks &
find -L . -type d -print | sort > ListOfDirs &
wait
comm -12 ListOfLinks ListOfDirs
)

(Refer to the POSIX specs for find, sort, and comm.)

Note that this will traverse directories pointed to by symlinks that it finds, so in some cases it will perform worse than your naïve version.

This outputs one path per line, with the usual caveat: it can't work if you have newlines in your filenames, so if you need to handle filenames with newlines, you'll need to pick some different tools -- see GNUish method above, or try something like ...

If you have Perl you could write:

perl -Mv5.10 -MFile::Find -e '
    find( { wanted => sub { -l && -d && say },
            no_chdir => 1 }, @ARGV )
' .

(Don't forget the . on the end; that's the current directory. Corresponding answers using Python and other languages are left as exercises for the reader; the key point is there's a call-back function that's invoked for each name.)

  • In POSIX find, the list of files/dirs for find to start searching on is not optional. To search the current working directory, use find . – Stéphane Chazelas Oct 21 '23 at 13:35
  • Note that find -L will follow those links to directories and start looking for more files recursively in there. – Stéphane Chazelas Oct 21 '23 at 13:36
  • Note that LC_ALL and LC_COLLATE take precedence over LANG. That comm-based approach only works if you can guarantee file paths won't contain newline characters. – Stéphane Chazelas Oct 21 '23 at 13:38
  • With perl, you'd like want say $File::Find::name or you'll only get the base names of the matching files. – Stéphane Chazelas Oct 21 '23 at 13:44
  • -Mv5.10 is surely less confusing than -M5.01 – ikegami Oct 21 '23 at 16:22
  • @StéphaneChazelas thanks for those reminders; updated accordingly. I'd already noted "the usual caveats about strange filenames", and the wider directory traversal.) – Martin Kealey Oct 23 '23 at 05:31
  • @ikegami I just realised that the v5.x.x notation was added at the same time as use version, back in v5.6.0, but I missed the ever-so-slightly later update in 5.6.1 where the documentation gave an example of them being used together. So gosh, I've been stuck for 25 years writing use v5.006 when I could have been using use v5.6 all along. I'm both embarrassed, and pleased to learn something new. :-( – Martin Kealey Oct 23 '23 at 05:58
  • I think that having sort fragment any filenames with embedded newlines only results in mangled output; the matching names are correctly identified, so the overall output is the same as you'd get by doing it "correctly" and then sorting the results. – Martin Kealey Oct 23 '23 at 06:11
1

This is my version which should be a bit more efficient than find . -type l -exec test -d {} \; -print for many links:

$ find / -type l -print0 |
xargs -0 stat -L -c'%F %n' |
awk '$1 == "directory" { print substr($0, 11) }'

It finds all links, feeds them into stat to determine the file type, then awk filters directories, leaving only the filename.

Without awkyou get something like:

directory /sys/module/libnvdimm/holders/nfit

Verification:

$ ll /sys/module/libnvdimm/holders/nfit
lrwxrwxrwx 1 root root 0 Oct 20 13:49 /sys/module/libnvdimm/holders/nfit -> ../../nfit
$ readlink -f /sys/module/libnvdimm/holders/nfit
/sys/module/nfit
$ ll -d /sys/module/nfit
drwxr-xr-x 6 root root 0 Oct 16 20:45 /sys/module/nfit
U. Windl
  • 1,411
  • 2
    Most times when people think they need to write find ... -print0 | xargs -0 cmd --cmdopt, they could instead write find ... -exec cmd -options {} +. The only time not to do that is if you actually have find ... -print0 | some other cmds here | xargs -0 cmd -options. Also, don't forget the -r option for xargs, because sometimes your cmd does unwanted things when invoked with no filenames. (e.g. ls defaults to ., and output a lot, rather than nothing) – Martin Kealey Oct 21 '23 at 04:16
  • Note that find -print0 is a GNUism (so "strictly POSIX" is out) but then you could write find -type l -printf '%Y %p\0' | grep -z ^d | cut -z -c3-. (Or of course, find -type l -xtype d -print0.) – Martin Kealey Oct 21 '23 at 04:31
  • I think @MartinKealey's point was that you could just be running find . -type l -exec stat -L -c'%F %n' {} + | awk '$1 == "directory" { print substr($0, 11) }' instead of piping through xargs (in any case any benefit you would have had from -print0 is immediately lost when piping to ask). – muru Oct 23 '23 at 08:50
  • "to ask" mean "through awk"? I don't quite understand. – U. Windl Oct 24 '23 at 05:52
  • Yes, sorry, that was a typo and should be "when piping to awk" – muru Oct 25 '23 at 10:57
1

Disclaimer: I'm the current author of rawhide (rh) (see https://github.com/raforg/rawhide)

With rawhide (rh) you can do:

rh 'l && td'

Or, more verbosely/readably:

rh 'link && target_dir'

l or link tests that the current candidate file is a symlink.

td or target_dir tests that the symlink target is a directory.

Actually, td/target_dir can't succeed unless the current candidate file is a symlink, so you only need:

rh td
raf
  • 171