Shell脚本中实现⾃动补全功能
对于er来说,⾃动补全是再熟悉不过的⼀个功能了。当你在命令⾏敲下部分的命令时,肯定会本能地按下Tab键补全完整的命令,当然除了命令补全之外,还有⽂件名补全。
Bash-completion
⾃动补全这个功能是Bash⾃带的,但⼀般我们会安ash-completion包来得到更好的补全效果,这个包提供了⼀些现成的命令补全脚本,⼀些基础的函数⽅便编写补全脚本,还有⼀个基本的配置脚本。但也正如之前说的,这个包不是必须的,只不过可以省些⼒⽓。
bash-completion这个包的安装位置因不同的发⾏版会有所区别,但是⼤致上启⽤的原理是类似的,⼀般会有⼀个名为bash_completion的脚本,这个脚本会在shell初始化时加载。例如对于RHEL系统来说,这个脚本位于/etc/bash_completion,⽽该脚本会由/etc/profile.d/bash_completion.sh中导⼊:
1 # Check for interactive bash and that we haven't already been sourced.
2 [ -z "$BASH_VERSION" -o -z "$PS1" -o -n "$BASH_COMPLETION" ] && return
3
4 # Check for recent enough version of bash.
5 bash=${BASH_VERSION%.*}; bmajor=${bash%.*}; bminor=${bash#*.}
6if [ $bmajor -gt 3 ] || [ $bmajor -eq 3 -a $bminor -ge 2 ]; then
7if shopt -q progcomp && [ -r /etc/bash_completion ]; then
8        # Source completion code.
9        . /etc/bash_completion
10fi
11fi
12 unset bash bmajor bminor
⽽在bash_completion脚本中会加载/etc/bash_completion.d下⾯的补全脚本:
if [[ $BASH_COMPLETION_DIR != $BASH_COMPLETION_COMPAT_DIR && \
-
d $BASH_COMPLETION_DIR && -r $BASH_COMPLETION_DIR && \
-x $BASH_COMPLETION_DIR ]]; then
for i in $(LC_ALL=C command ls"$BASH_COMPLETION_DIR"); do
i=$BASH_COMPLETION_DIR/$i
[[ ${i##*/} != @(*~|*.bak|*.swp|\#*\#|*.dpkg*|*.rpm@(orig|new|save)|Makefile*) \
&& -f $i && -r $i ]] && . "$i"
done
fi
unset i
补全脚本的名称⼀般就是命令名,这样⽐较容易查:
$ ls i*
iconv  iftop  ifupdown  info  iproute2  iptables
内置补全命令
Bash内置有两个补全命令,分别是compgen和complete。compgen命令根据不同的参数,⽣成匹配单词的候选补全列表,例如:
$ compgen -W 'hi hello how world' h
hi
hello
how
compgen最常⽤的选项是-W,通过-W参数指定空格分隔的单词列表。h即我们在命令⾏当前键⼊的单词,执⾏完后会输出候选的匹配列表,这⾥是以h开头的所有单词。
complete命令的参数有点类似compgen,不过它的作⽤是说明命令如何进⾏补全,例如同样使⽤-W参数指定候选的单词列表:
$ complete -W 'word1 word2 word3 hello' foo
$ foo w<Tab>
$ foo word<Tab>
word1  word2  word3
我们还可以通过-F参数指定⼀个补全函数:
$ complete -F _foo foo
现在键⼊foo命令后,会调⽤_foo函数来⽣成补全的列表,完成补全的功能,这⼀点正是补全脚本实现的关键所在,我们会在后⾯介绍。
补全相关的内置变量
除了上⾯的两个补全命令外,Bash还有⼏个内置的变量⽤来辅助补全功能,这⾥主要介绍其中三个:
COMP_WORDS: 类型为数组,存放当前命令⾏中输⼊的所有单词;
COMP_CWORD: 类型为整数,当前光标下输⼊的单词位于COMP_WORDS数组中的索引;
COMPREPLY: 类型为数组,候选的补全结果;
shell最简单脚本COMP_WORDBREAKS: 类型为字符串,表⽰单词之间的分隔符;
COMP_LINE: 类型为字符串,表⽰当前的命令⾏输⼊;
例如我们定义这样⼀个补全函数_foo:
$ function _foo()
{
echo -e "\n"
declare -p COMP_WORDS
declare -p COMP_CWORD
declare -p COMP_LINE
declare -p COMP_WORDBREAKS
}
$ complete -F _foo foo
假设我们在命令⾏下输⼊以下内容,再按下Tab键补全:
$ foo b
declare -a COMP_WORDS='([0]="foo" [1]="b")'
declare -- COMP_CWORD="1"
declare -- COMP_LINE="foo b"
declare -- COMP_WORDBREAKS="
对着上⾯的结果,我想应该⽐较容易理解这⼏个变量。当然正如我们之前据说,Bash-completion包并⾮是必须的,补全功能是Bash⾃带的。
编写脚本
补全脚本分成两个部分:编写⼀个补全函数和使⽤complete命令应⽤补全函数。后者的难度⼏乎忽略不计,重点在如何写好补全函数。难点在,似乎⽹上很少与此相关的⽂档,但是事实上,Bash-completion⾃带的补全脚本是最好的起点,可以挑⼏个简单的改改基本上就可以使⽤了。
⼀般补全函数(假设这⾥依然为_foo)都会定义以下两个变量:
local cur prev
其中cur表⽰当前光标下的单词,⽽prev则对应上⼀个单词:
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
初始化相应的变量后,我们需要定义补全⾏为,即输⼊什么的情况下补全什么内容,例如当输⼊-开头的选项的时候,我们将所有的选项作为候选的补全结果:
local opts="-h --help -f --file -o --output"
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
不过再给COMPREPLY赋值之前,最好将它重置清空,避免被其它补全函数⼲扰。
现在完整的补全函数是这样的:
function _foo() {
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="-h --help -f --file -o --output"
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
}
现在在命令⾏下就可以对foo命令进⾏参数补全了:
$ complete -F _foo foo
$ foo -
-f        --file    -h        --help    -o        --output
当然,似乎我们这⾥的例⼦没有⽤到prev变量。⽤好prev变量可以让补全的结果更加完整,例如当输⼊--file之后,我们希望补全特殊的⽂件(假设以.sh结尾的⽂件):
case"${prev}"in
-f|--file)
COMPREPLY=( $(compgen -o filenames -W "`ls *.sh`" -- ${cur}) )
;;
esac
现在再执⾏foo命令,--file参数的值也可以补全了:
$ foo --file<Tab>
a.sh
b.sh
c.sh
安装补全脚本
如果安装了Bash-completion包,可以将补全脚本放在/etc/bash_completion.d⽬录下,或者放到~/.bas
h_completion⽂件中。如果没有安装Bash-completion包,可以把补全脚本放到~/.bashrc或者其它能被shell加载的初始化⽂件中。