test 命令详解与参数实测(Ubuntu 22.04)

以 Ubuntu 22.04(bash 5.1.16 内建 test 与 coreutils 8.32 的 /usr/bin/test 双形态)实测讲解 test:文件/字符串/数值/组合/时间五族操作符全量退出码、[ ] 等价与引号保护、[[ ]] 模式与正则差异、退出码 0/1/2 三值体系与语法错误实测对比。

Linux命令Linux系统UbuntuUbuntu 22.04命令行Shell2026/10/16

test 命令详解与参数实测(Ubuntu 22.04)

目标:全面掌握 test/[ 的每族操作符与退出码语义——文件测试(-f/-d/-e/-s/-r/-w/-x/-L)、字符串测试(-z/-n/=/!=/<)、数值测试(-eq/-ne/-lt/-le/-gt/-ge)、组合(-a/-o/!)、时间与硬链接(-nt/-ot/-ef)、bash 扩展(-v/-o/-t)、[ ] 等价性与引号保护、[[ ]] 的模式匹配与正则差异,以及内建 test 与 /usr/bin/test 双形态行为对比,全部命令与回显已在 Ubuntu 22.04(bash 5.1.16(1)-release + GNU coreutils 8.32-4.1ubuntu1.4)实测通过。

一、环境信息

本文所有命令均在以下环境实测,回显逐字照抄:

  • 操作系统:Ubuntu 22.04.5 LTS(PRETTY_NAME="Ubuntu 22.04.5 LTS")
  • 内核架构:x86_64
  • test 双形态:bash 内建(GNU bash, version 5.1.16(1)-release)+ /usr/bin/test(coreutils 8.32-4.1ubuntu1.4,ELF 二进制)
  • [ 双形态:bash 内建 + /usr/bin/[(同一 coreutils 二进制,51648 字节)
  • 默认 PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
  • 执行身份:root
  • 演示数据目录:/root/demo/test/(wtz-file.txt、wtz-empty.txt、wtz-dir/、wtz-link、wtz-broken-link、wtz-old.txt/wtz-new.txt、wtz-hard.txt)

二、命令概览

2.1 语法与测试数据

test 不产生任何输出,只通过退出码说话:表达式为真退出 0、为假退出 1、用法/语法错误退出 2。它是 if/while/&&/|| 的判定核心。两种等价写法(来自实测 help test 与 help [ 首行):

test: test [expr]
[: [ arg... ]

本文测试数据约定:wtz-file.txt(8 字节普通文件、已加执行位)、wtz-empty.txt(0 字节)、wtz-dir/(目录)、wtz-link(指向 wtz-file.txt 的软链接)、wtz-broken-link(指向不存在目标的软链接)、wtz-old.txt/wtz-new.txt(间隔 1 秒创建)、wtz-hard.txt(wtz-file.txt 的硬链接)。每个参数小节均可独立执行复现。

2.2 help 全量输出(逐行照抄)

test 是内建命令,帮助用 help test 获取,操作符清单是权威依据,以下为完整实测输出:

$ help test
test: test [expr]
    Evaluate conditional expression.
    
    Exits with a status of 0 (true) or 1 (false) depending on
    the evaluation of EXPR.  Expressions may be unary or binary.  Unary
    expressions are often used to examine the status of a file.  There
    are string operators and numeric comparison operators as well.
    
    The behavior of test depends on the number of arguments.  Read the
    bash manual page for the complete specification.
    
    File operators:
    
      -a FILE        True if file exists.
      -b FILE        True if file is block special.
      -c FILE        True if file is character special.
      -d FILE        True if file is a directory.
      -e FILE        True if file exists.
      -f FILE        True if file exists and is a regular file.
      -g FILE        True if file is set-group-id.
      -h FILE        True if file is a symbolic link.
      -L FILE        True if file is a symbolic link.
      -k FILE        True if file has its `sticky' bit set.
      -p FILE        True if file is a named pipe.
      -r FILE        True if file is readable by you.
      -s FILE        True if file exists and is not empty.
      -S FILE        True if file is a socket.
      -t FD          True if FD is opened on a terminal.
      -u FILE        True if the file is set-user-id.
      -w FILE        True if the file is writable by you.
      -x FILE        True if the file is executable by you.
      -O FILE        True if the file is effectively owned by you.
      -G FILE        True if the file is effectively owned by your group.
      -N FILE        True if the file has been modified since it was last read.
    
      FILE1 -nt FILE2  True if file1 is newer than file2 (according to
                       modification date).
    
      FILE1 -ot FILE2  True if file1 is older than file2.
    
      FILE1 -ef FILE2  True if file1 is a hard link to file2.
    
    All file operators except -h and -L are acting on the target of a symbolic
    link, not on the symlink itself, if FILE is a symbolic link.
    
    String operators:
    
      -z STRING      True if string is empty.
    
      -n STRING
         STRING      True if string is not empty.
    
      STRING1 = STRING2
                     True if the strings are equal.
      STRING1 != STRING2
                     True if the strings are not equal.
      STRING1 < STRING2
                     True if STRING1 sorts before STRING2 lexicographically.
      STRING1 > STRING2
                     True if STRING1 sorts after STRING2 lexicographically.
    
    Other operators:
    
      -o OPTION      True if the shell option OPTION is enabled.
      -v VAR         True if the shell variable VAR is set.
      -R VAR         True if the shell variable VAR is set and is a name
                     reference.
      ! EXPR         True if expr is false.
      EXPR1 -a EXPR2 True if both expr1 AND expr2 are true.
      EXPR1 -o EXPR2 True if either expr1 OR expr2 is true.
    
      arg1 OP arg2   Arithmetic tests.  OP is one of -eq, -ne,
                     -lt, -le, -gt, or -ge.
    
    Arithmetic binary operators return true if ARG1 is equal, not-equal,
    less-than, less-than-or-equal, greater-than, or greater-than-or-equal
    than ARG2.
    
    See the bash manual page bash(1) for the handling of parameters (i.e.
    missing parameters).
    
    Exit Status:
    Returns success if EXPR evaluates to true; fails if EXPR evaluates to
    false or an invalid argument is given.

2.3 help [ 全量输出与版本归属

[ 是 test 的同义词,唯一差别是最后一个参数必须是字面 ],帮助极短,完整实测输出:

$ help [
[: [ arg... ]
    Evaluate conditional expression.
    
    This is a synonym for the "test" builtin, but the last argument must
    be a literal `]', to match the opening `['.

版本与归属:内建 test 无独立版本(随 bash 5.1.16 发布);外部实现 /usr/bin/test 与 /usr/bin/[ 归属 coreutils。特别说明:test 家族(内建与外部)都不支持 --version 惯例——实测 /usr/bin/test --version 无任何输出、退出码 0,--version 被当作单参数形态的非空字符串判真(与内建 test --version 行为一致);确认版本用 dpkg -S:

$ dpkg -S /usr/bin/test
coreutils: /usr/bin/test

$ ls -l /usr/bin/[
-rwxr-xr-x 1 root root 51648 Aug 25 15:12 /usr/bin/[

$ file /usr/bin/[
/usr/bin/[: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=658bd7663d2cd6de5e471f78f6f806fc9fa709b9, for GNU/Linux 3.2.0, stripped

$ type -a test [
test is a shell builtin
test is /usr/bin/test
test is /bin/test
[ is a shell builtin
[ is /usr/bin/[
[ is /bin/[

双形态的完整行为对比见 3.15。

三、参数逐项实测

3.1 文件存在与类型:-f / -d / -e

三个最常用操作符:-f 判普通文件、-d 判目录、-e 判存在(不分类型)。注意 help 中的关键规则:除 -h/-L 外,文件操作符都作用于符号链接的目标——断链的目标不存在,所以 -e 为假:

cd /root/demo/test
test -f wtz-file.txt; echo "-f 普通文件 退出码=$?"
test -f wtz-dir; echo "-f 对目录 退出码=$?"
test -d wtz-dir; echo "-d 目录 退出码=$?"
test -d wtz-file.txt; echo "-d 对普通文件 退出码=$?"
test -e wtz-file.txt; echo "-e 存在文件 退出码=$?"
test -e wtz-not-exist; echo "-e 不存在 退出码=$?"
test -e wtz-broken-link; echo "-e 断链(目标不存在) 退出码=$?"

实测回显:

-f 普通文件 退出码=0
-f 对目录 退出码=1
-d 目录 退出码=0
-d 对普通文件 退出码=1
-e 存在文件 退出码=0
-e 不存在 退出码=1
-e 断链(目标不存在) 退出码=1

💡 要点:-e 对断链返回假(目标视角)而 -L 对断链返回真(链接本身视角,见 3.4)——这一对是「判断软链接是否有效」的标准组合:-L 且 -e 有效链接、-L 且 ! -e 断链、! -L 且 -e 普通文件。-f 与 -e 的区别在类型:目录 -e 为真但 -f 为假。

3.2 文件大小:-s

-s 判「存在且非空」,是「文件有内容才处理」的标准判据:

test -s wtz-file.txt; echo "-s 非空文件 退出码=$?"
test -s wtz-empty.txt; echo "-s 空文件 退出码=$?"

实测回显:

-s 非空文件 退出码=0
-s 空文件 退出码=1

💡 要点:0 字节文件 -e 为真而 -s 为假。日志切割、下载完成校验(空文件视为失败)等场景用 -s 而不是 -e。

3.3 权限测试:-r / -w / -x

三者按当前执行用户的视角判定(root 下几乎全真)。目录的 -x 语义是「可进入/可搜索」:

test -r wtz-file.txt; echo "-r 可读 退出码=$?"
test -w wtz-file.txt; echo "-w 可写 退出码=$?"
test -x wtz-file.txt; echo "-x 可执行(已加x) 退出码=$?"
test -x /etc/hostname; echo "-x 不可执行文件 退出码=$?"
test -r wtz-dir; echo "-r 目录 退出码=$?"
test -w wtz-dir; echo "-w 目录 退出码=$?"
test -x wtz-dir; echo "-x 目录 退出码=$?"

实测回显:

-r 可读 退出码=0
-w 可写 退出码=0
-x 可执行(已加x) 退出码=0
-x 不可执行文件 退出码=1
-r 目录 退出码=0
-w 目录 退出码=0
-x 目录 退出码=0

💡 要点:非 root 用户下这套测试才有区分度(root 的 -r/-w 几乎恒真);脚本里「能不能执行这个程序」用 -x 判,目录的 -x 是搜索权(没有 x 的目录即使 -r 也无法 cd 进入或列出其中文件名)。

3.4 链接与设备:-L / -c

-L(与 -h 等价)作用于链接本身,断链也是真;-c 判字符设备:

test -L wtz-link; echo "-L 符号链接 退出码=$?"
test -L wtz-file.txt; echo "-L 普通文件 退出码=$?"
test -L wtz-broken-link; echo "-L 断链本身 退出码=$?"
test -c /dev/null; echo "-c 字符设备 退出码=$?"

实测回显:

-L 符号链接 退出码=0
-L 普通文件 退出码=1
-L 断链本身 退出码=0
-c 字符设备 退出码=0

💡 要点:-L 与其他文件操作符的视角差异(本身 vs 目标)是 help 中唯一显式声明的例外。/dev/null、/dev/tty 这类设备用 -c(块设备如 /dev/sda 用 -b)。

3.5 字符串判空:-z / -n

-z(zero length)判空串、-n 判非空,二者互为反义:

test -z ""; echo "-z 空串 退出码=$?"
test -z "abc"; echo "-z 非空串 退出码=$?"
test -n "abc"; echo "-n 非空串 退出码=$?"
test -n ""; echo "-n 空串 退出码=$?"

实测回显:

-z 空串 退出码=0
-z 非空串 退出码=1
-n 非空串 退出码=0
-n 空串 退出码=1

💡 要点:-z "$var" 判未定义/空变量比 [ $var ] 安全(后者在 var 为空时退化为无参数形态);变量必须加双引号,否则含空格值会被拆词导致 too many arguments(见 3.12 实测)。

3.6 字符串比较:= / != / <(字典序陷阱)

字符串比较按字典序而非数值,</> 在 test 中须转义(防止被解释为重定向):

test "abc" = "abc"; echo "= 相等 退出码=$?"
test "abc" = "abd"; echo "= 不等 退出码=$?"
test "abc" != "abd"; echo "!= 退出码=$?"
test 10 \< 2; echo "字符串字典序 10<2 退出码=$?"

实测回显:

= 相等 退出码=0
= 不等 退出码=1
!= 退出码=0
字符串字典序 10<2 退出码=0

💡 要点:10 < 2 字典序为真(字符 "1" 排在 "2" 前)退出 0——字符串比较陷阱:想比数值必须用 -lt(见 3.7),否则 "9" < "10" 这类结果全部颠倒。=/!= 在 bash 的 test 中 == 也可用([[ ]] 中 == 还支持模式匹配,见 3.14)。

3.7 数值比较:-eq / -ne / -lt / -le / -gt / -ge

六个操作符做整数比较;操作数非整数直接报错退出 2:

test 10 -eq 10; echo "-eq 相等 退出码=$?"
test 10 -eq 11; echo "-eq 不等 退出码=$?"
test 10 -ne 11; echo "-ne 不等 退出码=$?"
test 10 -lt 2; echo "-lt 10<2 退出码=$?"
test 2 -lt 10; echo "-lt 2<10 退出码=$?"
test 10 -le 10; echo "-le 相等边界 退出码=$?"
test 10 -gt 2; echo "-gt 10>2 退出码=$?"
test 10 -ge 10; echo "-ge 相等边界 退出码=$?"
test 1.5 -eq 1.5
echo "退出码=$?"

实测回显:

-eq 相等 退出码=0
-eq 不等 退出码=1
-ne 不等 退出码=0
-lt 10<2 退出码=1
-lt 2<10 退出码=0
-le 相等边界 退出码=0
-gt 10>2 退出码=0
-ge 相等边界 退出码=0
/tmp/test_1.sh: line 57: test: 1.5: integer expression expected
退出码=2

💡 要点:-lt 按数值比较,10 -lt 2 为假——与 3.6 的字典序结果(10 \< 2 为真)形成直接对照,两套体系不能混用。小数、空串等非整数操作数报 integer expression expected 退出 2;需要小数比较用 awk 或 bash 的 (( ))。

3.8 组合:-a / -o / !

-a(AND)、-o(OR)、!(NOT)在 test 内部组合多个表达式,六种真假组合实测:

test -f wtz-file.txt -a -d wtz-dir; echo "-a 与 真真 退出码=$?"
test -f wtz-file.txt -a -d wtz-file.txt; echo "-a 与 真假 退出码=$?"
test -d wtz-file.txt -o -f wtz-file.txt; echo "-o 或 假真 退出码=$?"
test -d wtz-file.txt -o -d wtz-empty.txt; echo "-o 或 假假 退出码=$?"
test ! -f wtz-not-exist; echo "! 对假取反 退出码=$?"
test ! -f wtz-file.txt; echo "! 对真取反 退出码=$?"

实测回显:

-a 与 真真 退出码=0
-a 与 真假 退出码=1
-o 或 假真 退出码=0
-o 或 假假 退出码=1
! 对假取反 退出码=0
! 对真取反 退出码=1

💡 要点:-a/-o 是 POSIX 标准但已被 POSIX 标记为 obsolescent(淘汰中),推荐用 shell 层的 &&/|| 组合多个 test(test A && test B),短路语义也更可控。! 是 test 自己的操作符,写法为 ! 表达式(! 与表达式之间有空格)。

3.9 单参数形态与无参数:test 的隐含规则

参数个数决定 test 的解析方式(help 所言 depends on the number of arguments)。单参数时「非空串即为真」,无参数时为假:

test "abc"; echo "单参数非空 退出码=$?"
test ""; echo "单参数空串 退出码=$?"
test; echo "无参数 退出码=$?"

实测回显:

单参数非空 退出码=0
单参数空串 退出码=1
无参数 退出码=1

这条规则解释了两个反直觉现象:缺右操作数的二元表达式不报错而是判字符串真,实测 test -f(单参数 "-f" 非空)退出 0;[ -f ]、[ = ] 同理为真(见 3.12)。漏写操作数时 test 静默给出「看似合理」的结果,比报错更危险。

💡 要点:写脚本时永远给 test 两个明确操作数(-n "$var" 而不是 $var),从根上避开参数个数歧义。

3.10 文件时间与硬链接:-nt / -ot / -ef

三个二元文件操作符按修改时间(-nt 新于 / -ot 旧于)与 inode(-ef 硬链接同体)判定:

touch wtz-old.txt; sleep 1; touch wtz-new.txt
test wtz-new.txt -nt wtz-old.txt; echo "-nt 新于 退出码=$?"
test wtz-old.txt -nt wtz-new.txt; echo "-nt 反向 退出码=$?"
test wtz-old.txt -ot wtz-new.txt; echo "-ot 旧于 退出码=$?"
ln -f wtz-file.txt wtz-hard.txt
test wtz-file.txt -ef wtz-hard.txt; echo "-ef 硬链接 退出码=$?"
test wtz-file.txt -ef wtz-old.txt; echo "-ef 无关文件 退出码=$?"

实测回显:

-nt 新于 退出码=0
-nt 反向 退出码=1
-ot 旧于 退出码=0
-ef 硬链接 退出码=0
-ef 无关文件 退出码=1

💡 要点:-ef 判「同一文件」的本质是 inode 相同——硬链接必真,同一文件的两个路径(含 usrmerge 的 /bin/x 与 /usr/bin/x)也真;检测文件是否被更新(配置热加载)常用 -nt 轮询。

3.11 bash 扩展:-v / -o / -t

三个操作符不在 POSIX test 清单里,是 bash 的扩展:-v 判变量已定义、-o 判 shell 选项开关、-t 判文件描述符连着终端:

test -v PATH; echo "-v PATH 已定义 退出码=$?"
test -v wtz_undefined_var; echo "-v 未定义变量 退出码=$?"
set -e
test -o errexit; echo "-o errexit 开启 退出码=$?"
set +e
test -o errexit; echo "-o errexit 关闭 退出码=$?"
test -t 0; echo "-t 0(脚本 stdin 非终端)退出码=$?"

实测回显:

-v PATH 已定义 退出码=0
-v 未定义变量 退出码=1
-o errexit 开启 退出码=0
-o errexit 关闭 退出码=1
-t 0(脚本 stdin 非终端)退出码=1

💡 要点:-v 与 -z 的区别——未定义变量 -v 为假、-z "$var" 为真(两种「空」都抓到);已定义为空串时 -v 真而 -z 真,按需选择。-t 0 在非交互脚本里为假、在交互终端里为真,是脚本判断「有没有终端交互能力」(决定是否提示输入)的标准写法。可移植脚本避免使用这三个操作符(dash 不支持)。

3.12 [ ] 等价形态与引号保护

[ 与 test 完全等价,仅多一个「末参数必须是 ]」的要求。含空格变量实测引号的决定性影响:

v="hello world"
[ "$v" = "hello world" ]; echo "带引号 退出码=$?"
[ $v = "hello world" ]; echo "不带引号 退出码=$?"

实测回显:

带引号 退出码=0
/tmp/test_2.sh: line 62: [: too many arguments
不带引号 退出码=2

[ 特有错误——缺右括号退出 2;单参数形态照 3.9 的规则判定:

[ -f wtz-file.txt
echo "缺 ] 退出码=$?"
[ -f ]; echo "[ -f ] 退出码=$?"
[ -f wtz-not-exist ]; echo "[ -f 不存在 ] 退出码=$?"
[ = ]; echo "[ = ] 退出码=$?"

实测回显:

/tmp/test_1.sh: line 74: test: argument expected
缺 ] 退出码=2
[ -f ] 退出码=0
[ -f 不存在 ] 退出码=1
[ = ] 退出码=0

💡 要点:不加引号时 $v 被拆词成 hello 和 world 两个独立参数,[ 收到 5 个参数报 too many arguments 退出 2——这就是「test 的变量必须双引号」的全部原因。[ -f ]/[ = ] 为 0 是单参数字符串语义(3.9),不是在测文件。

3.13 退出码三值体系与报错形态汇总

test 的退出码只有三种取值,全量实测:

test -f wtz-file.txt; echo "真 退出码=$?"
test -f wtz-not-exist; echo "假 退出码=$?"
test 1 =
echo "语法错 退出码=$?"
test 1.5 -eq 1.5
echo "退出码=$?"
[ -f wtz-file.txt
echo "缺 ] 退出码=$?"

实测回显:

真 退出码=0
假 退出码=1
/tmp/test_2.sh: line 88: test: 1: unary operator expected
语法错 退出码=2
/tmp/test_1.sh: line 57: test: 1.5: integer expression expected
退出码=2
/tmp/test_1.sh: line 74: test: argument expected
缺 ] 退出码=2

💡 要点:0 真 / 1 假 / 2 用法错。注意 2 会破坏 if test ...; then 之外的粗心用法——比如 while test ... 遇到语法错误会终止循环而非当作假;A && test ... || 兜底 里 2 会触发兜底分支。报错文案速查:缺二元操作数右值 unary operator expected、操作数不完整 argument expected、缺 ]、非整数 integer expression expected。

3.14 [[ ]] 差异:模式匹配、未引号安全、&&/||、正则

[[ ]] 是 bash 关键字(非 test 命令),四项增强逐一实测:

[[ "wtzabc" == wtz* ]]; echo "[[ 模式匹配 退出码=$?"
[ "wtzabc" == "wtz*" ]; echo "[ 字面比较 退出码=$?"
v="hello world"
[[ $v == "hello world" ]]; echo "[[ 变量不引号安全 退出码=$?"
[ $v == "hello world" ]; echo "[ 变量不引号 退出码=$?"
[[ -f wtz-file.txt && -d wtz-dir ]]; echo "[[ 内 && 退出码=$?"
[[ -f wtz-file.txt || -f wtz-not-exist ]]; echo "[[ 内 || 退出码=$?"
[[ "wtz2026" =~ ^wtz[0-9]+$ ]]; echo "[[ 正则匹配 退出码=$?"
[[ "abc2026" =~ ^wtz[0-9]+$ ]]; echo "[[ 正则不匹配 退出码=$?"
[[ -z $wtz_undefined_var ]]; echo "[[ -z 未定义变量 退出码=$?"

实测回显:

[[ 模式匹配 退出码=0
[ 字面比较 退出码=1
[[ 变量不引号安全 退出码=0
/tmp/test_2.sh: line 68: [: too many arguments
[ 变量不引号 退出码=2
[[ 内 && 退出码=0
[[ 内 || 退出码=0
[[ 正则匹配 退出码=0
[[ 正则不匹配 退出码=1
[[ -z 未定义变量 退出码=0

💡 要点:四个差异全部实测坐实——① 右值不加引号是模式(wtz* 通配匹配为真;[ 里加不加引号都是字面量,wtz* 与 wtzabc 不等);② 变量不引号不做拆词(含空格也安全,[ 同写法直接 2);③ 表达式内可用 &&/||([ 里是关键字,会报 missing ]);④ 独有 =~ 正则匹配(右值正则建议不引号或存变量,引号会变字面量)。代价:[[ ]] 不是 POSIX,dash/sh 下不可用。

3.15 内建 test 与 /usr/bin/test 双形态对比

脚本与交互 shell 默认用内建(type -a test 首行);enable -n test 禁用内建后落到外部二进制。两形态功能行为一致(同族语义),差异在边缘细节:

enable -n test; type -a test
test -f wtz-file.txt; echo "禁用内建后 test 退出码=$?"
enable test; type -a test
/usr/bin/test 1 -lt 2; echo "/usr/bin/test 数值真 退出码=$?"
/usr/bin/test 1 -gt 2; echo "/usr/bin/test 数值假 退出码=$?"
/usr/bin/test -f wtz-file.txt; echo "/usr/bin/test -f 退出码=$?"
/usr/bin/test --version; echo "退出码=$?"
/usr/bin/test; echo "/usr/bin/test 无参数 退出码=$?"
/usr/bin/test 1 -eq; echo "/usr/bin/test 缺右操作数 退出码=$?"
test 1 -eq; echo "内建 test 缺右操作数 退出码=$?"
/usr/bin/[ -f wtz-file.txt ]; echo "/usr/bin/[ 真 退出码=$?"
/usr/bin/[ -f wtz-not-exist ]; echo "/usr/bin/[ 假 退出码=$?"

实测回显:

test is /usr/bin/test
test is /bin/test
禁用内建后 test 退出码=0
test is a shell builtin
test is /usr/bin/test
test is /bin/test
/usr/bin/test 数值真 退出码=0
/usr/bin/test 数值假 退出码=1
/usr/bin/test -f 退出码=0
退出码=0
/usr/bin/test 无参数 退出码=1
/usr/bin/test: missing argument after ‘-eq’
/usr/bin/test 缺右操作数 退出码=2
内建 test 缺右操作数 退出码=2
/usr/bin/[ 真 退出码=0
/usr/bin/[ 假 退出码=1

💡 要点:判定行为完全一致(数值/文件/单参数/无参数的退出码一一对应),可用性差异有两处——报错文案不同(内建 test: 1: unary operator expected,外部 missing argument after '-eq')、--version 都被当字符串判真(test 家族没有版本选项惯例,确认版本用 dpkg -S /usr/bin/test)。/usr/bin/[ 是与 /usr/bin/test 同族的独立二进制,末参数 ] 同样必需。

3.16 技术拓展:test/[ 与 [[ ]] 对比总表

综合本章实测的差异汇总(用法示例均出自上文回显):

维度test / [ ][[ ]]
本质命令(内建优先,外部 /usr/bin/test 兜底)bash 关键字(仅 bash)
可移植性POSIX,dash/sh 通用bash/zsh/ksh 专属
右值通配符一律字面量([ a == wtz* ] 为假,实测)不引号即模式([[ wtzabc == wtz* ]] 为真)
正则匹配无=~ 支持 ERE(实测 ^wtz[0-9]+$)
变量不引号拆词报错(too many arguments,退出 2)不拆词,安全(退出 0)
表达式内 && ||不行(&& 是 shell 语法需断开/转义)直接支持(实测退出 0)
< > 比较需转义 \<(防重定向),且依赖 locale直接写 <,按字典序
组合 -a -oPOSIX 标准但已标记淘汰用 && || 替代
单参数语义非空串即真([ -f ] 为真)同样有,但表达式解析更严格
推荐场景/bin/sh 脚本、POSIX 严格环境bash 脚本(配合 set -u 更稳)

选型结论:写 #!/bin/bash 脚本直接用 [[ ]](免引号踩坑、有正则);写 POSIX sh 或被 source 进非 bash 环境的代码用 [ ] 并永远给变量加双引号。两者退出码体系一致(0/1/2),混用不冲突。

四、实战示例

4.1 场景一:文件就绪检查(存在 + 非空 + 可读)

数据文件落地后处理前的一次性校验,三个条件串联:

cd /root/demo/test
if test -s wtz-file.txt && test -r wtz-file.txt; then echo '文件就绪,可处理'; else echo '文件缺失或为空'; fi

实测回显(wtz-file.txt 8 字节可读):

文件就绪,可处理

把样本换成空文件再跑一遍:

if test -s wtz-empty.txt && test -r wtz-empty.txt; then echo '文件就绪,可处理'; else echo '文件缺失或为空'; fi

实测回显:

文件缺失或为空

要点:-s 而非 -e——0 字节文件常代表「写了一半/下载失败」,-s && -r 两连判把权限问题也拦住;用 && 而非 -a 让失败原因可通过分段日志定位。

4.2 场景二:脚本参数校验(-z 判空 + usage 退出)

#!/bin/bash
if [ -z "$1" ]; then
    echo "用法: $0 <目标目录>" >&2
    exit 2
fi
if [ ! -d "$1" ]; then
    echo "错误: '$1' 不是目录" >&2
    exit 1
fi
echo "参数校验通过: $1"

实测回显(分别以缺参、非目录、合法目录调用):

用法: /tmp/wtz-check.sh <目标目录>
(缺参退出码 2)
错误: '/nonexistent-wtz' 不是目录
(非目录退出码 1)
参数校验通过: /root/demo/test
(合法目录退出码 0)

要点:参数缺失用 -z(引号包裹应对一切空值);! -d 排除「存在但不是目录」;错误码分层(2 用法错、1 业务错)便于调用方区分。[ ] 写法保证脚本可在 /bin/sh 下运行。

4.3 场景三:[[ ]] 正则做日志格式校验

要求输入行符合「wtz+纯数字」的请求 ID 格式,[[ =~ 一步完成:

line="wtz2026"
if [[ "$line" =~ ^wtz[0-9]+$ ]]; then echo "请求ID合法: ${BASH_REMATCH[0]}"; else echo "请求ID非法"; fi
line="abc2026"
if [[ "$line" =~ ^wtz[0-9]+$ ]]; then echo "请求ID合法"; else echo "请求ID非法"; fi

实测回显:

请求ID合法: wtz2026
请求ID非法

要点:=~ 右侧正则不加引号(引号后 ^wtz[0-9]+$ 变字面量恒不匹配);匹配成功后 BASH_REMATCH[0] 保存整体命中、[1] 起为分组捕获——提取与校验一次完成,这是 [[ ]] 相对 grep -E 管道最大的优势。

五、注意事项与常见问题

Q1:test 到底怎么报告结果?为什么它没有任何输出? test 只设退出码:真 0、假 1、语法/用法错误 2(3.13 全量实测)。它从不打印内容,判断结果靠 $? 或直接放进 if/while/&&/||。所以 test -f x 单独执行「看不到任何东西」是正常的,要看结果 echo $? 或 test -f x && echo yes。

Q2:[ 是命令还是语法?[ ... ] 里为什么必须有空格? [ 是一个真实的命令(test 的别名,内建优先、/usr/bin/[ 兜底,2.3/3.15 实测),] 只是它的最后一个参数。因此 [ 后、] 前必须有空格(参数分隔),[$a=$b] 会被当成一个叫 [$a=$b] 的命令而 127。[[ 则是 bash 关键字,解析发生在 shell 内部,规则更宽松(见 3.14)。

Q3:[ $var = "x" ] 偶尔报 too many arguments,为什么? $var 未加双引号时发生拆词:值为 "hello world" 时 [ 收到 5 个参数,报 [: too many arguments 退出 2(3.12 实测)。规则:[ ] 里的变量展开必须加双引号 [ "$var" = "x" ];bash 脚本改用 [[ $var == "x" ]] 则天然免引号(3.14 实测)。

Q4:字符串比较和数值比较结果互相矛盾? 两套体系:</>(需转义)按字典序(10 \< 2 为真,3.6),-lt 等按数值(10 -lt 2 为假,3.7)。判数值永远用 -eq/-ne/-lt/-le/-gt/-ge;混用非整数还报 integer expression expected 退出 2。同理 = 判字符串、-eq 判整数,两边不可互换。

Q5:-e、-f、-L 对符号链接的判定为什么互相矛盾? 视角不同:除 -h/-L 外所有文件操作符作用于链接目标(help 原文声明)。断链场景实测(3.1/3.4):-e 假(目标不存在)、-L 真(链接本身存在)。判断「软链接有效」组合使用:[ -L f ] && [ -e f ] 有效、[ -L f ] && [ ! -e f ] 断链。

Q6:内建 test 和 /usr/bin/test 有行为差异吗?enable -n test 有什么用? 判定行为一致(3.15 实测数值/文件/单参数退出码一一对应),差异只在报错文案(unary operator expected vs missing argument after '-eq')与 --version(两者都当字符串判真、无输出)。enable -n test 临时禁用内建走外部二进制,用于验证脚本对 dash 等「无内建 test 差异」环境的兼容性,验证完 enable test 恢复。日常脚本无需操作,bash 会自动选内建(无 fork 开销)。

六、参考链接