Perl's file test operator -f returns true for symbolic links
find, perl, shell, unix
Solution
Quick solutions
$ ls | perl -lne 'print if stat && -f _'
1
2
3
4
5
file1
$ ls | perl -lne 'print if lstat && -f _'
file1
Symbolic links and find
By default, GNU `find` never dereferences or follows symbolic links, but the `find` documentation describes switches that change this policy.
The options controlling the behaviour of find with respect to links are as follows :-
`-P` `find` does not dereference symbolic links at all. This is the default behaviour. This option must be specified before any of the file names on the command line.
`-H` `find` does not dereference symbolic links (except in the case of file names on the command line, which are dereferenced). If a symbolic link cannot be dereferenced, the information for the symbolic link itself is used. This option must be specified before any of the file names on the command line.
`-L` `find` dereferences symbolic links where possible, and where this is not possible it uses the properties of the symbolic link itself. This option must be specified before any of the file names on the command line. Use of this option also implies the same behaviour as the `-noleaf` option. If you later use the `-H` or `-P` options, this does not turn off `-noleaf`.
`-follow` This option forms part of the “expression” and must be specified after the file names, but it is otherwise equivalent to `-L`. The `-follow` option affects only those tests which appear after it on the command line. This option is deprecated. Where possible, you should use `-L` instead.
Converting find commands to Perl
The standard distribution comes with a `find2perl` utility that is compatible with `find` from older Unix systems.
$ find2perl . -type f | perl
./file1
We could instead ask for files that are either plain files themselves or links to plain files.
$ find2perl . -follow -type f | perl
./1
./2
./3
./4
./5
./file1
In the code `find2perl` generates, the default `wanted` sub passed to `find` from the File::Find module is
sub wanted {
my ($dev,$ino,$mode,$nlink,$uid,$gid);
(($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) &&
-f _
&& print("$name\n");
}
but with `-follow`, we get
sub wanted {
my ($dev,$ino,$mode,$nlink,$uid,$gid);
(($dev,$ino,$mode,$nlink,$uid,$gid) = stat($_)) &&
-f _
&& print("$name\n");
}
Notice that the only difference is whether `wanted` calls `stat` or `lstat`, where the latter is documented as
`lstat EXPR` `lstat`
Does the same thing as the `stat` function (including setting the special `_` filehandle) but stats a symbolic link instead of the file the symbolic link points to. If symbolic links are unimplemented on your system, a normal `stat` is done. For much more detailed information, please see the documentation for `stat`.
If EXPR is omitted, stats `$_`.
As the sample outputs from `find2perl` shows, you can express your intent with the filetest operator but be precise about the semantics of symlinks with your choice of `stat` versus `lstat`.
That funny `_` token
The `_` at the ends of the quick solutions above is the special filehandle that the `lstat` documentation mentions. It holds a copy of the most recent result from `stat` or `lstat` as a way to avoid having to repeatedly make those expensive system calls. Filetest operators such as `-f`, `-r`, `-e`, and `-l` also fill this buffer:
If any of the file tests (or either the `stat` or `lstat` operator) is given the special filehandle consisting of a solitary underline, then the stat structure of the previous file test (or `stat` operator) is used, saving a system call. (This doesn't work with `-t`, and you need to remember that `lstat` and `-l` leave values in the stat structure for the symbolic link, not the real file.) (Also, if the stat buffer was filled by an `lstat` call, `-T` and `-B` will reset it with the results of `stat _`). Example:
print "Can do.\n" if -r $a || -w _ || -x _;
stat($filename);
print "Readable\n" if -r _;
print "Writable\n" if -w _;
print "Executable\n" if -x _;
Problem
I used to think that `-f` tested a file to see if it was a regular file, and not anything else. But Perl seems to be behaving differently. I looked up the perldoc entry , and it says: ``` -f File is a plain file. ``` Suppose I have a directory having one file called `file1`, and 5 symbolic links `1 2 3 4 5`, each of which point to `file1`, like so: ``` -rw-r--r-- file1 lrwxrwxrwx 1 -> file1 lrwxrwxrwx 2 -> file1 lrwxrwxrwx 3 -> file1 lrwxrwxrwx 4 -> file1 lrwxrwxrwx 5 -> file1 drwxr-xr-x ../ drwxr-xr-x ./ ``` If I run find on this directory using `-type f` filter, it gives output as expected: ``` % find . -type f ./file1 ``` But when I run a perl script using -f operator, it gives following output: ``` % ls | perl -e 'while(<>) { chomp; print "$_\n" if -f $_ }' 1 2 3 4 5 file1 ``` When I add the test of `-l` too, it works as expected: ``` % ls | perl -e 'while(<>) { chomp; print "$_\n" if -f $_ and not -l $_}' file1 ``` Are symbolic links considered plain files too? If so, Why? Is my usage of the file test incorrect?