zhangguanzhang's Blog

bash自定义补全

字数统计: 2.4k阅读时长: 9 min
2017/06/03 Share

总结下bash的补全

终端下按两次tab就是补全

补全常见的分为下面几种场景:

  • 第一部分的命令补全
  • 命令补全后补全路径到文件
  • 变量补全
  • 类似systemctl命令的选项补全

当然bash支持自定义补全,也就是让我们实现和systemctl命令那样补全

假设我们要systemctl disable --now firewalld
systemctl空格后按tab键会显示systemctl的第二个部分的所有支持的选项
此时继续输入ditab键会补全成systemctl disable,输入--now(相似的太多了,所以基本输入完)补全,当然最后的服务名此时补全不了,因为systemctl的补全不怎么完美

通过上面的过程知道了补全不单单是常见的前两种场景,如果我们使用第四种场景去自定义会大大加快我们的工作效率
一般我们会安装bash-completion包来得到更好的补全效果,这个包提供了一些现成的命令补全脚本,一些基础的函数方便编写补全脚本,还有一个基本的配置脚本。但也正如之前说的,这个包不是必须的,只不过可以省些力气。

bash补全相关脚本和加载流程

官方文档就那么点介绍,网上看到的博客也不详细,看了一些官方的补全脚本注释又太少,这里写下我的摸索经验

安装了bash-completion后会有下面这几个文件(建议最好有两台环境,一个安装一个不安装方便文章后面去测试)

PS: 很多安装的软件都有补全脚本,可以去/usr/share/bash-completion/completions/看看,有的话cp到/etc/bash_completion.d/里即可生效

1
2
/etc/profile.d/bash_completion.sh
/usr/share/bash-completion/bash_completion

先看下/etc/profile.d/bash_completion.sh,毕竟在/etc/profile.d目录里,/etc/profile里有source /etc/profile.d/*.sh类似代码
所以/etc/profile.d/bash_completion.sh会被先执行,代码内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Check for interactive bash and that we haven't already been sourced.
[ -z "$BASH_VERSION" -o -z "$PS1" -o -n "$BASH_COMPLETION_COMPAT_DIR" ] && return

# Check for recent enough version of bash.
bash=${BASH_VERSION%.*}; bmajor=${bash%.*}; bminor=${bash#*.}
if [ $bmajor -gt 4 ] || [ $bmajor -eq 4 -a $bminor -ge 1 ]; then
[ -r "${XDG_CONFIG_HOME:-$HOME/.config}/bash_completion" ] && \
. "${XDG_CONFIG_HOME:-$HOME/.config}/bash_completion"
if shopt -q progcomp && [ -r /usr/share/bash-completion/bash_completion ]; then
# Source completion code.
. /usr/share/bash-completion/bash_completion
fi
fi
unset bash bmajor bminor

先判断是否是交互式shell,不是就退出
然后判断shell的版本号

1
2
[root@guan ~]# echo $BASH_VERSION 
4.2.46(2)-release

bmajor是4,bminor是2,判断版本号是4以上才执行逻辑
shopt -q progcomp是判断是否打开了可编程的补全功能,默认是开启的,而且存在可读的/usr/share/bash-completion/bash_completion文件,则source
上面文件里代码接近2000行,只讲解大致流程
设置自定义补全脚本读取的目录

  • 激活扩展的模式匹配和可编程的补全功能
  • 设置一些命令只补全操作对象的关键字,比如groups命令只补全组名、unalias只补全当前别名、unset只补全已有变量……
  • BASH_COMPLETION_COMPAT_DIR 定义了加载bash补全脚本的目录,默认是/etc/bash_completion.d
  • 补全的时候转义一些符号,例如mkdir "a'b"创建了一个名为a'b的目录,你rm的时候希望补全的是a\'b,而不是a'b

此处挖坑,日后有空看再在这继续写/usr/share/bash-completion/bash_completion里函数的使用

quote转换传入$1的单引号为'\'',并在首位加上单引号,确保被shell展开后还是传进来的结果,用于被shell展开后是理想的结果

1
2
3
4
5
$ str=`quote "test'a"`
$ echo $str
'test'\''a'
$ eval echo $str
test'a


开始写补全脚本

bash补全最常见的是三个内置命令

  • compgen
  • complete
  • compopt

点击查看中文文档

看起来选项和选项的值还挺多的,但是做几个例子就能记住常见的选项和值以及各种内置环境变量了
下列是常见的选项

1
2
3
4
5
6
-F function	执行shell 函数,函数中生成COMPREPLY作为候选的补全结果
-C command 将 command 命令的执行结果作为候选的补全 结果
-G pattern 将匹配 pattern的文件名作为候选的补全结果
-W wordlist 分割 wordlist 中的单词,作为候选的补全结果
-p [name] 列出当前所有的补全命令
-r [name] 删除某个补全命令

三个命令中前两个命令选项都一样,compgen用来产生补全匹配的候选单词,complete使用函数来生成补全的候选单词,compopt只能用于compelete生成补全单词的函数内部

内置变量

除了上面三个命令外,Bash还有几个内置变量来辅助补全功能,如下:
COMP_WORDS 类型为数组,存放当前命令行中输入的所有单词.被COMP_WORDBREAKS拆分成单词
COMP_CWORD 类型为整数,当前输入的单词在COMP_WORDS中的索引
COMPREPLY 类型为数组,候选的补全结果就是从这个数组
COMP_WORDBREAKS 类型为字符串,表示单词之间的分隔符
COMP_LINE 类型为字符串,表示当前的命令行输入字符
COMP_POINT 类型为整数,表示光标在当前命令行的哪个位置

注意这里的候选和补全,候选是你按tab按键的输出,但是它不一定是帮你补全,你得让补全去补全候选里的单词

命令和变量的使用

建议下面步骤在没有安装bash-compelete环境下练习,因为部分命令官方也写了补全,可能会和我下面例子冲突

  1. 使用compgen生成匹配的单词候选补全
1
2
3
4
5
6
$ compgen -W 'word1 word2 word3 test' t
test
$ compgen -W 'word1 word2 word3 test' word
word1
word2
word3

从上面的compgen看出来是打印在标准输出的,t和word为你按了tab后传进去的单词,compgen -W是单独生成候选列表(也就是提示你可以补全哪些选项,仅仅是提示)

一般是complete -F指定补全函数里调用compgen去匹配当前单词从而生成符合的补全候选列表输出到终端,complete -F function command,command一般是一个单词,多个单词(命令)都会效有function生成的补全效果

  1. 自定义补全complete
    1
    2
    3
    4
    function _autotab() {
    COMPREPLY=(test1 test2 atc abc bhj cd)
    }
    complete -F _autotab foo

然后输入foo再输入一个空格后按tab即可补全abc atc bhj cd test1 test2作为候选
但是此时还不完全
foo t后按tab无法把t补全成test且还一直输出数组COMPREPLY的值,也就是前面说过候选不是补全,但是得人为的关联起来
应该使用compgen来实现补全候选里的单词

1
2
3
4
5
6
7
8
function _autotab() {
local cur
COMPREPLY=()
cur=${COMP_WORDS[COMP_CWORD]}
COMPREPLY=( $(compgen -W 'test1 test2 atc abc bhj cd' -- ${cur}) )

}
complete -F _autotab foo

此时即可正常补全

  1. 层级命令的补全
    此处拿ip命令的两个二级选项一共三种情况来做demo
    1
    2
    ip route showdump
    ip address {add|show}

linux里很多这种层级的选项命令,我们用户肯定希望每个部分都候选和补全出可能的所有结果,那么前面第二个demo明显不够用,我们得使用更多变量和写更好的函数来实现
先判断是不是第二部分没有补全,也就是COMP_WORDS的长度为1的时候候选补全route adress
长度和你输入完单词后按空格有关系,先输入下面代码

1
2
3
4
5
6
function _autotab() {
local cur
COMPREPLY=()
COMPREPLY=(${#COMP_WORDS[@]})
}
complete -F _autotab ip

输入下面步骤观察现象

1
2
3
ip <tab>
ip a<tab>
ip a <tab>

上面三个命令输入完成后无论第二个部分输入了否,长度都是2

1
2
3
4
5
6
7
8
9
10
11
12
function _autotab() {
local cur
COMPREPLY=()
cur=${COMP_WORDS[COMP_CWORD]}
if [ "${#COMP_WORDS[@]}" -eq 2 ];then
COMPREPLY=( $(compgen -W 'route address' -- ${cur}) )
elif [ "${COMP_WORDS[1]}" == address ];then
COMPREPLY=( $(compgen -W 'add show' -- ${cur}) )
fi

}
complete -F _autotab ip

官方函数的使用

安装了bash-completion后/usr/share/bash-completion/bash_completion里有很多定义好的函数使用起来更加事半功倍,最常见的一个就是下面的这部分了

1
2
3
4
5
6
7
local cur prev words cword
if declare -F _init_completion &>/dev/null; then
_init_completion -s
else
COMPREPLY=()
_get_comp_words_by_ref -n '=' cur prev words cword
fi

其中_get_comp_words_by_ref是比${COMP_WORDS[$COMP_CWORD]}更好,因为它能更好处理用户在单词中间完成的位置

第一个是光标当前单词,第二个是光标前一个单词,第三个是所有单词的数组,第四个是单词的个数
例如前面的小mode还可以改成下面

1
2
3
4
5
6
7
8
9
10
11
12
function _autotab() {
local cur prev words cword
COMPREPLY=()
_get_comp_words_by_ref -n '=' cur prev words cword
if [ "${#words[@]}" -eq 2 ];then
COMPREPLY=( $(compgen -W 'route address' -- ${cur}) )
elif [ "${words[1]}" == address ];then
COMPREPLY=( $(compgen -W 'add show' -- ${cur}) )
fi

}
complete -F _autotab ip

补全命令的长选项,下面是grep举例,grep --<tab>来补全

1
2
3
4
5
6
7
8
9
function t2(){
local cur prev words cword
COMPREPLY=()
_get_comp_words_by_ref -n '=' cur prev words cword
COMPREPLY=( $( compgen -W "$( $1 --help 2>&1 | \
sed -ne 's/.*\(--[-A-Za-z0-9]\{1,\}=\{0,1\}\).*/\1/p' | sort -u )" \
-- "$cur" ) )
}
complete -F t2 grep

上面这个demo摘自官方的_longopt函数,完美的补全离不开逻辑紧密的shell函数

CATALOG
  1. 1. bash补全相关脚本和加载流程
  2. 2. 此处挖坑,日后有空看再在这继续写/usr/share/bash-completion/bash_completion里函数的使用
  3. 3. 开始写补全脚本
    1. 3.1. 内置变量
    2. 3.2. 命令和变量的使用
  4. 4. 官方函数的使用