文件测试和stat
在Shell中,经常会通过中括号或test命令来测试文件是否存在、是否可读可写可执行等。Perl作为【高级Shell】脚本语言,也可以进行文件测试,并且测试语法和Shell的测试方式非常相似,比如都可以用-e "file"
测试file文件是否存在。
文件测试符
Perl中的测试操作都通过一些特殊的测试符进行,测试符均以短横线开头,后面是一个字母。例如-e "file"
。在可能产生歧义的情况下,这些测试符可以用括号包围,例如:(-e "a.log")
。
在介绍支持的操作符之前,先了解一些测试符的注意事项。
- 测试操作的返回值:
- 返回1表示测试结果为真
- 返回空字符串表示测试结果为假
- 返回undef值表示文件不存在或无法测试,此时会将错误信息设置到
$!
- 部分测试符返回具体值,例如测试文件大小的
-s
,它返回文件字节数量,空文件时返回0,也可表示布尔假
- 测试符可省略参数,此时默认测试
$_
对应的文件名-t
测试除外,因为它测试的总是句柄,-t
省略参数时默认测试STDIN- 省略参数时很容易出错,因为它们可能会把后面第一个非空白字符串当作测试参数。例如
-s / 1024
本意是获得文件大小的KB数量,但却测试了/
。如果一定要省略,建议使用小括号包围测试符(-s)/1024
-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而言,文件是否可执行 |
-o | effective uid是否是文件的所有者 |
-R | 对real uid/gid而言,文件是否可读 |
-W | 对real uid/gid而言,文件是否可写 |
-X | 对real uid/gid而言,文件是否可执行 |
-O | real 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在测试非软链接以及目标不存在的软链接时,返回空列表。