刚接触Linux那会儿,我特别羡慕那些能随手敲出一堆命令的老手。后来有次我需要给一台刚装完的服务器做“体检”,想确认系统里到底装了哪些可用命令,结果发现常规手段都不太好使——ls只能看目录内容,which一次只能查一个,find /usr/bin -type f列出来的文件又多又杂,还得自己过滤掉非执行文件。折腾一圈后,我偶然在Bash的man手册里翻到compgen这个内置命令,一试之下彻底打开了新世界。
compgen不是什么明星命令,不会排在各种Linux常用命令排行榜的开头位置,它更像一个藏在Bash内部的高级工具箱。最常用的用法就一句话:执行compgen -c,把当前shell环境里所有可用命令一次性列出来。很多网上流传的Linux命令大全,至少在执行文件枚举这一步,跟compgen -c干的是同一件事。这篇文章的目标读者很明确:对Linux已有一定基础、想提升命令行效率、或者需要在脚本和运维场景里动态处理命令清单的人。看完之后,你会明白compgen的原理、常用参数,以及如何在实际工作中把它用好。
1. compgen到底是什么——先搞清楚它的家底
1.1 不是独立命令,是Bash内置函数
先说一个常见误区:compgen并不是/bin或/usr/bin下的独立二进制程序,在文件系统里没有对应路径,它是Bash shell自带的补全生成命令,源码层面跟Bash的complete机制属于同一套体系。这也解释了为什么用which compgen查不到任何结果,而在Bash里直接敲compgen又能正常执行。你输入compgen,实际上是在跟当前shell对话,而不是向操作系统发起一次外部命令调用。
试着验证一下:
bash复制$ type compgen
compgen is a shell builtin
$ which compgen
# 没有任何输出
$ enable -a | grep compgen
enable compgen
type输出中的shell builtin就是最直接的证据。这个身份引出一个重要推论:只要有Bash就有compgen,跟你有没有装coreutils、busybox是否完整无关。哪怕是一个只放下bash和最小工具链的嵌入式Linux环境,只要交互shell选的是bash,compgen就能用。相反,如果你日常用的是sh、dash、zsh或者fish,那对不起,compgen大概率不存在。很多人在脚本里踩到compgen: command not found的坑,往往就是用了sh执行脚本而不是bash。
1.2 基础语法:选项加可选前缀
compgen的语法结构很简洁:
bash复制compgen [option] [word]
option决定补全类型,word是可选的过滤前缀。比如compgen -c git,会列出所有以git开头的可执行命令、别名、内置命令和函数。注意这个word不是通配符,不做模糊匹配,只做前缀匹配。想找网络相关命令?compgen -c net只会给出net开头的,比如netstat,不会帮你带出ifconfig。想要模糊匹配就得继续接管道grep。
为什么设计成前缀匹配?因为补全机制的本职就是帮shell实现Tab键补全。你在命令行敲了net然后按Tab,shell关心的就是net之后还能补什么,前缀过滤是最高效也最贴合使用场景的方式。理解了这一点,也就理解了compgen的定位:不是为了列清单而生的通用工具,而是把shell补全能力暴露给用户层面的一个接口。
1.3 跟Tab补全究竟是什么关系
在Bash里按Tab,shell内部会调用程序化补全机制去生成候选词。compgen就是把同样的机制暴露出来,让你手动执行。简单说,你手敲compgen -c的效果,相当于对“空命令”按了一次Tab并强制打印所有候选;而compgen -c git,则相当于输入git后再按Tab,只是不会真的去改变你的命令行。
这个关系非常有用。当你发现某个命令的Tab补全行为不太对劲时,可以用compgen配合同样的选项去调试候选列表,看看shell到底能不能生成你期望的补全结果。我早期写Bash补全脚本时,最常用的调试手段就是手动执行compgen,而不是反复去按Tab观察。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 用compgen列出所有Linux命令——核心玩法
2.1 -c参数:一键拉出全部可用命令
compgen -c是我用得最频繁的参数,它会结合当前PATH环境变量扫描出所有可执行的命令,同时把Bash的别名、内置命令、函数也一并算进来。换句话说,它输出的不止是文件系统里那些二进制,而是“你在当前这个shell环境里实际能敲的所有命令”。这个差异非常关键,因为很多外部命令扫描工具会漏掉alias和builtin。
比如在bash里配置了ll='ls -alF',用ls /usr/bin绝对看不到ll,但compgen -c能把它列出来。同理,cd、echo、type这些内置命令也不存在对应文件,但compgen -c照样会输出。换句话说,compgen -c的结果覆盖了命令解析的所有来源,比单纯遍历PATH目录要全面得多。
示例:
bash复制$ compgen -c | head -15
.
.
# 具体内容取决于你的系统和PATH,通常包含
# alias、apt、apt-get、awk、bash、cat、cd、
# cp、curl、cut、date、echo、grep、ls... 等
要看到完整清单,直接compgen -c就行,数量通常相当可观。在我手头一台最小化安装的Debian上,输出有两千多行;在桌面发行版上三千多行很常见。第一次看到这个数字,你会直观地感觉到Linux系统比自己想象中要“富有”得多。
2.2 跟其他列命令的方式比,compgen赢在哪
在发现compgen之前,我试过好几种办法,各有各的坑。
| 方法 | 能否一次列出全部命令 | 是否包含别名/内置命令 | 操作成本 |
|---|---|---|---|
| ls /usr/bin | 只覆盖单目录 | 否 | 低,但不全面 |
| find / -type f -executable 2>/dev/null | 能列,但混杂大量库文件和脚本 | 否 | 输出噪声大 |
| type -a 命令名 | 一次只能查一个 | 是 | 无法批量 |
| echo $PATH | 只显示路径 | 否 | 还得自己遍历 |
| compgen -c | 一次全部 | 是 | 零成本,直接使用 |
find那种方式问题最大,它会扫出/usr/lib下面大量带可执行权限的.so文件,看着像命令,实际上根本不能直接运行。type -a虽然能查别名和内置命令,但一次只能查一个,没法做全量梳理。compgen -c把“全量枚举+来源覆盖”这两件事同时做完了,这是它最大的价值。
补充一点:compgen -c默认按命令名输出,不会带完整路径,所以结果干净,方便后续处理。如果确实需要路径,可以再配合type -a单独查。
2.3 前缀过滤:批量查询以某字符串开头的命令
有时候你只想快速确认某些命令是否存在,或者看看某个命令族都有哪些成员。比如git通常不是单独一个二进制,它还会附带git-receive-pack、git-upload-pack等辅助命令。用compgen -c git可以一次看到所有git开头的命令:
bash复制$ compgen -c git
git
git-receive-pack
git-shell
git-upload-archive
git-upload-pack
如果拿不准名字拼写,只记得开头几个字母,这个前缀过滤功能比查man手册还快。比如想找跟系统服务管理相关的命令,可以先猜systemctl,然后compgen -c system看全家桶有哪些。再用grep来做非前缀的模糊匹配,比如compgen -c | grep -i ssh,就能顺手找出ssh、scp、sftp、ssh-agent这些相关项。
这里有个小技巧:结合sort和uniq处理输出。因为别名、内置命令和执行文件可能产生同名条目,虽然compgen在多数情况下会自动去重,但为了保险,做统计或者对比时建议先sort -u,避免把同一命令重复计入。
3. 不只是列命令——compgen参数全解析
3.1 参数全家桶:-a、-b、-k、-d、-f、-u、-g
compgen能补全的对象远不止命令,它的参数体系覆盖了shell里几乎所有需要补全的类型。
- compgen -a:列出当前会话所有别名。
- compgen -b:列出所有Bash内置命令,比如cd、echo、export、read。
- compgen -k:列出shell关键字,for、while、if、case这些流程控制词都在里面。
- compgen -d:列出当前目录下的目录名。
- compgen -f:列出当前目录下的文件名,包括目录。
- compgen -u:列出系统所有用户名。
- compgen -g:列出系统所有用户组。
这些参数单独用可能不起眼,但组合起来非常有用。比如在交互式脚本里想让用户选目录,可以先通过compgen -d生成候选列表塞给select循环;想知道系统上有哪些用户,compgen -u直接给你答案,不用去解析/etc/passwd。在很多场合,compgen的枚举结果比手动解析配置文件更可靠,因为它是shell自己视角下的世界。
3.2 -A家族:按类型精确控制补全来源
除了单个字母的简写参数,compgen还提供了一套更精确的-A参数,可以用类型关键字控制补全来源。常用的类型有:
- compgen -A command:等同-c,列出命令。
- compgen -A alias:等同-a,列出别名。
- compgen -A builtin:等同-b,列出内置命令。
- compgen -A function:列出当前shell里所有已定义的函数。
- compgen -A variable:列出所有shell变量。
- compgen -A exported:列出所有导出变量。
- compgen -A file:等同-f,列出文件。
- compgen -A directory:等同-d,列出目录。
- compgen -A signal:列出所有信号名,比如HUP、INT、TERM。
-A家族在写脚本时更友好,因为语义清晰,不会像-a和-b那样容易混淆。比如我需要遍历一个shell脚本里定义的所有函数,可以直接for fn in $(compgen -A function); do ...; done,不用去解析脚本源码。这在做函数名冲突检查和模块加载验证时非常省力。
有一个组合用法我常用来调试环境:compgen -A variable | sort,查看当前shell继承和设置了哪些变量。它比env输出更全,因为env只显示导出变量,而compgen -A variable会把未导出的shell变量也列出来。排查环境变量问题时,这个命令往往能帮上大忙。
3.3 用compgen打造自定义补全
compgen真正的大杀器是配合complete命令做自定义Tab补全。比如我给自己写过一个mycmd管理脚本,支持start、stop、restart、status几个子命令。默认情况下bash不会给mycmd做任何补全,但加上两行配置,就能获得完整的子命令补全:
bash复制_mycmd_complete() {
COMPREPLY=( $(compgen -W "start stop restart status" -- "${COMP_WORDS[COMP_CWORD]}") )
}
complete -F _mycmd_complete mycmd
这里的逻辑是:当Tab被按下时,complete触发_mycmd_complete函数,函数内部用compgen -W从一个单词列表里生成匹配当前输入的候选词,再赋给COMPREPLY数组,bash拿到数组后就能展示或补全。整个过程把Tab补全的“数据来源”完全交到了你手里。
再进一步,候选词可以来自文件系统、数据库查询结果,甚至另一个命令的输出。比如我想给一个work命令做项目目录补全,让work pro
bash复制_work_complete() {
local dirs
dirs=$(ls -d ~/projects/*/ 2>/dev/null | xargs -n1 basename)
COMPREPLY=( $(compgen -W "$dirs" -- "${COMP_WORDS[COMP_CWORD]}") )
}
complete -F _work_complete work
对于路径类补全,Bash其实有更轻量的complete -o dirnames,但通过compgen -W可以自由组合任意数据源,灵活性高很多。调试这类补全脚本时,先手动执行compgen -W "start stop restart status" -- "s",看输出是不是s开头的几个子命令,再回头看函数哪里写错,效率会高很多。
4. 运维实战:compgen的典型应用场景
4.1 新装系统后快速体检命令集
每次装完一台全新的Linux系统,我会习惯性跑一下compgen -c | wc -l,通过命令总数对系统有个整体判断。这个数在不同环境下差异很大:最小化安装的Debian/Ubuntu可能两千出头,带桌面环境的发行版经常三千以上,而裁剪过的嵌入式Linux可能只有几十。数字本身没有严格的好坏标准,但它是一个很好的参照指标。
如果某次安装后发现命令总数比预期少很多,我就会怀疑是不是PATH配置有问题,或者某些包没装全。比如/usr/local/bin没有加入PATH,那么即使你手工编译安装了软件,compgen -c也看不到对应的命令。这时候检查顺序通常是:先echo $PATH看路径是否完整,再ls对应目录确认二进制真实存在,最后排查是否有软链接丢失。compgen -c在这里充当了一个“集成测试”的角色,只要它列不出来,说明shell视角下命令确实不可用。
对于虚拟机和容器镜像,这个技巧同样适用。启动容器后第一件事跑compgen -c | wc -l,能快速判断基础镜像里预装了多少工具,比翻Dockerfile更直观。
4.2 脚本里批量检测命令是否存在
在写部署脚本或者巡检脚本时,经常需要判断一批依赖命令是否存在。网上常见的写法是command -v,一次判断一个。如果要批量检查几十个命令,我更喜欢用compgen -c把命令全集拉出来,再跟依赖清单做处理。比如:
bash复制#!/bin/bash
need_cmds=(curl wget git python3 make gcc)
missing=()
for cmd in "${need_cmds[@]}"; do
compgen -c "$cmd" | grep -qx "$cmd" || missing+=("$cmd")
done
if [ ${#missing[@]} -gt 0 ]; then
echo "缺失命令: ${missing[*]}"
exit 1
fi
这里用compgen -c "$cmd"做前缀匹配,然后grep -qx保证精确匹配整行,避免curl和curl-config这类前缀相同但命令不同的干扰。整个过程不会误判,也不会漏掉别名或者内置命令,这在执行环境差异较大的场景里很关键。比如某个工具实际是个alias,用test -f去检查二进制路径就会误报缺失,但compgen -c能看到alias,就能准确判断“这个命令名能不能用”。
注意:如果只是脚本里临时判断单条命令,command -v "$cmd"更轻量,没必要动用compgen。compgen的批量优势只有在命令数量多、需要一致逻辑时才明显。这算是工具选型上的一个心得:不是所有场景都用同一个命令,关键看匹配模式适不适合。
4.3 配合grep做命令审计和梳理
Linux系统里命令太多,很多时候你想知道自己这台机器上到底有哪些和某类功能相关的命令。compgen -c加上grep组合,就是一套轻量又灵活的命令审计工具。
想找网络相关的命令:
bash复制compgen -c | grep -iE 'net|ssh|scp|curl|wget|ping' | sort -u
想找压缩解压相关命令:
bash复制compgen -c | grep -iE 'tar|zip|gzip|bz2|xz|zstd'
想看看系统里有没有不该出现的可疑程序名,可以拉全量清单后做筛选核对。这种审计方式不需要任何第三方工具,纯bash就能完成,非常适合在受限环境或内网机器上使用。
如果清单要留档,输出重定向到文件再配合diff做前后对比,就能监控某台服务器上的命令集合是否发生变化。比如部署前跑一次compgen -c > before.txt,部署后再跑一次,用diff before.txt after.txt就能看出新增或者消失的命令。这个思路比单独比对目录内容更贴近“实际操作视角”,因为命令如果没有进入PATH,就算文件存在也无法直接调用。
4.4 嵌入式Linux与最小化环境下的实战经验
嵌入式Linux是我接触compgen最多的地方之一。很多设备为了节省存储空间,不会装完整的coreutils,命令数量大幅缩水,但如果你用BusyBox或者自定义rootfs,只要能确保shell是bash,compgen依然可用。这时候它就是排查“这个命令到底编译进去没有”的好帮手。
举个例子,在某个精简系统上,我怀疑tcpdump命令没有被编译进固件,直接执行会报not found。这时候可以先后台执行compgen -c tcp,立刻就能看到补全候选里有没有tcpdump。如果有,说明命令名在PATH里,只是执行环境有问题;如果没有,说明固件确实没包含它。这比翻看busybox符号链接或者文件系统清单要快得多,尤其在不开终端、只能用串口调试的嵌入式设备上,少一次文件系统遍历就少一分麻烦。
需要特别提醒的是,嵌入式系统里默认shell往往被设置成sh,而sh可能只是dash或者busybox ash的符号链接,这种情况下compgen会直接报command not found。解决方法是显式调用bash:/bin/bash -c 'compgen -c',或者把脚本的shebang写成#!/bin/bash。
提示:脚本里使用compgen前,先确认shebang是#!/bin/bash,不要用sh去执行。这是我踩过最多的坑,没有之一。
5. 常见问题与排查手册
5.1 为什么报compgen: command not found
这是搜索引擎里出现频率最高的问题之一,原因通常不是系统坏了,而是当前shell不对。compgen是Bash内置命令,只有Bash才认识它。如果你在sh、dash、fish或者zsh里执行compgen -c,shell会告诉你这个命令不存在,因为它们没有实现compgen。
排查步骤很直接:
bash复制echo $SHELL # 看默认shell
ps -p $$ # 看当前进程实际用的shell
bash --version # 确认bash版本
bash -c 'compgen -c | head' # 临时验证
如果确认当前就是bash还报错,再看bash版本。compgen从bash 2.0时代就有,现代系统基本不会低于这个版本,所以版本问题很少见,但极老的嵌入式环境或者自制rootfs上确实可能碰上。另外在cron、systemd定时任务里跑脚本时,默认shell可能是sh,脚本内使用compgen前必须显式切换到bash。
5.2 Bash与zsh、fish的差异
很多从zsh转过来的朋友习惯在zsh里敲compgen,然后一脸疑惑。zsh确实没有compgen,它有自己的一套补全系统。如果你需要在zsh里达到类似效果,可以用print -rl -- $commands列出PATH里的外部命令,用whence -m '*'列出函数或者别名。fish就更不一样了,补全脚本自成体系,跟bash完全不兼容。
| Shell | 是否有compgen | 替代方案 |
|---|---|---|
| bash | 有 | compgen -c |
| zsh | 无 | print -rl -- $commands |
| fish | 无 | functions、builtin -n |
| dash/sh | 无 | 不支持 |
这样的差异说明,compgen不仅是工具,更是bash生态的一部分。如果你长期在bash工作环境里,花时间研究compgen是比较值得的;如果主力shell是zsh或fish,不如直接学对应的原生产物,硬套bash思路反而别扭。
5.3 命令太多、输出太慢怎么办
当PATH包含大量目录、文件系统响应慢的时候,compgen -c可能明显卡顿。在普通桌面系统上这个感觉不强烈,但在某些目录挂在网络存储上的环境里,PATH遍历会拖慢整体响应。
我的做法是给常用场景建立缓存。比如在.bashrc里加一组别名:
bash复制alias cmdcache='compgen -c | sort -u > ~/.cache/cmds.txt'
alias lscmds='cat ~/.cache/cmds.txt'
定期跑一次cmdcache生成快照,平时查询直接cat,避免每次都实时遍历。系统命令集合变化频率其实很低,这种缓存方式完全够用。如果需要最新结果,随时手动再跑一次即可。
如果觉得compgen -c的输出重复条目多,排序前记得加sort -u。个别老版本bash在PATH目录存在同名可执行文件时,会输出重复项,统一在管道后面处理是最省心的。
5.4 与fzf组合成命令速选器
最后分享一个我每天在用的组合:compgen -c把全量命令拉出来,交给fzf做模糊搜索,选中后直接执行。这相当于给命令行配了一个可视化的命令选择器,特别适合那种“记得开头几个字母、拼不全名字”的情况。
bash复制run-cmd() {
local cmd
cmd=$(compgen -c | sort -u | fzf --preview 'man {} 2>/dev/null | head -40 || type {}')
[ -n "$cmd" ] && "$cmd"
}
按下快捷键调用run-cmd,输入关键词,预览窗口直接显示man手册或者类型信息,回车就能执行。第一次用的时候,那种“原来我系统里有这么多好用的命令”的感觉,比翻十遍命令大全都来得直接。我把这个函数绑定到Ctrl+Space,几秒钟就能完成从检索到执行,效率提升非常明显。
需要注意的是,fzf并不是Linux自带工具,需要额外安装,但这不影响compgen本身的通用性。即使没有fzf,compgen -c | sort -u | less也能让你安安静静地把全量命令翻一遍。工具组合是加分项,compgen -c才是真正的底座。
我跟compgen从陌生到熟练,好像是花了一个下午的时间。那台刚交付的服务器任务结束后,我又在自定义补全、脚本检测、嵌入式调试里反复用到了它。现在每次装好一个Linux系统,第一个习惯动作还是compgen -c | wc -l。最后说一个小技巧:如果你在写bash脚本时需要处理补全逻辑,别急着写一长串case去匹配,先试试compgen能不能帮你生成候选列表,很多时候它已经替你把最麻烦的枚举部分做完了。
