which 命令详解与参数实测(Ubuntu 22.04)
以 Ubuntu 22.04(debianutils 5.5)实测讲解 which:-a 全部命中、退出码 0/1/2、PATH 顺序与实时扫描、alias/函数/内建不被识别的行为边界,并给出 which 与 type、command -v 的三兄弟对比。
which 命令详解与参数实测(Ubuntu 22.04)
目标:全面掌握 which 在 PATH 中定位可执行文件的行为——
-a列出全部命中、退出码 0/1/2 的判定规则、PATH 顺序与实时扫描特性、以及「which 不识别 alias/函数/内建」这一关键边界,并与type、command -v做三兄弟横向对比,全部命令与回显已在 Ubuntu 22.04(debianutils 5.5-1ubuntu2)实测通过。
一、环境信息
本文所有命令均在以下环境实测,回显逐字照抄:
- 操作系统:Ubuntu 22.04.5 LTS(
PRETTY_NAME="Ubuntu 22.04.5 LTS") - 内核架构:x86_64
- which 所属软件包:debianutils 5.5-1ubuntu2(which 是 POSIX shell 脚本
/usr/bin/which.debianutils) - 对照工具 type/command:bash 内建(bash 5.1.16(1)-release)
- 默认 PATH:
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - 执行身份:root
- 演示数据目录:
/root/demo/which/(含两个自制命令目录wtz-bin1、wtz-bin2)
二、命令概览
2.1 语法与测试数据
which 的作用只有一件事:按 PATH 环境变量的目录顺序,逐个查找参数给出的命令名对应的磁盘可执行文件,并把找到的完整路径打印到标准输出。语法(来自实测 which --help 输出的 Usage 行):
Usage: /usr/bin/which [-a] args
全部选项只有一个 -a:默认只打印第一个命中(PATH 靠前的那个),加 -a 打印 PATH 上所有同名命中。测试数据约定:演示命令统一使用 wtz- 前缀(如 wtz-hello、wtzmove),分别放在 /root/demo/which/wtz-bin1 与 /root/demo/which/wtz-bin2 两个自制目录中,用于构造「同名命令出现在 PATH 多个目录」的场景;每个参数小节均可独立执行复现。
2.2 --help 全量输出(逐行照抄)
debianutils 的 which 不支持 --help 长选项,直接执行会打印用法并报非法选项,以下为 which --help 的完整实测输出(这本身就是它的「帮助」形态):
$ which --help
Usage: /usr/bin/which [-a] args
Illegal option --
which --help 退出码=2
关键差异解读:与 GNU 风格工具不同,which 只接受 POSIX 短选项 -a,任何 -- 开头的长选项(--help、--version、-v)一律报 Illegal option 并以退出码 2 结束。这在使用脚本判断 which 成败时要特别注意:退出码 2 表示「用法错误」,0/1 才表示「找到/没找到」。
2.3 版本与归属信息
Ubuntu 22.04 的 which 属于 debianutils 软件包,且 /usr/bin/which 本身是个经过 alternatives 机制中转的符号链接,实测链路如下:
$ dpkg -s debianutils | grep -E '^(Package|Version|Status)'
Package: debianutils
Status: install ok installed
Version: 5.5-1ubuntu2
$ dpkg -S /usr/bin/which.debianutils
debianutils: /usr/bin/which.debianutils
$ ls -l /usr/bin/which /etc/alternatives/which
lrwxrwxrwx 1 root root 26 Oct 15 09:04 /etc/alternatives/which -> /usr/bin/which.debianutils
lrwxrwxrwx 1 root root 23 Sep 24 14:05 /usr/bin/which -> /etc/alternatives/which
$ readlink -f /usr/bin/which
/usr/bin/which.debianutils
$ file /usr/bin/which.debianutils
/usr/bin/which.debianutils: POSIX shell script, ASCII text executable
which 没有 --version 选项(同 2.2,报 Illegal option -- 退出码 2),确认版本用 dpkg -s debianutils。真实实现是 2000 余行的 POSIX shell 脚本 which.debianutils,/usr/bin/which 通过 alternatives 链接指向它——Debian 系保留这套中转是为了允许其他实现(如 GNU which)注册替换。
💡 技术拓展:「查一个命令到底是什么」在 Linux 上有三个常驻工具,定位各不相同——
工具 归属 查什么 识别 alias/函数/内建 which debianutils 5.5(外部脚本) 只查 PATH 上的磁盘文件 否,一律找不到 type bash 内建 命令在当前 shell 的全部解释形态 是,按实际优先级列出 command -v bash 内建 当前 shell 实际会执行的那个 是,-V 给详细描述 which 的价值在于「绕开 shell 当前状态、模拟纯 PATH 查找」;type/command 的价值在于「反映当前 shell 真实生效的解释」。三者对比的完整实测见 3.5 与 3.7 节。
三、参数逐项实测
3.1 基础查找:which 命令名
本节验证最常用形态:给一个或多个命令名,which 逐个打印 PATH 上第一个命中的完整路径。预期结果是所有磁盘上存在的命令都以「目录/文件名」形式输出:
which bash
which ls
which gzip
which mktemp
echo "找到时退出码=$?"
which bash gzip zcat mktemp
实测回显:
$ which bash
/usr/bin/bash
$ which ls
/usr/bin/ls
$ which gzip
/usr/bin/gzip
$ which mktemp
/usr/bin/mktemp
$ echo "找到时退出码=$?"
找到时退出码=0
$ which bash gzip zcat mktemp
/usr/bin/bash
/usr/bin/gzip
/usr/bin/zcat
/usr/bin/mktemp
💡 要点:
- 输出的是绝对路径而非相对路径,即使该命令是通过 PATH 里的相对目录项(如
.)命中也是如此。 - which 支持一次传多个参数,逐行输出各自的命中路径;找到任一即整体退出码 0(部分找不到的混合场景见 3.3)。
- 默认 PATH 顺序是
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin,Ubuntu 22.04 因 usrmerge,/bin是指向/usr/bin的符号链接,所以/usr/bin/ls与/bin/ls是同一文件(-a时两个都会列出,见 3.2)。
3.2 -a:列出 PATH 上全部命中
-a(all)把 PATH 上每一个含同名可执行文件的目录都输出一行,按 PATH 顺序排列。这是排查「同名命令多版本」的核心参数:
which -a ls
which -a echo
which -a test
echo "which -a test 退出码=$?"
实测回显:
$ which -a ls
/usr/bin/ls
/bin/ls
$ which -a echo
/usr/bin/echo
/bin/echo
$ which -a test
/usr/bin/test
/bin/test
$ echo "which -a test 退出码=$?"
which -a test 退出码=0
再构造一个同名命令分布在两个自制目录的场景,同时验证「默认只报第一个、-a 全报」:
printf '#!/bin/bash\necho "i am wtz-bin1"\n' > /root/demo/which/wtz-bin1/wtz-hello
printf '#!/bin/bash\necho "i am wtz-bin2"\n' > /root/demo/which/wtz-bin2/wtz-hello
chmod +x /root/demo/which/wtz-bin1/wtz-hello /root/demo/which/wtz-bin2/wtz-hello
export PATH=/root/demo/which/wtz-bin1:/root/demo/which/wtz-bin2:$PATH
echo "当前 PATH 前三段: $(echo $PATH | cut -d: -f1-3)"
which wtz-hello
which -a wtz-hello
实测回显:
当前 PATH 前三段: /root/demo/which/wtz-bin1:/root/demo/which/wtz-bin2:/usr/local/sbin
/root/demo/which/wtz-bin1/wtz-hello
/root/demo/which/wtz-bin1/wtz-hello
/root/demo/which/wtz-bin2/wtz-hello
💡 要点:
- 不带
-a时 which 只打印 PATH 上第一个命中的目录(这里 shell 实际会执行wtz-bin1里的版本);加-a后wtz-bin1、wtz-bin2两个都列出,顺序与 PATH 一致。 ls、echo、test的-a输出出现/usr/bin/ls与/bin/ls两行,是 usrmerge 下同一文件的两个路径视图,不是两个程序。-a不改变退出码逻辑:全部找不到仍为 1(实测which -a wtz-not-exist-cmd-wtz退出码=1),找到任一为 0。
3.3 退出码实测:0 / 1 / 2 三种取值
which 的退出码是脚本化使用的主要接口,三种取值全部实测:
which bash; echo "找到时退出码=$?"
which wtz-not-exist-cmd; echo "找不到时退出码=$?"
which bash wtz-not-exist-cmd; echo "部分找不到退出码=$?"
which -a wtz-not-exist-cmd-wtz; echo "which -a 找不到 退出码=$?"
which --version; echo "which --version 退出码=$?"
which
echo "无参数退出码=$?"
实测回显:
$ which bash; echo "找到时退出码=$?"
/usr/bin/bash
找到时退出码=0
$ which wtz-not-exist-cmd; echo "找不到时退出码=$?"
找不到时退出码=1
$ which bash wtz-not-exist-cmd; echo "部分找不到退出码=$?"
/usr/bin/bash
部分找不到退出码=1
$ which -a wtz-not-exist-cmd-wtz; echo "which -a 找不到 退出码=$?"
which -a 找不到 退出码=1
$ which --version; echo "which --version 退出码=$?"
Illegal option --
Usage: /usr/bin/which [-a] args
which --version 退出码=2
$ which
$ echo "无参数退出码=$?"
退出码=1
💡 要点:
0= 全部参数都找到;1= 有任一参数没找到(包括部分找不到、无参数空查);2= 用法错误(非法选项)。- 脚本判断「外部命令是否安装」时,
which 命令 >/dev/null 2>&1配合$?可行,但要注意它对内建命令恒返回 1(见 3.5),更稳的惯例是command -v(见 3.7)。 - 无参数调用 which 不报错、静默退出码 1,这一行为在把
$?当「最后一个查询结果」的脚本里容易踩坑。
3.4 PATH 顺序与实时扫描:hash 缓存对照
which 每次调用都重新扫描当前 PATH,不使用 bash 的 hash 缓存。用同一个命令文件在两个目录间移动来实测两者差异:
printf '#!/bin/bash\necho v1\n' > /root/demo/which/wtz-bin1/wtzmove
chmod +x /root/demo/which/wtz-bin1/wtzmove
export PATH=/root/demo/which/wtz-bin1:/root/demo/which/wtz-bin2:$PATH
wtzmove
type wtzmove
mv /root/demo/which/wtz-bin1/wtzmove /root/demo/which/wtz-bin2/wtzmove
type wtzmove
which wtzmove
hash -r
type wtzmove
实测回显:
v1
wtzmove is hashed (/root/demo/which/wtz-bin1/wtzmove)
wtzmove is hashed (/root/demo/which/wtz-bin1/wtzmove)
/root/demo/which/wtz-bin2/wtzmove
wtzmove is /root/demo/which/wtz-bin2/wtzmove
💡 要点:
- 文件从 wtz-bin1 移到 wtz-bin2 后,
type仍报旧位置(bash 的 hash 缓存了第一次解析结果,实际执行也会走旧路径直到hash -r);which立即报告新位置——它每次都实时遍历 PATH。 - 这个差异在生产排障里非常实用:「换了二进制路径但不生效」多半是 bash hash 缓存问题,
which能看到新位置正是判断依据;让 shell 生效用hash -r或exec bash。 - which 的搜索范围只有 PATH,不含 bash 的 hash 表、alias、函数与内建注册表,所以结果始终反映「PATH 此刻长什么样」。
3.5 which 不识别 alias、函数与内建
which 的能力边界:它只认磁盘文件,对 shell 内部定义的 alias、函数、内建命令一律返回「找不到」。在同一个 shell 里用 type 对照实测:
shopt -s expand_aliases
alias wtzll='ls -alF'
which wtzll
echo "which wtzll 退出码=$?"
type wtzll
wtzmyfunc() { echo 'hello from func'; }
which wtzmyfunc
echo "which 函数退出码=$?"
type wtzmyfunc
which cd
echo "which cd 退出码=$?"
type cd
which echo
echo "which echo 退出码=$?"
type echo
实测回显:
$ which wtzll
which wtzll 退出码=1
wtzll is aliased to `ls -alF'
$ which wtzmyfunc
which 函数退出码=1
wtzmyfunc is a function
wtzmyfunc ()
{
echo 'hello from func'
}
$ which cd
which cd 退出码=1
cd is a shell builtin
$ which echo
/usr/bin/echo
which echo 退出码=0
echo is a shell builtin
在交互式 shell 中复核 alias 场景(交互 shell 才默认开启 alias 展开):
bash -i -c "alias wtzx='echo hi'; type wtzx; which wtzx; echo which退出=\$?"
实测回显:
wtzx is aliased to `echo hi'
which退出=1
💡 要点:
- alias、函数、纯内建(cd/type/command)在 which 眼里不存在,退出码一律 1;
type则如实报告aliased to/a function/a shell builtin。 echo是特例:它是内建,但磁盘上也有/usr/bin/echo,which 找到文件就返回 0——which 找到 ≠ 当前 shell 会执行它(内建优先级更高,真正执行的是内建 echo)。- 因此 which 的语义是「PATH 上有没有这个可执行文件」,而 type 的语义是「当前 shell 会把这个名字解释成什么」。写脚本判断可用性时选哪个,取决于你想问哪个问题。
3.6 内建遮蔽下的磁盘文件查找:which -a test
test 命令是「内建 + 磁盘文件」双形态的典型(bash 内建 test 与 coreutils 的 /usr/bin/test 并存),用它验证 which 在内建遮蔽下仍能列出全部磁盘文件:
which test
echo "which test 退出码=$?"
which -a test
type -a test
实测回显:
$ which test
/usr/bin/test
$ which test 退出码=0
$ which -a test
/usr/bin/test
/bin/test
$ type -a test
test is a shell builtin
test is /usr/bin/test
test is /bin/test
💡 要点:which 只输出 /usr/bin/test、/bin/test 两行磁盘文件,对「test is a shell builtin」无感知;type -a 则把内建排在磁盘文件之前。想确认「脚本里 test 到底用的哪个实现」,答案是交互/脚本中一律内建,只有 enable -n test 禁用内建后才落到 /usr/bin/test——这正是 which 与 type 回答的是两个不同问题的直接证据。
3.7 技术拓展:which、type、command -v 三兄弟对比
三个工具的回答维度不同,实测汇总成对照表(行为均来自本章回显):
| 维度 | which | type | command -v |
|---|---|---|---|
| 归属 | debianutils 5.5(外部脚本) | bash 内建 | bash 内建 |
| 查找范围 | 仅 PATH 磁盘文件 | alias > 函数 > 内建 > 关键字 > PATH | 同 type 的解析顺序 |
| alias/函数/内建 | 找不到(退出码 1) | 如实报告形态 | 如实报告形态 |
| hash 缓存 | 不使用,实时扫 PATH | 报 is hashed (路径) | 同 type |
| 输出形态 | 绝对路径 | ls is /usr/bin/ls 句式 | 文件给路径、内建给裸名(如 cd) |
| 退出码 | 0 找到 / 1 没找到 / 2 用法错 | 全找到 0,任一没有 1 | 同 type |
| 脚本惯例 | 判断外部文件存在性 | 人工排障看全貌 | 推荐的存在性判断 |
command -v echo; which echo
command -v cd; which cd
实测回显:
echo
/usr/bin/echo
command -v cd 输出裸名 cd 且退出码 0(当前 shell 能执行),which cd 无输出退出码 1(PATH 上无文件);command -v echo 输出裸名、which echo 输出文件路径。一句话选型:脚本里判存在用 command -v,排障看全部形态用 type -a,只关心 PATH 文件本身才用 which。
四、实战示例
4.1 场景一:确认脚本依赖的外部命令装在哪
部署脚本前先确认关键二进制的落点与版本来源,避免「以为用的是 /usr/local/bin 的新版、实际跑的是 /usr/bin 旧版」:
which gzip zcat mktemp
dpkg -S "$(which zcat)"
实测回显:
/usr/bin/gzip
/usr/bin/zcat
/usr/bin/mktemp
gzip: /bin/zcat
要点:which zcat 得到路径后用 dpkg -S 反查所属包(gzip 包),形成「命令 → 路径 → 包」的排查链路;两者都基于磁盘事实,结果互相印证。
4.2 场景二:排查同名命令多版本遮蔽
新版工具装到 /usr/local/bin 后不生效是高频问题,用 -a 一次看全所有同名命中:
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
which -a ls
type -a ls
实测回显:
/usr/bin/ls
/bin/ls
ls is aliased to `ls --color=auto'
ls is /usr/bin/ls
ls is /bin/ls
要点:which -a 列出 PATH 上全部磁盘命中(usrmerge 下两行同一文件);type -a 额外暴露 .bashrc 第 71 行的 alias ls='ls --color=auto'——交互 shell 里 ls 实际先经过 alias 再落到文件。两个输出放在一起,「遮蔽来源」一目了然。
4.3 场景三:用 which 验证 PATH 改动是否生效
新增一个更高优先级的命令目录后,用 which 与实际执行做端到端验证:
mkdir -p /root/demo/which/wtz-newbin
printf '#!/bin/bash\necho "hello-from-wtz"\n' > /root/demo/which/wtz-newbin/wtz-hello
chmod +x /root/demo/which/wtz-newbin/wtz-hello
export PATH=/root/demo/which/wtz-newbin:$PATH
which wtz-hello
wtz-hello
实测回显:
/root/demo/which/wtz-newbin/wtz-hello
hello-from-wtz
要点:which 报告新目录里的路径、实际执行输出新脚本的内容(此前的 wtz-bin1/wtz-bin2 命中被新路径遮蔽),PATH 改动验证闭环。若 which 找得到但执行报「command not found」,检查文件是否 chmod +x 及 shebang 是否正确;若 which 找不到,用 echo $PATH 核对目录拼接是否遗漏。
五、注意事项与常见问题
Q1:为什么 which cd、which type 没有任何输出?
cd/type/command 等是 bash 内建命令,不在 PATH 上,which 只扫 PATH 所以找不到,退出码 1(见 3.5 实测)。查看内建用 type cd(输出 cd is a shell builtin)或 help cd。which echo 有输出是因为磁盘上恰好也有 /usr/bin/echo 这个外部实现,不代表当前 shell 执行的是它。
Q2:which --help / --version 报 Illegal option 是坏了吗?
不是。debianutils 的 which 只实现了 -a 一个短选项,任何长选项都报 Illegal option -- 并以退出码 2 结束(见 2.2/3.3 实测)。看用法就认 Usage: /usr/bin/which [-a] args 这一行;查版本用 dpkg -s debianutils。
Q3:脚本里用 which 判断命令存在,为什么有时误判?
两个原因:① 内建/alias/函数会被 which 判为「不存在」,但脚本实际能执行它们(bash 优先走内建);② which 只查 PATH,不含 hash 缓存与 shell 内部状态。脚本惯例改为 if command -v 命令名 >/dev/null 2>&1; then ...,它的判定与「当前 shell 实际会执行什么」一致(见 3.7 对比)。
Q4:which 找到的路径和实际执行的行为对不上?
典型原因是 bash hash 缓存:命令文件移动后 which 实时扫描报新路径,而已缓存的 shell 仍执行旧路径(见 3.4 的 wtzmove 实测)。让 shell 与磁盘重新对齐用 hash -r;反过来想看「shell 缓存的路径」用 type 命令名(输出 is hashed (...))。
Q5:退出码到底怎么用?
0 = 所有参数都在 PATH 上找到;1 = 至少一个没找到(含部分找不到、无参数);2 = 用法错误(非法选项)。注意「部分找不到」整体也是 1,所以多参数查询只能区分「全找到/没全找到」,要精确定位哪个缺,逐个查或配合 -a 输出判读。
Q6:/usr/bin/why 什么的——为什么 which 是个三层符号链接?
/usr/bin/which → /etc/alternatives/which → /usr/bin/which.debianutils(见 2.3 实测链路)。中间一层是 Debian alternatives 机制的管理入口,允许安装其他 which 实现(如 GNU which)后由 update-alternatives 切换;末尾的 .debianutils 后缀就是给「同槽位其他实现」让出的命名空间。行为差异排查时用 readlink -f /usr/bin/which 确认真实实现文件。
六、参考链接
- which(1) 手册(Ubuntu 22.04 jammy):https://manpages.ubuntu.com/manpages/jammy/en/man1/which.1.html
- debianutils 软件包页(Debian):https://packages.debian.org/jammy/debianutils
- GNU Coreutils 手册(which 不属 coreutils,可对照 command -v 语义):https://www.gnu.org/software/coreutils/manual/
- update-alternatives(8) 手册(alternatives 链接机制):https://manpages.ubuntu.com/manpages/jammy/en/man8/update-alternatives.8.html
- bash(1) 手册(type/command -v 内建语义):https://manpages.ubuntu.com/manpages/jammy/en/man1/bash.1.html