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?

2
  • 2
    ls -l | grep '^l' is wrong, it'll fail for files containing ^l or new lines Why not parse ls (and what to do instead)?, mywiki.wooledge.org/ParsingLs
    – phuclv
    Commented Oct 19, 2023 at 11:37
  • There certainly are reasons not to parse ls, but that's not one of them. grep '^l' will never match a circumflex followed by a lower-case ell. You'd have to use grep '\^l' or grep -F '^l'.
    – Jim L.
    Commented Nov 15, 2023 at 16:14

6 Answers 6

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' _ {} +
1
  • 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. Commented Oct 19, 2023 at 19:23
12

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

1
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.

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.)

8
  • 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 . Commented Oct 21, 2023 at 13:35
  • Note that find -L will follow those links to directories and start looking for more files recursively in there. Commented Oct 21, 2023 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. Commented Oct 21, 2023 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. Commented Oct 21, 2023 at 13:44
  • -Mv5.10 is surely less confusing than -M5.01
    – ikegami
    Commented Oct 21, 2023 at 16:22
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
5
  • 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) Commented Oct 21, 2023 at 4: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.) Commented Oct 21, 2023 at 4: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
    Commented Oct 23, 2023 at 8:50
  • "to ask" mean "through awk"? I don't quite understand.
    – U. Windl
    Commented Oct 24, 2023 at 5:52
  • Yes, sorry, that was a typo and should be "when piping to awk"
    – muru
    Commented Oct 25, 2023 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

You must log in to answer this question.

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