0%

bash补全函数

编写bash补全函数的关键就是根据上下文生成合适的COMPREPLY数组。这里记录了bash补全的相关资料,以及常用的代码片段。

关于补全,建议先通读一遍Programmable Completion,可以了解当按下<tab>按键时,bash是如何处理的。这里专注于补全函数,相关描述如下。

After these matches have been generated, any shell function or command specified with the -F and -C options is invoked. When the command or function is invoked, the COMP_LINE, COMP_POINT, COMP_KEY, and COMP_TYPE variables are assigned values as described above (see Bash Variables). If a shell function is being invoked, the COMP_WORDS and COMP_CWORD variables are also set. When the function or command is invoked, the first argument ($1) is the name of the command whose arguments are being completed, the second argument ($2) is the word being completed, and the third argument ($3) is the word preceding the word being completed on the current command line. No filtering of the generated completions against the word being completed is performed; the function or command has complete freedom in generating the matches.

Any function specified with -F is invoked first. The function may use any of the shell facilities, including the compgen and compopt builtins described below (see Programmable Completion Builtins), to generate the matches. It must put the possible completions in the COMPREPLY array variable, one per array element.

注册补全函数

使用bash内置命令complete可以为指定shell命令注册补全函数。如下代码表示shell命令foo的补全函数是_foo。在shell命令中,依次敲入foo空格<tab>时,补全函数_foo将在当前shell运行。

1
complete -F _foo foo

可以使用命令complete -p func来查询指定命令的补全函数。例如cd命令的补全函数是_cd

1
2
pk@pkdev22:~$ complete -p cd
complete -o nospace -F _cd cd

上下文

阅读Programmable Completion可以得知,当调用补全函数时,bash会准备好COMP_LINECOMP_POINTCOMP_KEYCOMP_TYPECOMP_WORDSCOMP_CWORD变量。bash手册关于这些变量的描述如下。

变量 说明
COMP_LINE The current command line. This variable is available only in shell functions and external commands invoked by the programmable completion facilities (see Programmable Completion).
COMP_POINT The index of the current cursor position relative to the beginning of the current command. If the current cursor position is at the end of the current command, the value of this variable is equal to ${#COMP_LINE}. This variable is available only in shell functions and external commands invoked by the programmable completion facilities (see Programmable Completion).
COMP_KEY The key (or final key of a key sequence) used to invoke the current completion function.
COMP_TYPE Set to an integer value corresponding to the type of completion attempted that caused a completion function to be called: TAB, for normal completion, ‘?’, for listing completions after successive tabs, ‘!’, for listing alternatives on partial word completion, ‘@’, to list completions if the word is not unmodified, or ‘%’, for menu completion. This variable is available only in shell functions and external commands invoked by the programmable completion facilities (see Programmable Completion).
COMP_WORDS An array variable consisting of the individual words in the current command line. The line is split into words as Readline would split it, using COMP_WORDBREAKS as described above. This variable is available only in shell functions invoked by the programmable completion facilities (see Programmable Completion).
COMP_CWORD An index into ${COMP_WORDS} of the word containing the current cursor position. This variable is available only in shell functions invoked by the programmable completion facilities (see Programmable Completion).

直接看描述,有点迷迷糊糊,上例子。COMP_LINE是一个字符串,表示整个命令行,如果末尾有空格,则包含空格。COMP_POINTCOMP_LINE的长度。COMP_KEY不知道是干啥的。COMP_TYPE在第一次敲<tab>时,是9,即<tab>的ASCII值,第二次敲<tab>时,是63,即?的ASCII值。COMP_WORDSCOMP_LINE很类似,但COMP_WORDS是一个数组。COMP_CWORD是数组COMP_WORDS的下标,

变量 foo a b<tab> foo a b<space><tab>
COMP_LINE foo a b foo a b<space>
COMP_POINT 7 8
COMP_KEY 9 9
COMP_TYPE 9 或 63 9 或 63
COMP_WORDS foo a b foo a b <space>
COMP_CWORD 2 3

根据bash提供的这些变量,可以推算出两个关键的信息,prevcurprev表示上一个单词,cur表示当前的单词。绝大多数情况下,可以根据这两个信息计算出补全列表。

1
2
3
4
_foo() {
local prev="${COMP_WORDS[COMP_CWORD-1]}"
local cur="${COMP_WORDS[COMP_CWORD]}"
}

bash提供了辅助函数_init_completion用来初始化变量prevcurwordscword。其中words等同于COMP_WORDScword等同于COMP_CWORD

1
2
3
4
_foo() {
_init_completion || return
# 接下来的代码可以使用 prev / cur / words / cword 变量。
}

返回补全结果

补全函数需要填充COMPREPLY数组,向bash返回补全列表。根据日常使用补全的经验,有两种情况。第一种情况,当前的选项输入到一半了。第二种情况,上一个选项输入完成了,开始输入下一个选项。

对于第一种情况,只需要根据当前的输入${cur}做一个筛选,将匹配的结果填充到COMPREPLY数组即可。例如foo命令有三个选项--foo1--foo2--bar。当输入foo --foo<tab>时,期望bash会提示--foo1--foo2。bash的内置命令compgen可以帮助做筛选。补全函数的参考实现如下。

1
2
3
4
5
_foo() {
_init_completion || return
COMPREPLY=( $(compgen -W '--foo1 --foo2 --bar' -- ${cur}) )
return 0
}

第二种情况比较复杂,需要根据之前的输入,向COMPREPLY数组填充不同的值。例如foo命令的所有选项只允许出现一次,当输入foo --foo1 <tab>时,期望只会提示--foo2--bar。参考实现如下。首先遍历选项列表,如果不在${words[*],则追加到变量comp_opts。最后根据${cur}做筛选。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_foo() {
_init_completion || return
COMPREPLY=()

local opts=("--foo1" "--foo2" "--bar")
local comp_opts
for (( i=0; i < ${#opts[@]}; i++)); do
# 遍历所有选项,如果不在${words[*]},则追加到comp_opts。
if [[ "${words[*]}" != *" ${opts[i]} "* ]]; then
comp_opts+="${opts[i]} "
fi
done
COMPREPLY=( $(compgen -W "${comp_opts}" -- ${cur}) )
}

bash比较子字符串时,子字符串必须放在后面。

实际情况往往更加的复杂,比如很多shell命令还有子命令,这些子命令有不同的选项。但思路都是类似的,根据上下文生成合适的COMPREPLY数组。

compgen

除了用-W指定补全的数据外,compgen还提供了生成常用数据的选项。比如compgen -ucompgen -A user会输出系统的所有用户,编写chmod的补全函数就会用到。

-A action 短选项 说明
-A alias -a 别名
-A arrayvar 数组变量名
-A binding
-A builtin -b shell内置命令
-A command -c 命令
-A directory [dir] -d 当前路径或指定路径的子目录
-A disabled 禁用的shell内置命令
-A enabled 使能的shell内置命令
-A export -e 导出的shell变量
-A file [dir/] -f 当前路径或指定路径的文件。如果加了可选参数,一定用/结尾
-A function shell函数
-A group -g 组名
-A helptopic 内置命令help接受的主题
-A hostname 主机名
-A job -j 任务名
-A keyword -k shell关键字
-A running 正在运行的任务名
-A service -s 服务名
-A setopt set内置命令-o选项的有效参数
-A shopt shopt内置命令接受的选项名
-A signal 信号名
-A stopped 停止的任务
-A user -u 用户名
-A variable -v 变量名

调试技巧

在调试补全函数的过程中,经常需要打印某些变量的值。如果直接使用echo输出,会和补全提示混在一起,非常不好看。建议重定向到某一个文件。

示例

bash-completion

软件包bash-completion实现了非常多命令的补全,我们可以阅读这些脚本,学习是如何实现的。在Ubuntu系统,这些脚本在/usr/share/bash-completion/目录下面。

cdwork

假如有一个工作目录,期望在任意目录敲cdwork命令,则跳转到工作目录。如果执行cdwork subdir1,则跳转到工作目录下的subdir1子目录。支持任意一级子目录补全,即输入cdwork <tab>时,补全工作目录下的所有子目录。以下实现是参考_cd函数改的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export WORKDIR="/path/to/work/dir"
function cdwork() {
cd "${WORKDIR}/$1"
}

_cdwork()
{
local cur prev words cword;
_init_completion || return;
local IFS='
' i j k;
compopt -o filenames;
local -r mark_dirs=$(_rl_enabled mark-directories && echo y);
local -r mark_symdirs=$(_rl_enabled mark-symlinked-directories && echo y);

k="${#COMPREPLY[@]}";
for j in $(compgen -d -- $WORKDIR/$cur);
do
if [[ ( -n $mark_symdirs && -L $j || -n $mark_dirs && ! -L $j ) && ! -d ${j#$WORKDIR/} ]]; then
j+="/";
fi;
COMPREPLY[k++]=${j#$WORKDIR/};
done;

_filedir -d;
if ((${#COMPREPLY[@]} == 1)); then
i=${COMPREPLY[0]};
if [[ $i == "$cur" && $i != "*/" ]]; then
COMPREPLY[0]="${i}/";
fi;
fi;
return
}

complete -o nospace -F _cdwork cdwork