當我們輸入ls
再按下TAB時, 會自動列出當前路徑下所有的檔案;
當我們輸入ls a
再按下TAB時, 會自動列出當前路徑下所有以a開頭的檔案; 若只有一個以a開頭的檔案, 將會自動補全;
當我們輸入type
再按下TAB時, 會自動列出全所有可執行的命令;
當我們輸入docker rmi
再按下TAB時, 會自動列出所有映象名;
一個顯示檔案, 一個顯示命令, 一個顯示容器名, 這是怎麼做到的?
本文將帶你一探究竟, 並以docker為例, 實現一個簡單的docker自動補全規則
上述功能, 是 Bash 2.05 版本新增的功能, 叫做自動補全. 自動補全允許我們對命令和選項設定補全規則, 按下TAB之後, 會根據我們設定的規則返回補全列表, 當補全列表只有一個元素時, 就會自動補全.
bash自動補全用到最主要的命令就是complete
, 這是一個Bash的內建命令(builtin), 用於指定某個命令的補全規則, complete語法如下:
complete [-abcdefgjksuv] [-o comp-option] [-DEI] [-A action] [-G globpat] [-W wordlist] [-F function] [-C command] [-X filterpat] [-P prefix] [-S suffix] [name …]
complete -pr [-DEI] [name …]
選項:
-o comp-option
定義一些補全的行為, 可以使用的行為如下:
nospace 補全後不在最後新增空格
nosort 對於補全列表不要按字母排序
-A action
使用預設的補全規則, 可使用的補全規則如下:
alias 補全列表設定為所有已定義的別名. 等同於-a
builtin 補全列表設定為所有shell內建命令. 等同於-b
command 補全列表設定為所有可執行命令. 等同於-c
directory 補全列表設定為當前路徑下所有目錄. 等同於-d,
也就是說 complete -d xxx 與 complete -A directory xxx 等價, 只是寫法不一樣
export 補全列表設定為所有環境變數名. 等同於-e
file 補全列表設定為當前路徑下所有檔案. 等同於-f
function 補全列表設定為所有函數名
signal 補全列表設定為所有訊號名
user 補全列表設定為所有使用者名稱. 等同於-u
variable 補全列表設定為所有變數名. 等同於-v
-F function
用函數來定義補全規則, 函數執行後 COMPREPLY 變數做為補全列表
-W wordlist
用一個字串來做為補全列表
-p name
顯示某個命令的補全規則, 如果 name 為空的話則顯示所有命令的補全規則
-r
移除某個命令的補全規則
ls
命令預設的補全列表是當前路徑下所有檔案, 現在, 我們改變其補全規則, 讓其補全列表變為所有可執行命令
$ cd /
# 先測試下 ls 預設的補全規則
$ ls<TAB>
bin/ boot/ dev/ etc/ home/ lib/ lib32/ lib64/ libx32/ media/ mnt/ opt/ proc/ root/ run/ sbin/ srv/ sys/ tmp/ usr/ var/
# 修改 ls 的補全規則, 讓所有可執行命令作為其補全列表
$ complete -c ls
# 測試修改補全規則後的 ls
$ ls who<TAB>
who whoami whoopsie whoopsie-preferences
提示: 上述改變的補全規則只在當前shell有效, 即不會影響到其他使用者, 重新登入後也會失效. 所以想要恢復ls
命令的補全規則的話, 只需要退出再重新登入伺服器就好了. 至於如何永久改變補全規則, 請看後文.
我們再來看下type
命令預設的補全規則, 發現type
命令設定的補全列表是所有可執行命令
$ complete -p type
complete -c type
至此, 我們應該知道引言中所提出的問題, 為什麼ls
命令會檔案而type
命令會列出命令
儘管Bash預設了很多補全規則, 但是很明顯, 如果我們自己想給docker
命令寫補全規則的話, 預設的補全規則顯然是不能滿足我們需求的. 所以, 我們可以用-W
選項來自定義補全列表.
假設我們自己寫了個mydocker
命令, 可以使用的功能有mydocker rm
, mydocker rmi
, mydocker stop
, mydocker start
, 顯然, mydocker
的補全列表為rm rmi stop start
, 我們可以使用下面的命令來設定補全規則
# 將 rm rmi stop start 設定為 mydocker 的補全列表
$ complete -W 'rm rmi stop start' mydocker
$ mydocker<TAB>
rm rmi start stop
$ mydocker st<TAB>
start stop
到這一步, 我們已經能給相當一部分的命令來定義補全規則了. 但是, 上述的'-W'選項, 是靜態的補全規則, 不會隨著某些條件的改變而變化; docker rmi <TAB>
所有顯示的映象名, 會隨著映象的增刪而改變; docker rm <TAB>
所有顯示的容器名, 會隨著容器的增刪而改變; 是動態的補全規則, 這是如何做到的呢?
我們直接通過-p
選項來檢視docker預設的補全規則就好了, 發現docker命令是通過-F _docker
來指定補全規則; 再通過type _docker
來檢視_docker
是什麼玩意, 發現_docker
是一個非常複雜的函數
$ complete -p docker
complete -F _docker docker
$ type _docker
_docker is a function
_docker ()
{
......
}
接下來, 我們來好好聊一聊-F
這個選項
-F
選項會指定一個函數做為補全規則, 每次按下TAB時, 就會呼叫這個函數, 並且將COMPREPLY
的值做為補全列表, 所以我們需要在函數中處理COMPREPLY
變數
除了COMPREPLY
變數外, Bash還提供了一些變數來方便我們獲取當前的輸入
變數名 | 型別 | 說明 |
---|---|---|
COMP_LINE | 字串 | 當前的命令列輸入的所有內容 |
COMP_WORDS | 陣列 | 當前的命令列輸入的所有內容, 和COMP_LINE 不同的是, 這個變數是一個陣列 |
COMP_CWORD | 整數 | 當前的命令列輸入的內容位於COMP_WORDS 陣列中的索引 |
COMPREPLY | 陣列 | 補全列表 |
接下來我們編寫一個補全指令碼來測試這些變數, 指令碼名字可以隨便取, 暫且叫做 test.sh, 檔案內容如下:
_complete_test() {
echo
echo "COMP_LINE: $COMP_LINE" # 當前的命令列輸入的所有內容(字串)
echo "COMP_WORDS: ${COMP_WORDS[@]}" # 當前的命令列輸入的所有內容(陣列)
echo "COMP_CWORD: $COMP_CWORD" # 陣列的索引
echo "last_word: ${COMP_WORDS[COMP_CWORD]}" # 最後一個輸入的單詞
echo "COMPREPLY: $COMPREPLY" # 補全列表
}
complete -F _complete_test mydocker
我們通過執行source test.sh
來使指令碼生效, 然後來測試指令碼
$ source test.sh
$ mydocker <TAB>
COMP_LINE: mydocker # 當前的命令列輸入的所有內容(字串)
COMP_WORDS: mydocker # 當前的命令列輸入的所有內容(陣列)
COMP_CWORD: 1 # 陣列的索引
last_word: # 最後一個輸入的單詞
COMPREPLY: # 補全列表
$ mydocker xy<TAB>
COMP_LINE: mydocker xy # 當前的命令列輸入的所有內容(字串)
COMP_WORDS: mydocker xy # 當前的命令列輸入的所有內容(陣列)
COMP_CWORD: 1 # 陣列的索引
last_word: xy # 最後一個輸入的單詞
COMPREPLY: # 補全列表
我們理解了上述的變數之後, 我們是不是可以這樣做: 獲取當前輸入的內容, 如果是mydocker
的話, 將補全列表設定為rm rmi stop start
; 如果是mydocker rm
的話, 查詢出所有的容器名, 並將補全列表設定為所有的容器名, start
和stop
同理; 如果是mydocker rmi
的話, 補全列表設定為所有的映象名. 因為每次自動補全都會執行我們的函數, 所以我們的補全列表就是動態的了
在修改test.sh指令碼之前, 我們造一點測試資料, 拉取兩個映象並執行這兩個映象
$ docker pull redis
$ docker pull redmine
接下來將test.sh指令碼修改為如下內容:
_complete_mydocker() {
local prev
prev="${COMP_WORDS[COMP_CWORD-1]}"
case "${prev}" in
rm) COMPREPLY=( $(docker ps -a | tail -n +2 | awk '{print $NF}') ) ;;
rmi) COMPREPLY=( $(docker images | tail -n +2 | awk '{print $1}') );;
mydocker) COMPREPLY=( rm rmi stop start ) ;;
esac
}
complete -F _complete_mydocker mydocker
注意: case語句中判斷的是倒數第二個輸入的單詞, 因為當我們執行mydocker r<TAB>
時, 最後一個單詞是r
, 倒數第二個單詞是mydocker
, 顯然此時我們需要的是mydocker
的補全列表
修改完指令碼後, 要再次執行source test.sh
才能使指令碼生效. 然後來測試指令碼
$ mydocker <TAB>
rm rmi start stop
# 貌似有點問題?
$ mydocker rm<TAB>
rm rmi start stop
$ mydocker rmi <TAB>
redis redmine
# 貌似又有問題?
$ mydocker rmi redi<TAB>
redis redmine
目前的補全指令碼還是存在一些問題, 其實也很容易發現問題, 無論我們輸入mydocker rmi re
還是mydocker rmi redi
, 都會匹配到補全指令碼中的rmi) COMPREPLY=( $(docker images | tail -n +2 | awk '{print $1}') );;
, 我們返回的補全列表COMPLETE
都是同樣的結果, 補全列表並沒有變, 補全列表返回的都是redis redmine
. 然而, 我們想要的是, 輸入mydocker rmi re
返回redis redmine
, 輸入mydocker rmi redi
返回redis
, 這就需要compgen命令出場了
Tips: 可能有些讀者會有疑問, 為什麼設定同樣的候選列表, 使用-W
就和預期一樣而使用-F
就會出現上述問題, 因為-W
已經幫我們實現了類似compgen的功能, 而-F
需要我們手動處理才行
compgen也是一個Bash內建命令, 其選項幾乎和complete是通用的, 其作用就是篩選, 看幾個例子大家就明白怎麼用了
# -W指定補全列表, 並返回與st相匹配的值
$ compgen -W 'rm rmi start stop' -- st
start
stop
# -W指定補全列表, 並返回與sto相匹配的值
$ compgen -W 'rm rmi start stop' -- sto
stop
# -b指定補全列表為Bash內建命令, 並返回與c相匹配的值
$ compgen -b -- c
caller
cd
command
compgen
complete
compopt
continue
學會了compgen命令, 我們再來修改指令碼, 將COMPREPLY=( rm rmi stop start )
修改為COMPREPLY=( $(compgen -W "rm rmi stop start" -- 最後一個單詞) )
就可以動態修改補全列表了
最後將指令碼修改如下:
_complete_mydocker() {
local cur prev mydocker_opts images contains
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
mydocker_opts="rm rmi stop start"
images=$(docker images | tail -n +2 | awk '{print $1}')
contains=$(docker ps -a | tail -n +2 | awk '{print $NF}')
case "${prev}" in
rm) COMPREPLY=( $(compgen -W "${contains}" -- ${cur}) ) ;;
rmi) COMPREPLY=( $(compgen -W "${images}" -- ${cur}) );;
mydocker) COMPREPLY=( $(compgen -W "${mydocker_opts}" -- ${cur}) ) ;;
esac
}
complete -F _complete_mydocker mydocker
執行指令碼後再次測試指令碼, 已經能達到我們想要的效果了
$ mydocker <TAB>
rm rmi start stop
$ mydocker rm<TAB>
rm rmi
$ mydocker rmi <TAB>
redis redmine
$ mydocker rmi re<TAB>
redis redmine
# 這裡就會自動補全了
$ mydocker rmi redi<TAB>
筆者用docker相關的命令用的比較多, 不想每次敲這麼長, 所以直接執行alias d=docker
把d
設定為docker
的別名, 設定後方是方便了很多, 但是用不了自動補全
沒關係, 既然docker有自動補全, 那麼d也必須有自動補全. 通過執行complete
命令發現, docker的補全規則是_docker
函數提供的
$ complete -p docker
complete -F _docker docker
那我們只需要執行complete -F _docker d
, 將d
的補全規則設定為_docker
, 這樣d
也可使用自動補全了
$ d <TAB>
build cp events help images inspect login network plugin pull restart run secret start swarm top version
commit create exec history import kill logout node port push rm save service stats system unpause volume
container diff export image info load logs pause ps rename rmi search stack stop tag update wait
上述例子中, 我們執行補全規則指令碼, 使用的是. completion_script
或者source completion_script
的形式來執行, 而不是通過./completion_script
或bash completion_script
的形式來執行, 是因為: 前者的作用範圍是當前shell; 而後者會在子shell中執行, 不會影響到當前shell, 看起來就和沒執行一樣. 子shell是另外一個很重要的概念, 感興趣的讀者可自行了解.
由於source completion_script
的作用範圍是當前shell, 所以我們設定的補全規則不會影響到其他使用者, 同時也會在重新登入後失效. 要使補全規則永久生效, 我們將source completion_script
本新增到 ~/.bashrc 或者 ~/.profile 檔案中即可. 因為這兩個檔案是Bash的初始化檔案, 每次登入Bash都會執行初始化檔案, 所以就可以達到永久生效的效果.
最後提一下自動補全指令碼是如何自動載入的. 入口是 /etc/bash.bashrc 這個檔案, 其會呼叫 /usr/share/bash-completion/bash_completion 或 /etc/bash_completion
$ cat /etc/bash.bashrc
......
......
if ! shopt -oq posix; then
if [ -f /usr/share/bash-completion/bash_completion ]; then
. /usr/share/bash-completion/bash_completion
elif [ -f /etc/bash_completion ]; then
. /etc/bash_completion
fi
fi
檢視 /etc/bash_completion 得知, 無論呼叫哪個檔案, 最後實際上呼叫的都是 /usr/share/bash-completion/bash_completion
$ cat /etc/bash_completion
. /usr/share/bash-completion/bash_completion
開啟 /usr/share/bash-completion/bash_completion 檔案, 在2151行左右, 有以下一段程式碼, 大概意思就是會執行 /etc/bash_completion.d 中的每個檔案, 所以, 我們將自動補全指令碼放在這個路徑下, 並設定好讀許可權, 每次登入系統就會自動載入, 也可以達到永久生效的效果.
$ cat /usr/share/bash-completion/bash_completion
......
......
compat_dir=${BASH_COMPLETION_COMPAT_DIR:-/etc/bash_completion.d}
if [[ -d $compat_dir && -r $compat_dir && -x $compat_dir ]]; then
for i in "$compat_dir"/*; do
[[ ${i##*/} != @($_backup_glob|Makefile*|$_blacklist_glob) \
&& -f $i && -r $i ]] && . "$i"
done
fi
實際上, Ubuntu中一般的自動補全指令碼一般放在 /usr/share/bash-completion/completions/, 也會自動載入, 入口是 /etc/bash_completion.d 的2132行左右寫道了complete -D -F _completion_loader
, 這裡就不展開講了.