文件测试和stat

在Shell中,经常会通过中括号或test命令来测试文件是否存在、是否可读可写可执行等。Perl作为【高级Shell】脚本语言,也可以进行文件测试,并且测试语法和Shell的测试方式非常相似,比如都可以用-e "file"测试file文件是否存在。

文件测试符

Perl中的测试操作都通过一些特殊的测试符进行,测试符均以短横线开头,后面是一个字母。例如-e "file"。在可能产生歧义的情况下,这些测试符可以用括号包围,例如:(-e "a.log")

在介绍支持的操作符之前,先了解一些测试符的注意事项。

  1. 测试操作的返回值:
    • 返回1表示测试结果为真
    • 返回空字符串表示测试结果为假
    • 返回undef值表示文件不存在或无法测试,此时会将错误信息设置到$!
    • 部分测试符返回具体值,例如测试文件大小的-s,它返回文件字节数量,空文件时返回0,也可表示布尔假
  2. 测试符可省略参数,此时默认测试$_对应的文件名
    • -t测试除外,因为它测试的总是句柄,-t省略参数时默认测试STDIN
    • 省略参数时很容易出错,因为它们可能会把后面第一个非空白字符串当作测试参数。例如-s / 1024本意是获得文件大小的KB数量,但却测试了/。如果一定要省略,建议使用小括号包围测试符(-s)/1024
  3. -l测试符测试是否是软链接,它会追踪软链接,如果是软链接但不是有效的软链接,比如目标不存在,将返回undef并报错

Perl支持下面这些操作符,详细说明可参考手册:perldoc -f -X

符号含义
-e测试file文件是否存在
-z测试file文件是否存在且是否为空,测试目录永远假,因为目录永不为空
-s测试file文件是否存在,返回文件字节大小
-f文件是否为普通文件
-d文件是否为目录文件
-l文件是否为软链接(字符链接)
-b文件是否为块设备
-c文件是否是字符设备文件
-p文件是否为命名管道
-F文件是否为socket文件
-t测试文件句柄是否是一个已打开的终端设备
-r对effective uid/gid而言,文件是否可读
-w对effective uid/gid而言,文件是否可写
-x对effective uid/gid而言,文件是否可执行
-oeffective uid是否是文件的所有者
-R对real uid/gid而言,文件是否可读
-W对real uid/gid而言,文件是否可写
-X对real uid/gid而言,文件是否可执行
-Oreal uid是否是文件的所有者
-u文件是否设置了setuid
-g文件是否设置了setgid
-k文件是否设置了sticky
-M最后一次修改(mtime)距离目前的天数
-A最后一次访问(atime)距离目前的天数
-C最后一次inode修改(ctime)距离目前的天数
-T文件看起来像文本文件
-B文件看起来像二进制文件(和-T相反)

上面-M/-A/-C会计算天数,它是(小时数/24)来计算的。例如,6小时前修改的文件,它的天数就是0.25天。例如:

# 修改文件mtime时间为6小时前
$ touch -m -t "6 hours ago" /tmp/b.log

下面的代码将输出0.25:

print (-M "/tmp/b.log");  # 0.250162037037037

优化测试

每一次测试操作,都是在执行一次stat系统调用,它可能会访问磁盘来获取文件的各种元数据信息(共十三种信息),因此速度很慢。

如果要测试大量文件,且需要测试每个文件的多种属性,重复多次测试将会使效率非常低。例如,要从包含100W个小文件的目录中找出具有可读可写且3天内未修改过的文件(运维的常事),就需要多次对同一个文件进行测试。

每一次测试都是请求stat系统调用,一次stat系统调用本身就会返回文件的所有元数据信息,这些信息可以缓存下来以供后续使用,因此,如果需要测试同一个文件的多个属性,建议只测试一次该文件,并在之后访问缓存下来的该文件信息。

perl每次测试文件时,非常人性化地将文件信息缓存下来了,下次测试该文件时,直接对特殊的测试目标下划线_进行测试即可,这会访问缓存中的信息。

# 测试可读且可写
say "xxx" if -w "a.log" and -r "a.log"; # 不推荐
say "xxx" if -w "a.log" and -r _;       # 推荐

_的缓存周期可以延续到下一次对具体文件的测试。例如,下面第三条语句测试的是b.log是否可读。

print "xxx" if -w "a.log";
print "xxx" if -w "b.log";
print "xxx" if -r _;

另一种优化多次测试的方式是连写测试符。

测试符连写

可以将多个测试操作符连在一起写。

  • 连写的时候,从右向左依次执行,并按照and逻辑运算符判断真假,即所有测试都为真才返回真
  • 对于返回真/假值的测试符,连写的测试符前后顺序不会影响结果
  • 对于返回非真/假值的测试符(即-s/-M/-A/-C),强烈建议不要连写

例如下面两个语句,它们在最终测试结果上是等价的。第一个语句先测试可写性,再测试可读性,只有两者均为真时if条件才为真。

print "xxx" if -r -w "a.log";
print "xxx" if -w -r "a.log";

但是,返回非真/假值的测试符,需要小心小心再小心。例如,-s返回的是文件字节数而非布尔值:

while(<*>){
  if (-s -f $_ < 512){     # 这里的结果会出乎意料
    print "${_}'s size < 512 bytes\n";
  }
}

上面的if条件子句等价于(-f $_ and -s _) < 512,它会输出小于512字节的普通文件,以及所有非普通文件。由于and是短路运算的,如果测试的目标$_不是一个普通文件,而是一个目录,-f $_就会返回假,并结束测试,由于 布尔假对应数值0,使得和512的大小比较永远返回真。

所有,对于返回非真/假值的测试符,应该避免测试符连写:

while(<*>){
  if (-f $_ and (-s _) < 512){
    print "${_}'s size < 512 bytes\n";
  }
}

stat函数和lstat函数

Perl除了支持上面各种测试符来测试文件属性,还支持stat函数获取文件的元数据信息,它们都会发起stat系统调用。

stat函数返回共13项属性信息,这13项属性先后顺序分别是(可随时通过perldoc -f stat查看):

use 5.010;
$filename=$ARGV[0];

my @arr = ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
           $atime,$mtime,$ctime,$blksize,$blocks)
        = stat($filename);

say '$dev     :',$arr[0];
say '$inode   :',$arr[1];
say '$mode    :',$arr[2];
say '$nlink   :',$arr[3];
say '$uid     :',$arr[4];
say '$gid     :',$arr[5];
say '$rdev    :',$arr[6];
say '$size    :',$arr[7];
say '$atime   :',$arr[8];
say '$mtime   :',$arr[9];
say '$ctime   :',$arr[10];
say '$blksize :',$arr[11];
say '$blocks  :',$arr[12];

输出结果:

$dev     :2050
$inode   :67326520
$mode    :33188
$nlink   :1
$uid     :0
$gid     :0
$rdev    :0
$size    :12
$atime   :1533544992
$mtime   :1533426824
$ctime   :1533426824
$blksize :4096
$blocks  :8

各项属性的含义:

属性含义
dev文件所属文件系统的设备ID
inode文件inode号
mode文件类型和文件权限(两者都是数值表示)
nlink文件硬链接数
uid文件所有者的uid
gid文件所有者的gid
rdev文件的设备ID(只对特殊文件有效,即设备文件)
size文件大小,单位字节
atime文件atime的时间戳(从1970-01-01开始计算的秒数)
mtime文件mtime的时间戳(从1970-01-01开始计算的秒数)
ctime文件ctime的时间戳(从1970-01-01开始计算的秒数)
blksize文件所属文件系统的block大小
blocks文件占用block数量(块大小一般是512字节,可通过stat -c "%B"命令获取块大小)

需要注意的是,$mode返回的是文件类型和文件权限的结合体,且文件权限并非直接的8进制权限值,要计算出Unix系统中直观的权限数值(如0755、0644),需要和0777做位与运算。

# 将返回0644、0755类型的八进制权限值
printf "perm :%04o\n",$mode & 0777;  

也可以直接从stat结果中取出某项属性的值:

my $mode = (stat($filename))[2];

以下是stat函数的其它一些注意事项:

  • stat函数测试成功时,返回属性列表,测试失败时返回空列表
  • stat测试成功时,会缓存为_
  • 如果省略stat函数的参数,则默认测试$_

对于软链接文件,stat会追踪到链接的目标,如果软链接是无效链接(目标不存在),将报错。如果不想追踪,则使用lstat函数替代stat,lstat在测试非软链接以及目标不存在的软链接时,返回空列表。