在CentOS服务器上编写Shell脚本时,条件判断是控制流程的关键,而文件测试运算符又是其中最容易用错的部分。很多脚本部署后表现出奇怪的行为,比如明明文件存在却提示找不到、以为可以写入结果权限不足、或者备份逻辑判断反了导致数据覆盖。这些问题的根源往往不是业务逻辑本身,而是对test命令和方括号中文件测试运算符的求值规则理解不够。本文以CentOS实际环境为例,从存在性、类型、权限、时间戳四个维度梳理常用运算符,并给出可直接运行的脚本片段。

文件测试运算符并不是独立的命令,而是test命令的参数,也可以写在[ ]或[[ ]]中。比如[ -f /etc/passwd ]实际执行的是test -f /etc/passwd,根据文件是否为普通文件返回0或非0状态码。下面的内容会通过实际例子展示这些运算符的用法和坑点。
一、文件存在性与类型判断
判断一个路径是否存在是脚本中最基础的操作,但不同类型需要不同的运算符。-e只检查路径是否存在,不管它是文件、目录还是符号链接;-f要求目标是普通文件,-d要求目标是目录。还有-L或-h判断符号链接本身,-b和-c分别判断块设备和字符设备。在CentOS的/dev目录下,这些设备文件很常见,例如/dev/sda是块设备,/dev/tty是字符设备。
很多人会混淆-e和-f。如果路径指向一个目录,-e返回真,而-f返回假;如果路径是悬挂的符号链接,-e在多数test实现里会返回假,但-L会返回真。因此写脚本时必须明确自己判断的是路径本身还是路径指向的目标。下面是一个典型示例:
#!/bin/bash
path="/etc/passwd"
if [ -e "$path" ]; then
echo "路径存在"
else
echo "路径不存在"
fi
if [ -f "$path" ]; then
echo "这是一个普通文件"
elif [ -d "$path" ]; then
echo "这是一个目录"
elif [ -L "$path" ]; then
echo "这是一个符号链接"
fi
上面的脚本先检查路径是否存在,再进一步判断具体类型。注意路径变量$path必须加双引号,否则当路径包含空格或通配符时会被分词展开,导致判断错误。CentOS下很多配置目录如/etc/sysconfig/network-scripts虽然没有空格,但用户主目录路径可能包含空格,养成加引号的习惯可以避免大量奇怪问题。
符号链接的判断还有一点需要留意:如果使用[[ ]]而不是[ ],对符号链接的处理可能一致,但不同Shell版本间存在差异。在CentOS的标准Bash中,[ -L link ]和[ -h link ]效果相同,都是判断链接文件本身。如果需要检查链接指向的目标是否存在,通常要组合-e来判断,或者直接读取链接内容。
二、权限与属性判断:从可读可写到特殊权限
权限判断在实际运维中非常常见,比如写日志前检查文件是否可写,读取配置前检查是否可读。-r、-w、-x分别判断当前执行脚本的用户对文件是否拥有读、写、执行权限。-s判断文件大小是否大于0,常用于避免处理空文件。-O判断当前用户是否为文件属主,-G判断当前用户的有效组是否与文件属组相同。
特殊权限位也有对应运算符。-g检查文件是否设置了SGID位,-u检查SUID位,-k检查粘滞位。粘滞位常见于/tmp目录,设置后只有文件属主能删除自己的文件。下面的脚本展示了如何检查一个日志文件是否非空且可写,满足条件才追加内容:
#!/bin/bash
logfile="/var/log/myapp.log"
if [ -f "$logfile" ] && [ -s "$logfile" ] && [ -w "$logfile" ]; then
echo "开始写入日志..."
echo "$(date): 任务执行成功" >> "$logfile"
else
echo "日志文件不存在、为空或不可写,跳过写入"
fi
这里的逻辑与符号表示三个条件同时成立才执行写入。>>是追加重定向,它会把日志内容追加到文件末尾。在CentOS中,逻辑与也可以用单方括号加-a表示,例如[ -f "$logfile" -a -s "$logfile" -a -w "$logfile" ],不过这种写法在复杂条件下可读性较差,且容易因参数解析出错,建议优先使用[[ ]]搭配逻辑与和逻辑或。
权限判断有个重要陷阱:root用户可以读任何文件,所以-r对root几乎永远返回真,但-w对只读文件系统上的文件可能仍返回假。另外,-x判断目录时表示能否进入目录,而不是目录本身是否可执行。对于普通用户,权限判断基于实际权限位和当前用户身份,不会考虑ACL等扩展权限,因为test命令只查看传统权限模型。如果脚本使用ACL控制权限,这些运算符的结果可能会与预期不完全一致。
三、时间戳比较与逻辑组合
在备份、同步、增量构建等场景中,经常需要比较两个文件的新旧。-nt表示“比另一个文件新”,-ot表示“比另一个文件旧”。这两个运算符会在双方修改时间不同时返回对应结果,如果修改时间相同则返回假。-ef用于判断两个路径是否指向同一个文件(通过inode和设备号比较),这在处理硬链接或符号链接时很有用。
下面是一个备份脚本片段,它比较源配置文件和备份文件,只有当源文件比备份文件新时才执行复制,避免无意义的覆盖:
#!/bin/bash
src="/etc/nginx/nginx.conf"
backup="/backup/nginx.conf"
if [ -f "$src" ] && { [ ! -f "$backup" ] || [ "$src" -nt "$backup" ]; }; then
cp "$src" "$backup"
echo "备份已更新"
else
echo "无需更新备份"
fi
这段代码先确保源文件存在,然后判断备份文件不存在或者源文件比备份文件新,满足条件才执行cp。使用大括号{ }将或关系包起来,是为了让整个组合作为一个条件参与逻辑与,避免优先级歧义。如果没有大括号,逻辑与和逻辑或的优先级可能不符合直觉,导致执行顺序错误。
时间戳比较的坑主要体现在纳秒级或文件系统同步延迟。在CentOS默认的ext4或xfs文件系统上,stat显示的时间可以精确到纳秒,但test命令的-nt和-ot通常只比较秒级,如果两个文件在同一秒内被修改,即使纳秒不同,运算符也可能返回假。另外,-nt和-ot对符号链接的处理是直接比较链接文件本身的时间,而不是目标文件的时间。如果目标文件更新了但符号链接的mtime没变,比较结果会不准确。针对这种场景,建议先使用readlink -f获取真实路径再比较。
逻辑组合方面,除了逻辑与和逻辑或,还可以使用!取反。注意方括号与运算符之间必须有空格,例如[ ! -f file ],写成[! -f file]就会报语法错误。在CentOS的Bash中,推荐使用[[ ]]代替单方括号,因为[[ ]]是Shell关键字,支持更自然的逻辑组合、模式匹配,并且不需要对变量加引号以防分词,但为了兼容较老的脚本解释器,很多运维人员仍习惯使用[ ]。
四、完整实战:CentOS下自动清理过期日志
前面分别介绍了运算符的用法,现在把它们组合成一个实际运维脚本。需求是:检查某个日志目录,如果目录存在且可写,则找出所有扩展名为.log且超过30天未修改的普通文件,将它们移动到备份目录;如果备份目录不存在就创建;最后输出处理结果。这个脚本涉及-d、-w、-f、-s以及时间戳比较的混合使用。
#!/bin/bash
log_dir="/var/log/myapp"
backup_dir="/backup/oldlogs"
if [ ! -d "$log_dir" ]; then
echo "日志目录不存在: $log_dir"
exit 1
fi
if [ ! -d "$backup_dir" ]; then
mkdir -p "$backup_dir"
fi
if [ ! -w "$backup_dir" ]; then
echo "备份目录不可写: $backup_dir"
exit 1
fi
count=0
for file in "$log_dir"/*.log; do
if [ -f "$file" ] && [ "$file" -ot /tmp/expire_marker ]; then
mv "$file" "$backup_dir/"
count=$((count+1))
fi
done
echo "共移动 $count 个过期日志文件"
这个脚本里使用了一个技巧:创建一个参考标记文件/tmp/expire_marker,通过touch -d '30 days ago' /tmp/expire_marker预先设置它的修改时间为30天前,然后对每个日志文件用-ot与标记文件比较,判断是否更旧。这种方法比使用find命令更直观,适合在纯Shell脚本中实现简单的过期判断。
实际部署时会发现,直接使用find配合-mtime +30通常更高效,但文件测试运算符方式胜在逻辑清晰、便于添加额外判断。例如可以要求文件非空才移动:在循环中加入[ -s "$file" ]。还可以通过[ -O "$file" ]确保只处理当前用户拥有的文件,避免误移其他用户的文件。将运算符灵活组合,能构建出非常灵活的运维逻辑。
最后要提醒的是,编写这类脚本时务必在CentOS真实环境或虚拟机中测试。不同的Shell实现(如dash、sh)对[[ ]]和-nt的支持程度不同。CentOS默认的/bin/sh通常链接到Bash,但部分极简容器环境可能使用dash,此时[[ ]]会报错。为了保证可移植性,建议使用[ ]单方括号写法,并坚持给变量加双引号。

