刚接触 Linux 的时候大多数人最先形成的肌肉记忆就是敲命令、敲到一半按 Tab让系统帮你补全。补全确实好用但你可能没想过那个瞬间 Bash 到底是怎么知道该给你列哪些候选词的。答案里藏着一个经常被忽略的内建命令compgen。compgen 的全称是 completion generator直译过来就是“补全生成器”。它是 Bash 内建命令本职工作是按照条件生成一串匹配的单词列表。这些单词既能直接打印在终端里也能被 Tab 补全机制拿来当作候选集。换句话说你在命令行里看到的那些自动提示、自动补全结果有很大一部分都由它在幕后产生。这篇文章我会从补全机制讲起把 compgen 的常用选项、真实运维场景、脚本写法逐个拆开。适合写 shell 脚本的人、想给自定义命令加补全的人以及只是对 Bash 内部机制好奇的朋友。不要求你有多深的编程基础只要常年在终端里敲命令读完就能用起来。1. 先从 Bash 补全机制说起compgen 到底承担什么角色1.1 一次 Tab 按下后发生了什么我先带你走一遍补全流程。当你在终端里输入systemctl statu然后按下 TabBash 并不是简单地去“猜”你想打什么。它实际上是在执行一套补全例程先判断当前命令行里输入的是哪个命令再根据这个命令对应的补全规则找到候选词集合最后把候选词里匹配你当前已输入前缀的那些项挑出来。整个过程大致是这样Bash 解析命令行找到当前光标位置对应的单词。根据命令名查找是否注册过补全规则。比如 systemctl 通常配有一套由 bash-completion 包提供的补全函数。调用对应的补全函数或者使用默认补齐逻辑。函数内部会调用 compgen 或者类似的匹配逻辑生成候选列表。Bash 把候选列表做前缀过滤、去重然后显示出来。所以你会发现真正“生成一堆可选名字”的动作在 Bash 里就是由 compgen 这类工具完成的。没有它补全就成了巧妇难为无米之炊。1.2 complete 和 compgen 的明确分工很多教程把complete和compgen放在一起讲但两者分工完全不同。complete是“注册规则”的命令它告诉 Bash当用户输入某个命令时应该用哪种方式去补全。比如complete -W start stop restart ./service.sh意味着输入./service.sh时候选词就是后面那三个。compgen是“生成结果”的命令它负责按照给定条件产出候选词。你可以单独在终端里运行它看它输出一串东西也可以在 complete 的函数定义里调用它把结果赋值给COMPREPLY数组。用个生活化一点的类比complete 是菜谱规定了做什么菜、用什么食材compgen 是灶台和锅按菜谱把食材真正炒出来。你打开终端自己敲compgen -c | head能看到一长串命令名这就是它在“出锅”。理解了这层关系之后再去用 compgen 就不会觉得它只是一个无聊的内建命令了。它是你自定义补全时绕不开的核心工具。2. 核心选项逐个拆解把 compgen 的“词库”打开2.1 常用字母选项compgen 最简单的用法就是指定一个类型它把该类下的所有名称都打印出来。比如compgen -c这条命令会列出所有可用命令名包括别名、内建命令、函数和可执行文件。下面是几个最常见的字母选项我把它们整理成了一张表。选项含义典型示例-a列出所有别名compgen -a-b列出所有 Bash 内建命令compgen -b-c列出所有命令别名、内建、函数、可执行程序compgen -c-d只列出目录名compgen -d-e列出所有已导出的变量compgen -e-f列出文件名compgen -f-g列出系统用户组compgen -g-j列出当前 shell 的任务jobcompgen -j-k列出 shell 保留关键字compgen -k-s列出服务名compgen -s-u列出系统用户compgen -u-v列出所有变量名compgen -v带一个过滤词也是常见做法比如我想看所有以sy开头的命令名compgen -c sy输出会包含systemctl、sysctl、systemd-analyze之类的名字。注意这里的匹配规则是前缀匹配不是子串匹配。也就是说compgen -c ctl不会匹配到systemctl因为名字开头并不是ctl。2.2 -A 分组一种更统一的查询方式老版本 Bash 中选项越来越多每个字母对应一种名称空间用起来不够灵活。-A选项提供了一种更统一的写法它用一个分组名来指定要查询的内容。分组名可以用完整名称也可以用缩写Bash 会尽量识别。我整理一下目前 Bash 支持的 -A 分组alias别名、arrayvar数组变量、bindingReadline 键绑定、builtin内建命令、command命令、directory目录、disabled被禁用的内建命令、enabled已启用的内建命令、export已导出变量、file文件、functionshell 函数、group用户组、helptopichelp 帮助主题、hostname主机名、job任务、keyword保留关键字、running运行中的任务、service服务名、setoptset 命令选项、shoptshell 选项、signal信号名、stopped已停止的任务、user用户名、variable全部变量。在实际使用中我比较常用的是这几个compgen -A user compgen -A export compgen -A directory compgen -A signalcompgen -A user和compgen -u是等价的compgen -A variable和compgen -v也是等价的。新版本里我更推荐用-A写法因为可读性好别人看脚本时一眼就知道你要查询的是哪个命名空间。2.3 -W 单词列表自定义候选词比上面这些更有意思的是-W它允许你把自定义的单词列表交给 compgen让它做前缀过滤。compgen -W start stop restart reload status s这段命令会把列表里以s开头的单词输出也就是start和status。如果用-W时不给过滤词就会把整个列表都打出来。这里的单词列表是冒号分隔的。如果你从某个变量里拿值正好可以用IFS或字符串处理来转换成这种格式。-W是补全函数中最常用的一个选项因为很多命令的合法参数就那么几个完全可以硬编码成一个空格分隔的字符串。后面我会专门讲怎么在自定义补全里用它。3. 实战场景这些命令放在真实环境里怎么用3.1 用 compgen 给 shell 脚本做参数补全假设我写了一个部署脚本deploy.sh它接受dev、test、prod这几个环境参数。我想让用户在使用时按 Tab 能提示这几个参数该怎么实现新建一个补全函数然后注册到 complete 上_deploy_completion() { local cur cur${COMP_WORDS[COMP_CWORD]} COMPREPLY( $(compgen -W dev test prod -- $cur) ) } complete -F _deploy_completion ./deploy.sh这里有几个关键点COMP_WORDS是当前命令行按空格拆分后的数组。COMP_CWORD是当前光标所在单词的下标。COMPREPLY是 Bash 约定的补全结果数组你要把候选词放进这个数组。compgen -W dev test prod -- $cur中的--表示后面的内容不要当成选项解析。如果用户已经输入了一个以横线开头的字符这个写法能避免被 compgen 误认为是自己的选项。把这段代码写进~/.bashrc或者一个单独的补全脚本里再 source 一下之后输入./deploy.sh d再按 Tab就会被补全成./deploy.sh dev。3.2 从系统命名空间获取动态列表除了自定义样例compgen 在系统信息查询方面也很有用。假设我写一个批量管理脚本要处理的主机清单和用户清单都来自系统本身我不想去硬编码那就可以这样# 列出可以操作的系统用户 compgen -u # 列出所有服务名 compgen -s | head -20 # 列出所有已导出的环境变量 compgen -e | grep -E ^PATH|^HOME这些输出是动态生成的意味着系统里新增了用户或者新的服务脚本下次跑的时候自动就能感知到不需要维护一个静态列表。比如检查某个用户是否存在可以这样写if compgen -u | grep -qx $target_user; then echo 用户存在 else echo 用户不存在 figrep -qx表示精确匹配整行避免user1匹配到user12的问题。这种写法比直接读/etc/passwd更稳妥因为 compgen 走的是系统标准的用户枚举路径遇到 LDAP 这类非本地用户也能覆盖到。3.3 用 -P 和 -S 生成带前缀后缀的候选如果你需要生成“看起来像完整选项”的结果-P和-S可以做前缀和后缀修饰。例如我想生成一组 URLcompgen -W api www admin -P https:// -S .example.com输出会是https://api.example.com https://www.example.com https://admin.example.com这在做测试时非常方便不用手动拼接字符串。另一个场景是补全目录时自动带斜杠。比如compgen -d -P ./ -S /会把目录名统一输出为./xxx/的形式省去后面补路径时还要补斜杠的麻烦。需要注意-P和-S只是字符串拼接不做合法性检查。如果拼接出来的文本不是你想要的多半是你的源列表本身有问题。3.4 用 -G 和 -X 做文件名匹配与过滤-G支持按照 glob 模式生成文件名-X则用来排除匹配模式。比如我要找出当前目录下的所有.log文件排除debug.logcompgen -G *.log -X debug.log这里的模式匹配走的是 Bash 的文件名扩展规则不是正则表达式。查日志的时候这个组合我经常用compgen -G app-*.log -X app-*.old | sort它会把符合app-*.log的文件列出来同时排除掉以.old结尾的旧日志。配合-S还能做“压缩包名”之类的输出修饰。4. compgen 的输出再处理让结果帮你干活4.1 命令替换的分词陷阱compgen 默认把每个结果输出为一行。这个特性在终端里看着清爽但在脚本里用命令替换时要小心。常见写法是for user in $(compgen -u); do echo 处理用户$user done用户名里通常没有空格这个写法基本安全。但如果换成处理文件名系统里的文件名完全可能带空格用命令替换就会被拆得乱七八糟。我见过不少新手在这个地方踩坑补全了一个带空格的文件名结果 for 循环把一行拆成了两段。正确做法是用while readwhile IFS read -r file; do echo 处理文件$file done (compgen -f)IFS read -r保证不做字段拆分、也不处理反斜杠转义。因为 compgen 输出是行分隔的这种读取方式最稳。4.2 按前缀过滤与 grep 组合虽然 compgen 支持直接传一个前缀词做过滤但在更复杂的场景下我会把它的输出再交给 grep 处理。grep 支持正则会灵活很多。比如我想找出所有命令里包含git的命令compgen -c | grep git这里用的是子串匹配能搜到git、gitk、git-flow、gh也可能被匹配到取决于具体命名。想只看以git开头的命令可以加^compgen -c | grep ^git我个人的惯例是简单的前缀匹配让 compgen 自己干复杂的模式匹配交给 grep。二者结合能让脚本逻辑很清晰。4.3 返回码与其他细节compgen 的退出码也很重要。它返回 0 表示有匹配项返回 1 表示没有匹配项。这给了脚本一个干净的判断方式。if compgen -u | grep -qx $name; then ... fi等价写法可以直接利用 compgen 前缀过滤if compgen -u $name | grep -q .; then echo 找到用户 $name ficompgen -u $name会输出所有以$name开头的用户名。但如果我想精确判断某个名字是否恰好是用户名就不能只靠前缀匹配还是需要 grep-x或者比较输出行。平时调试时可以直接在终端里看退出码compgen -u root echo $?有输出、返回码为 0就说明匹配成功没输出、返回码为 1则没有匹配。利用这个特性可以少写不少判断语句。5. 常见问题与排查技巧实录5.1 常见问题速查表我根据自己的使用经验把最容易踩的坑整理成了一张表。现象原因解决方法compgen 输出为空过滤词带了不可见字符或者匹配方式不对确认当前 locale必要时用od -c查看字节compgen 在 sh 下运行报错Bash 内建命令POSIX sh 不支持改用 Bash 执行脚本 shebang 写#!/usr/bin/env bash-W 列表包含多个空格的单词被拆开-W按空格分隔单词列表不要在-W中放入带空格的参数或用数组配合-A之类方案自定义补全不生效补全函数名注册错误或函数未 source用complete -p 命令名查看当前注册规则使用--和$cur时输入以横线开头被误解析忘记在过滤词前加--写成compgen -W ... -- $cur补全结果大量重复多个命名空间包含同名项统一用sort -u去重git 等命令的补全覆盖了自己的补全函数bash-completion 定义了更复杂的补全优先级确认补全函数名是否被覆盖必要时用complete -r清除再注册5.2 在补全函数中调试写补全函数最痛苦的是看不到中间变量。其实可以手动模拟 Bash 的补全流程先定义好COMP_WORDS和COMP_CWORD再调用你的补全函数然后打印COMPREPLY。# 模拟输入 ./deploy.sh pro COMP_WORDS(./deploy.sh pro) COMP_CWORD1 _deploy_completion declare -p COMPREPLY这样我不用真的去按 Tab就能看到补全函数到底生成了什么。遇到复杂的补全逻辑我还会在函数里临时加一行printf %s\n 当前单词: $cur 2输出会直接显示在终端里方便确认传入函数的参数对不对。调完再把这行删掉就行。5.3 几个容易忽略的细节我用 compgen 这些年有几个细节是常见文档里不会特意写明的这里单独提一下。第一compgen的结果顺序和地区有关也受 Bash 版本影响。如果你把结果再交给表格或菜单显示最好先sort一下。第二-A交互式查询结果和compgen -c的结果可能有重复比如别名、内建命令、函数都被归到 command 类别之下。所以在写脚本时如果要做精确归类用-a、-b、-A function分别拿比用-c更可控。第三compgen 在处理文件名时会受FIGNORE变量影响。这个变量里列出的后缀会被忽略比如你设置了FIGNORE.o:.txt那么compgen -f就不会返回这些文件。遇到“文件怎么少了一些”的怪问题先查它。这些细节不是考试点但排查问题的时候能帮你省大量时间。6. 扩展从 compgen 到更复杂的 Bash 补全体系6.1 一个完整自定义补全的例子只学会了 compgen 的单个选项还不够真实场景中补全往往要同时支持子命令、选项、文件路径。我给你看一个相对完整的示例一个叫log-tool的脚本支持view、tail、stats三个子命令并且view和tail后面要接日志文件名。_log_tool_completion() { local cur prev subcmd COMPREPLY() cur${COMP_WORDS[COMP_CWORD]} prev${COMP_WORDS[COMP_CWORD-1]} # 如果当前输入是第一个位置参数补全子命令 if [[ $COMP_CWORD -eq 1 ]]; then COMPREPLY( $(compgen -W view tail stats -- $cur) ) return fi subcmd${COMP_WORDS[1]} case $subcmd in view|tail) # 第二个参数补全日志文件 COMPREPLY( $(compgen -f -- $cur) ) ;; stats) # stats 子命令接受 -h --human 等选项 COMPREPLY( $(compgen -W -h --human --bytes --count -- $cur) ) ;; *) COMPREPLY( $(compgen -f -- $cur) ) ;; esac } complete -F _log_tool_completion log-tool这段逻辑并不复杂但它在推动 Bash 补全的边界。你可以看到compgen 只是被反复用在了不同条件下配合 case 分支就能做出像模像样的 CLI 交互体验。另外如果想让补全结果里包含目录并且自动给目录加斜杠可以在注册补全时加行为选项complete -o filenames -F _log_tool_completion log-tool-o filenames会告诉 Bash 按文件名规则处理结果目录后面自动补/文件里的特殊字符也能正确转义。这是我自己做补全时必加的一项。6.2 性能与脚本规范建议compgen 本身执行很快真正影响性能的是它背后涉及的命令和文件系统操作。比如compgen -u在系统用户非常多、且走网络用户目录服务时会变慢compgen -f在大目录下会明显变慢。我一般的原则是把补全函数保持在毫秒级避免在 Tab 按下时执行重命令。需要查询系统服务或用户时给结果做缓存。补全函数里不要写交互式逻辑它只负责产出候选词。能用-W硬编码的不要每次都动态计算。性能问题不只在 compgen 本身更多是使用方式的问题。让 Tab 补全整个过程保持迅捷用户才会愿意用这个功能。我在实际项目中经常会把一组常用的 compgen 查询封装成小函数放进 bashrc比如cmds() { compgen -c $1; }、users() { compgen -u $1; }这样日常排查时敲两个字母就能快速列出想要的命名空间。一个顺手的小工具关键是要知道它在你按下 Tab 的那一刻已经默默做了多少事。下次再有人问你“Linux 命令大全怎么背”你可以告诉他用 compgen把整台机器的命令、用户、变量都列出来比死记硬背有用多了。
