Shell命令tab自动补全的实现
一.Shell命令自动补全的实现
1.ip 命令的自动补全是如何实现的?
1.ip命令相关补全文件
/usr/share/bash-completion/completions/ip
2.加载流程
- 用户进入交互式bash(终端)
~/.bashrc中加载/usr/share/bash-completion/bash_completion/usr/share/bash-completion/bash_completion中定义了_completion_loader函数- 当对一个命令按
TAB时,如果这个命令没有补全定义,会触发_completion_loader函数,然后从/usr/share/bash-completion/completions/<command>查找对应的补全脚本并加载。 - 脚本只加载一次,一旦某个命令的补全脚本被加载,它就在当前 Shell 会话里生效,不会每次 TAB 都重新加载。
3.效果展示
$ ip <tab><tab>
address ioam monitor neighbour ntbl tcpmetrics xfrm
addrlabel l2tp mptcp netconf route token
fou link mroute netns rule tunnel
help macsec mrule nexthop sr tuntap
ila maddress neighbor ntable tap vrf
$ ip -<tab><tab>
-Version -details -force -rcvbuf -statistics
-batch -family -oneline -resolve -timestamp注意:
如果tab无法正常使用,查看是否安装bash-completion包
二.可编程完成内置函数 Bash官网
有三个内置命令可用于操作可编程完成功能:一个用于指定如何完成特定命令的参数,两个用于在完成时修改完成。
1.compgen
compgen [option] [word]根据选项生成单词的可能的完成匹配,该选项可以是完整内置函数complete的任何选项(-p, -r, -D, -E, 和 -I 除外),并将匹配写入标准输出。
使用 -F 或 -C 选项时,可编程完成工具设置的各种 shell 变量虽然可用,但不会具有有用的值。
匹配的生成方式与可编程完成代码直接从具有相同标志的完成规范生成匹配的方式相同。如果指定了单词word,则仅显示与单词word匹配的补全。
除非提供了无效选项,或者未生成任何匹配项,否则返回值为 true。
帮助
$ compgen --help
compgen: compgen [-abcdefgjksuv] [-o option] [-A action] [-G globpat] [-W wordlist] [-F function] [-C command] [-X filterpat] [-P prefix] [-S suffix] [word]
Display possible completions depending on the options.
根据选项显示可能的完成情况。
Intended to be used from within a shell function generating possible
completions. If the optional WORD argument is supplied, matches against
WORD are generated.
旨在从生成可能完成的shell函数中使用。如果提供了可选的WORD参数,则会生成与WORD的匹配项。
Exit Status:
退出状态:
Returns success unless an invalid option is supplied or an error occurs.
除非提供无效选项或发生错误,否则返回成功。Example
$ compgen -W "external internal source other" -- inter
internal补全时候的使用
# tab补全时,会根据当前输入的部分字段进行补全 如:doc<tab> 会自动补全为 docker,动态生成
COMPREPLY=($(compgen -W "docker k8s physical other" -- $cur))
COMPREPLY=():
COMPREPLY 是一个 Bash 数组,负责存储自动补全的候选结果。当用户按下 Tab 键时,Bash 会使用 COMPREPLY 中的内容作为候选项进行补全。
compgen -W "docker k8s physical other":
compgen 是 Bash 的一个内置命令,常用于自动补全。
-W 选项告诉 compgen 使用后面的字符串("docker k8s physical other")作为候选项列表。
"docker k8s physical other" 是用户可以选择的固定候选项。这些选项可以是命令、参数或者其他你想要提供给用户的输入选项。
-- $cur:
-- 用于分隔选项和实际输入。后面的 $cur 表示当前用户输入的部分单词(即用户当前在命令行中输入的内容)。
$cur 是自动补全脚本中常用的变量,表示当前光标位置之前的部分输入,通常由 _get_comp_words_by_ref 函数提取出来。
# tab补全时,不会根据当前输入的部分字段动态生成 如:doc<tab> 会显示所有选项 docker k8s other physical
COMPREPLY=("docker" "k8s" "physical" "other")2.complete
complete [-abcdefgjksuv] [-o comp-option] [-DEI] [-A action] [-G globpat]
[-W wordlist] [-F function] [-C command] [-X filterpat]
[-P prefix] [-S suffix] name [name …]
complete -pr [-DEI] [name …]指定每个名称的参数应如何完成。
如果提供了 -p 选项,或者没有提供任何选项,则现有的完成规范将以允许将其重新用作输入的方式打印。
-r 选项删除每个名称name的完成规范,或者,如果未提供名称name,则删除所有完成规范。
-D 选项指示其他提供的选项和操作应应用于“默认default”命令完成;也就是说,尝试对先前未定义完成的命令进行完成。
-E 选项指示其他提供的选项和操作应应用于“空empty”命令完成;也就是说,尝试在空行上完成。
-I 选项指示其他提供的选项和操作应应用于行上初始非赋值字上的补全,或在命令分隔符(例如“;”或“|”之后)(通常是命令名称补全)。如果提供多个选项,则 -D 选项优先于 -E,并且两者都优先于 -I。如果提供了 -D、-E 或 -I 中的任何一个,则忽略任何其他名称参数;这些完成仅适用于选项指定的情况。
上面描述了尝试单词完成时应用这些完成规范的过程(请参阅可编程完成)。
其他选项(如果指定)具有以下含义。在调用完整的内置函数之前,应该引用 -G、-W 和 -X 选项(以及 -P 和 -S 选项)的参数,以防止它们扩展。
-o comp-option
comp-option 除了简单的补全生成,还控制 compspec 行为的几个方面。 comp-option 可能是以下之一:
bashdefault
如果 compspec 未生成匹配项,则执行其余的默认 Bash 补全。
default
如果 compspec 没有生成匹配项,则使用 Readline 的默认文件名补全。
dirnames
如果 compspec 没有生成匹配项,则执行目录名称补全。
filenames
告诉 Readline compspec 生成文件名,因此它可以执行任何特定于文件名的处理(例如在目录名中添加斜线、引用特殊字符或抑制尾随空格)。此选项旨在与使用以下指定的 shell 函数一起使用-F。
noquote
告诉 Readline 如果完成的单词是文件名则不要引用它们(默认是引用文件名)。
nosort
告诉 Readline 不要按字母顺序对可能的完成列表进行排序。
nospace
告诉 Readline 不要在行尾完成的单词后附加空格(默认)。
plusdirs
生成由 compspec 定义的任何匹配项之后,将尝试完成目录名称,并将任何匹配项添加到其他操作的结果中。
-F function
shell 函数function在当前 shell 环境中执行。执行时,第一个参数($1)是要完成其参数的命令的名称,第二个参数($2) 是要完成的单词,第三个参数($3) 是要完成的单词之前的单词,如上所述(请参阅可编程完成)。当功能完成时,可编程完成将从数组变量 COMPREPLY 的值中检索可能的完成。
Example
# 查看某个命令有没有配置自动补全
$ complete -p tools
complete -o default -F __start_tools tools
$ complete -p tools-linux
complete -o default -o nospace -F __start_tools tools-linux
#
$ complete -p tools-linux
complete -o default -F __start_tools tools-linux
$ complete -p ./tools-linux
complete -o default -F __start_tools ./tools-linux
# 可以
$ ./tools-linux
# 删除补全
complete -r ./tools_linux3.compopt
compopt [-o option] [-DEI] [+o option] [name]根据选项修改每个名称的完成选项,如果未提供名称,则修改当前执行的完成选项。如果未给出选项,则显示每个名称或当前完成的完成选项。
option 的可能值是对上述完整内置有效的值。
-D 选项指示其他提供的选项应应用于“默认”命令完成;也就是说,尝试对先前未定义完成的命令进行完成。
-E 选项指示其他提供的选项应适用于“空”命令完成;也就是说,尝试在空行上完成。
-I 选项指示其他提供的选项应应用于行上初始非赋值字上的补全,或在命令分隔符(例如“;”或“|”之后)(通常是命令名称补全)。
如果提供多个选项,则 -D 选项优先于 -E,并且两者都优先于 -I
返回值为 true,除非提供了无效选项、尝试修改不存在完成规范的名称的选项或发生输出错误。
if [[ $(type -t compopt) = "builtin" ]]; then
complete -o default -F __start_tools tools
else
complete -o default -o nospace -F __start_tools tools
fi
4.示例
命令自动补全所在目录: /usr/share/bash-completion/completions
内置变量
| 变量名 | 说明 |
|---|---|
| COMP_WORDS | 类型为数组,存放当前命令行中输入的所有单词 |
| COMP_CWORD | 类型为整数,当前输入的单词在COMP_WORDS中的索引 |
| COMPREPLY | 类型为数组,候选的补全结果 |
| COMP_WORDBREAKS | 类型为字符串,表示单词之间的分隔符 |
| COMP_LINE | 类型为字符串,表示当前的命令行输入字符 |
| COMP_POINT | 类型为整数,表示光标在当前命令行的哪个位置 |
| COMPREPLY | 一个数组变量,bash从这个变量中读取可编程补全所调用的shell函数生成的补全条目。 |
示例
complete_tools.sh
__tools_init_completion()
{
COMPREPLY=()
_get_comp_words_by_ref "$@" cur prev words cword
}
__start_tools()
{
local cur prev words cword split
if declare -F _init_completion >/dev/null 2>&1; then
_init_completion -s || return
else
__tools_init_completion -n "=" || return
fi
echo "开始"
echo $cur
echo $prev
echo "${words[@]}"
echo $cword
echo "结束"
}
if [[ $(type -t compopt) = "builtin" ]]; then
complete -o default -F __start_tools tools
else
complete -o default -o nospace -F __start_tools tools
fi使用
通过source complete_tools.sh加载,持久化可以添加到 .bashrc 中
测试
1.tools v<tab><tab>
# 输出
curl = v
prev = tools
words = tools v
cword = 1
2.tools v v<tab><tab>
# 输出
curl = v
prev = v
words = tools v v
cword = 2
3.tools v <tab><tab>
curl =
prev = v
words = tools v
cword = 2
结束
说明
cur prev words cword
变量cur中包含了命令行当前所在的单词,prev为前一个单词, words为完整的命令行单词数组,cword为单词数组的当前下标
三.Bash 变量
这些变量由 Bash 设置或使用,但其他 shell 通常不会特殊对待它们。
Bash 使用的一些变量在不同的章节中进行了描述:用于控制作业控制工具的变量(请参阅作业控制变量)。
1.COMP_WORDBREAKS
Readline 库在执行单词补全时将其视为单词分隔符的字符集。如果COMP_WORDBREAKS 未设置,它将失去其特殊属性,即使随后将其重置也是如此。
$ echo $COMP_WORDBREAKS
"'><=;|&(:2.COMP_WORDS
数组变量
一个数组变量,由当前命令行中的各个单词组成。该行被拆分成单词,就像 Readline 拆分它一样,使用 COMP_WORDBREAKS如上所述的方法。此变量仅在由可编程完成功能调用的 shell 函数中可用(请参阅可编程完成)。
3.COMP_CWORD
当前单词索引
包含当前光标位置的单词的 ${COMP_WORDS} 索引。该变量仅在由可编程完成工具调用的 shell 函数中可用(请参阅可编程完成)。
4.COMPREPLY
一个数组变量,Bash 从中读取由可编程完成工具调用的 shell 函数生成的可能完成(请参阅可编程完成)。每个数组元素包含一种可能的完成。
四.bash-completion 包
如果要实现tab,需要安装bash-completion包
_get_comp_words_by_ref
_get_comp_words_by_ref 是 Bash 的一个辅助函数,通常用于编写 Bash 自动补全功能时,帮助获取用户在命令行中输入的参数值。命令自动补全所在目录: /usr/share/bash-completion/completions
1.加载流程
1./etc/bash.bashrc中加载的部分是注释的
# enable bash completion in interactive shells
#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
#fi2.~/.bashrc中加载,用户登陆后会加载
# enable programmable completion features (you don't need to enable
# this, if it's already enabled in /etc/bash.bashrc and /etc/profile
# sources /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
fi3./usr/share/bash-completion/bash_completion文件存在,就会加载该文件
这个主文件定义了 _completion_loader 函数。
4.按需调用_completion_loader
当你在命令行输入某个命令然后按 TAB 时,如果该命令没有补全定义,Bash 就会触发 _completion_loader。
_completion_loader 会去 /usr/share/bash-completion/completions/<command> 查找对应的补全脚本并加载。
5.脚本只加载一次
一旦某个命令的补全脚本被加载,它就在当前 Shell 会话里生效,不会每次 TAB 都重新加载。
示例
# ip命令补全
/usr/share/bash-completion/completions/ip
# 新打开一个终端
# 查看ip命令是否存在补全,以下说明 ip 的补全函数还没有加载
$ type _ip
-bash: type: _ip: not found
$ complete -p ip
-bash: complete: ip: no completion specification
# 按一次 TAB,触发补全
$ ip <TAB>
# 再次查看,说明补全函数已经加载
$ type _ip
_ip is a function
_ip ()
{
...
}
# 查看已存在补全函数
$ complete -p ip
complete -F _ip ip五.tools管理工具定制
1.tools命令补全示例
# complete_tool.sh
_service_tool() {
local cur prev prevprev threevar TOOLS_COLS_ALL
COMPREPLY=()
cur=${COMP_WORDS[COMP_CWORD]}
prev=${COMP_WORDS[COMP_CWORD - 1]}
prevprev=${COMP_WORDS[COMP_CWORD - 3]}
threevar=${COMP_WORDS[2]}
TOOLS_COLS_ALL="host env job rollback restart branch build gitlab deljenkins addjenkins grafana nacosjdbc"
case $prev in
'tools')
COMPREPLY=($(compgen -W '-t' -- $cur))
;;
'-t')
COMPREPLY=($(compgen -W "$TOOLS_COLS_ALL" -- $cur))
;;
'host')
COMPREPLY=($(compgen -W '-h' -- $cur))
;;
'env')
COMPREPLY=($(compgen -W '-e -a' -- $cur))
;;
'job')
COMPREPLY=($(compgen -W '-j -e -a' -- $cur))
;;
'rollback')
COMPREPLY=($(compgen -W '-j -e -v' -- $cur))
;;
'restart')
COMPREPLY=($(compgen -W '-j -e' -- $cur))
;;
'branch')
COMPREPLY=($(compgen -W '-j -e -b -w' -- $cur))
;;
'build')
COMPREPLY=($(compgen -W '-j -e -k -l' -- $cur))
;;
'gitlab')
COMPREPLY=($(compgen -W '-e -g -w' -- $cur))
;;
'deljenkins')
COMPREPLY=($(compgen -W '-j -e' -- $cur))
;;
'addjenkins')
COMPREPLY=($(compgen -W '-j -e -b -g' -- $cur))
;;
'grafana')
COMPREPLY=($(compgen -W '-e -g -d' -- $cur))
;;
'nacosjdbc')
COMPREPLY=($(compgen -W '-g -b -h' -- $cur))
;;
'-e')
local env
if [[ ${threevar} == "addjenkins" ]]; then
return 0
fi
env=$(tools -t env | awk -F '|' '{print $2}' | tail -n +4 | grep -v "^$")
COMPREPLY=($(compgen -W "${env[@]}" -- $cur))
;;
'-j')
local envname jobname deletechar
deletechar=",'"
if [[ ${threevar} == "job" && ${prevprev} == '-e' ]]; then
envname=${COMP_WORDS[COMP_CWORD - 2]}
#jobname=$(tools -t job -e ${envname} | grep ${envname} | awk 'BEGIN{FS="\[|\]"}{print $2}')
#jobname=${jobname//[$deletechar]/}
jobname=$(tools -t job -e ${envname} | grep ${envname} | sed "s/[\']/\n/g"|sed '/[,\|]/d'|sort)
COMPREPLY=($(compgen -W "${jobname[@]}" -- $cur))
elif [[ ${threevar} == "rollback" && ${prevprev} == '-e' ]]; then
envname=${COMP_WORDS[COMP_CWORD - 2]}
jobname=$(tools -t build -e ${envname} -l | awk -F "|" '{print $2}')
COMPREPLY=($(compgen -W "${jobname[@]}" -- $cur))
elif [[ ${threevar} == "restart" && ${prevprev} == '-e' ]]; then
envname=${COMP_WORDS[COMP_CWORD - 2]}
jobname=$(tools -t build -e ${envname} -l | awk -F "|" '{print $2}')
COMPREPLY=($(compgen -W "${jobname[@]}" -- $cur))
elif [[ ${threevar} == "branch" ]]; then
if [[ ${prevprev} == '-e' ]]; then
envname=${COMP_WORDS[COMP_CWORD - 2]}
elif [[ ${COMP_WORDS[3]} == '-e' ]]; then
envname=${COMP_WORDS[4]}
fi
jobname=$(tools -t build -e ${envname} -l | awk -F "|" '{print $2}')
COMPREPLY=($(compgen -W "${jobname[@]}" -- $cur))
elif [[ ${threevar} == "addjenkins" ]]; then
return 0
elif [ ${prevprev} == '-e' ]; then
envname=${COMP_WORDS[COMP_CWORD - 2]}
jobname=$(tools -t build -e ${envname})
COMPREPLY=($(compgen -W "${jobname[@]}" -- $cur))
fi
;;
'-h')
local hostname
hostname=$(grep "Host " /etc/ssh/ssh_config |grep -v [*\#]|awk '{print $2}')
COMPREPLY=($(compgen -W "${hostname[@]}" -- $cur))
;;
'-b')
local branch
if [[ ${threevar} == branch ]]; then
branch="release-2.4.0"
COMPREPLY=($(compgen -W "${branch}" -- $cur))
fi
;;
esac
return 0
}
complete -F _service_tool tools
加载,即可使用
source complete_tool.sh自动加载
# copy文件到/usr/share/bash-completion/completions,非必须,可以自己调整,或者放到/usr/local/bin目录下,然后source
cp complete_tool.sh /usr/share/bash-completion/completions
# 编辑~/.bashrc文件
在后面添加
source /usr/share/bash-completion/completions/complete_tool.sh2.提高补全速度
1.通过全局变量缓存结果
你可以使用全局变量来缓存候选项,并确保 tools -t env 只执行一次。
# 定义全局缓存变量
_env_cache=""
_my_autocomplete() {
local cur prev
_get_comp_words_by_ref -n = cur prev
# 检查缓存,如果为空则调用命令
if [[ -z "$_env_cache" ]]; then
_env_cache=$(tools -t env | awk -F '|' '{print $2}' | tail -n +4 | grep -v "^$")
fi
# 使用缓存的值进行补全
COMPREPLY=($(compgen -W "${_env_cache[@]}" -- "$cur"))
}
complete -F _my_autocomplete mycommand
2.通过静态文件缓存
如果你希望跨多个 shell 会话使用同样的缓存结果,可以将 tools -t env 的结果存储在文件中,并在需要时读取文件。
_my_autocomplete() {
local cur prev env_file="/tmp/env_cache.txt"
_get_comp_words_by_ref -n = cur prev
# 如果缓存文件不存在或为空,则执行命令并缓存结果
if [[ ! -s "$env_file" ]]; then
tools -t env | awk -F '|' '{print $2}' | tail -n +4 | grep -v "^$" > "$env_file"
fi
# 读取缓存文件中的值
env=$(cat "$env_file")
COMPREPLY=($(compgen -W "$env" -- "$cur"))
}
complete -F _my_autocomplete mycommand
这种方式会将结果存储到 /tmp/env_cache.txt 文件中,你可以根据需要手动或定期更新该文件。
通过过期时间来更新缓存
你可以为缓存文件设置一个过期时间,比如 10 分钟。如果缓存文件的修改时间超过这个时间,则重新生成缓存。
_my_autocomplete() {
local cur prev
_get_comp_words_by_ref -n = cur prev
# 缓存文件路径和过期时间(单位:秒)
local env_file="/tmp/env_cache.txt"
local cache_lifetime=600 # 10 分钟
# 如果文件不存在,或者超过了缓存时间,则更新文件
if [[ ! -f "$env_file" || $(($(date +%s) - $(stat -c %Y "$env_file"))) -ge $cache_lifetime ]]; then
tools -t env | awk -F '|' '{print $2}' | tail -n +4 | grep -v "^$" > "$env_file"
fi
# 读取缓存文件中的值
env=$(cat "$env_file")
COMPREPLY=($(compgen -W "$env" -- "$cur"))
}
complete -F _my_autocomplete mycommand