|
1. 引言
当我们输入ls 再按下TAB时, 会自动列出当前路径下所有的文件;
当我们输入ls a再按下TAB时, 会自动列出当前路径下所有以a开头的文件; 若只有一个以a开头的文件, 将会自动补全;
当我们输入type 再按下TAB时, 会自动列出全所有可执行的命令;
当我们输入docker rmi 再按下TAB时, 会自动列出所有镜像名;
一个显示文件, 一个显示命令, 一个显示容器名, 这是怎么做到的?
本文将带你一探究竟, 并以docker为例, 实现一个简单的docker自动补全规则
2. complete命令
上述功能, 是 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命令会列出命令
3. 自定义补全列表
尽管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 所有显示的镜像名, 会随着镜像的增删而改变; docker rm 所有显示的容器名, 会随着容器的增删而改变; 是动态的补全规则, 这是如何做到的呢?
我们直接通过-p选项来查看docker预设的补全规则就好了, 发现docker命令是通过-F _docker来指定补全规则; 再通过type _docker来查看_docker是什么玩意, 发现_docker是一个非常复杂的函数- $ complete -p docker
- complete -F _docker docker
- $ type _docker
- _docker is a function
- _docker ()
- {
- ......
- }
复制代码 接下来, 我们来好好聊一聊-F这个选项
4. 动态补全列表
-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时, 最后一个单词是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需要我们手动处理才行
5. compgen命令
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>
复制代码 6. 别名的自动补全
笔者用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
复制代码 7. 补全规则永久生效
上述例子中, 我们执行补全规则脚本, 使用的是. 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都会执行初始化文件, 所以就可以达到永久生效的效果.
8. 自动加载
最后提一下自动补全脚本是如何自动加载的. 入口是 /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, 这里就不展开讲了.
9. 参考
来源:https://www.cnblogs.com/wbourne/p/17447471.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作! |
|