對於Linuxer來說,自動補全是再熟悉不過的一個功能了。當你在命令行敲下部分的命令時,肯定會本能地按下Tab鍵補全完整的命令,當然除了命令補全之外,還有文件名補全。
Bash-completion
自動補全這個功能是Bash自帶的,但一般我們會安裝bash-completion包來得到更好的補全效果,這個包提供了一些現成的命令補全腳本,一些基礎的函數方便編寫補全腳本,還有一個基本的配置腳本。但也正如之前說的,這個包不是必須的,只不過可以省些力氣。
bash-completion這個包的安裝位置因不同的發行版會有所區別,但是大致上啟用的原理是類似的,一般會有一個名為bash_completion的腳本,這個腳本會在shell初始化時加載。例如對於RHEL系統來說,這個腳本位於/etc/bash_completion,而該腳本會由/etc/profile.d/bash_completion.sh中導入:
# Check for interactive bash and that we haven't already been sourced. [ -z "$BASH_VERSION" -o -z "$PS1" -o -n "$BASH_COMPLETION" ] && return # Check for recent enough version of bash. bash=${BASH_VERSION%.*}; bmajor=${bash%.*}; bminor=${bash#*.} if [ $bmajor -gt 3 ] || [ $bmajor -eq 3 -a $bminor -ge 2 ]; then if shopt -q progcomp && [ -r /etc/bash_completion ]; then # Source completion code. . /etc/bash_completion fi fi 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
: 類型為數組,候選的補全結果;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