bash completion 指令自动补全详解

龙弘盛
2023-12-01

简介

  • 什么是补全, 什么是补全规则;

  • 补全规则怎么选;

  • 补全规则怎么生成候选项;

  • compgen, complete, compopt指令;

  • 案例

项目路径

什么是补全

  • shell交互式命令行<tab>键就会触发补全;

  • 补全一般是补全参数; 当然也有补全指令; 补全可以避免使用者记忆, 减少拼写错误;

补全规则

  • 补全规则: compspec: completion specification;

  • 即为某个指令生成参数补全集合的一些脚本指令, bash内部的一些变量, bash可以提供的很多信息;

  • 生成的是一系列的单词组; 供候选补齐;

补全规则选择

  • 空指令: 使用complete -E定义的规则, 用其生成候选集合; 一般可以配置为自己常用的一些指令; 比如编译环境下生成编译指令集合;

  • 相对或绝对路径: 查找和路径完全匹配的规则; 无匹配则用其最后文件名作为指令进行规则搜索;

  • 指令: 普通指令则同样进行匹配搜索;

  • 无指令匹配: 采用complete -D定义的默认规则生成;

  • 无默认规则: 指令是否为alias, 如果是恢复原状, 使用恢复后的指令搜索; (部分版本支持)

  • 无任何匹配规则: 采用Bash的默认补全机制;

补全规则生成候选项

  • -A action 或其简写 -abcdefgjksuv 生成集合;

生成集合: 根据拼写一截的补全; 比如git sta就有补全stage stash status;根据sta过滤; -f -d生成集合会被FIGNORE过滤;

  • -G 根据文件扩张规则生成集合

比如-G "*.sh", 这个就是文件扩张生成当前目录下sh结尾的文件; 不会被sta这种影响, 会全部纳入; 但是受到FIGNORE的影响;

  • -W 字符串被${IFS}分词后的集合

-W "hello world $PATH", 进行bash的扩张后生成的字符串, 再进行bash分词规则生成字符串集合; git sta会从过滤集合中过滤前缀sta的集合; -W某些场景完全可以替代; 差距就是是否需要对集合过滤;

  • -F -C: 会生成变量; COMP_LINE, COMP_POINT, COMP_KEY, COMP_TYPE; -F还会生成COMP_WORDS, COMP_CWORD; 入参:$1指令名, $2补全名, 如sta; $3不全名前一个单词, 比如git sta, sta前一个就是git; 无过滤规则; 即生成的所有都纳入候选, 需指令提供者或函数提供者根据输入参数进行过滤;

-F优先: 是shell函数; 可以通过compgen -A function查看当前环境下的函数定义; 当然一般是局部自定义的; -F指定的function一般结合compgen, comopt使用; 返回集合通过COMREPLY数组, 即一般使用COMPREPLY=( hello world );
-C次之: 基本和-F相同, 除了没有COMP_WORDS, COMP_CWORD; 输出到标准输出的字符集按\n进行分词, 生成结果集; 可以结合\使用;

  • -X: 过滤文件扩张集合, 不会过滤-F, -C生成的集合;

过滤规则:&被替代为sta; 可以用\&保持愿意; !则是取反; shopt nocasematch可以按照不区分大小写匹配; complete -G "*" -X "!&*" todo; 完全匹配的会被删除; 保留不匹配的;

  • -P -S添加前后缀;

补充-o选项;

  • 如果前面没有任何匹配集合, 且规则指定了-o dirnames; 会将当前目录下的文件夹名纳入匹配, 并过滤; 但是如果有匹配集合这个选项就不会生效;
  • 指定了-o plusdirs; 会将目录纳入集合; 和-o dirnames区别在于, -o plusdirs一定会生效; complete -W "hello" -o plusdirs todo;
  • 如果上面的complete指定的规则; bash completion,Readline的补齐规则会被禁用; 可以通过-o bashdefault开启bash complettion,-o default开启Readline completion 作为补充手段, 这个是没有任何生成集合的时候才会生效, 有就不会生效;

  • 如果有compspec是目录, 会补全/; 可以通过一些手段规避; complete -W "hello $(find . -maxdepth 1 -mindepth 1 -type d -printf "%f\n")" todo; compgen -d生成的也没有后缀;

补充: 动态加载规则

  • 一般都是一次性的加载; 但是当我们-F的返回值是124的时候, 会重新加载当前指令的complete; 即返回124表示当前指令的规则发生了变化;
_completion_loader()
{
. "/etc/bash_completion.d/$1.sh" >/dev/null 2>&1 && return 124
}
complete -D -F _completion_loader -o bashdefault -o default
  • 默认规则, 用于动态生成某个指令的规则; 即使用了指令并触发才生成;

指令: compgen, complete, compopt

compgen

格式: compgen [option] [word]

  • 这个是手动触发; 即模拟complete;
  • option是可以是complete的所有选项, 除了-r, -p; -F -C在这里也不太好用; 因为相关参数没有设置;
  • word用于过滤; 即git sta案例; 可以理解为sta就是这里的word参与过滤; 没有就不过滤; 返回所有;
  • 具体使用参见后面案例

compopt

格式: compopt [-o option] [-DEI] [+o option] [name], 即控制某个指令的选项开关; 不修改其他规则;

  • -o option: 打开某个选项;
  • +o option: 关闭某个选项;
  • name: 指定具体指令, 未指定表示当前指令;
  • [-DEI]: 指定是name忽略;

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 ...]

简介

-abcdefgjksuv-A action的简写, 后面介绍; -o comp-option: 补充手段开关; -DEI 默认规则或空指令规则; -A action: bash提供生成集合; -G 自定义文件扩张生成集合; -W "hello world $(ls) $((1+1)) ${var}": 按照${IFS}分词生成自定义集合; -F function: shell脚本生成集合; -C指令生成集合; -X文件扩张过滤器; -P -S前后缀; -pr 输出或删除;

complete -pr [-DEI] [name ...]

  • -p: 输出name ...对应的规则; 未指定name则是输出所有;

  • -r: 删除name ...对应的规则; 未指定name则是删除所有;

  • -pr [-DEI]: 删除默认, 空白指令规则, name会忽略;

  • -D: 默认选项, 即某指令没有规则, 则使用默认规则; 为所有没有规则的指令提供的规则;

  • -E: 空规则; 即没有指定指令的时候的规则; 即空白行;

  • -I: 部分不支持; 即指令补齐规则; ; |也会触发;

  • -DEI同时指定; -D > -E > -I; 优先级小的会被忽略;

-o comp-option

  • -o bashdefault: 有规则默认关闭; 开启后规则未生成任何候选项时才触发bash补齐规则;

  • -o default: 有规则默认关闭; 开启后规则未生成任何候选项时才触发Readline补齐规则;

  • -o dirnames: 开启后规则未生成任何候选项时, 提供当前目录下的文件夹, 会有后缀/;

  • -o filenames: 开启后规则未生成任何候选项时, 提供当前目录下的所有文件; 并会进行文件夹加/, 特殊字符文件或文件夹括起来; 消除末尾空格; 一般和-F使用;

  • -o noquote: 关闭路径中特殊符号处理; 默认会括起来;

  • -o nosort: 结果集不进行排序; 默认排序;

  • -o nospace: 补齐后不添加空格; 默认添加空格; 这种一般很少用; 都会添加空格分词;

  • -o plusdirs: 规则有匹配集合时才添加;并规律; 且有后缀/;

-A action: 都可以通过compgen -A action查看生成集合; 一般在函数中使用;

  • -A alias | -a: 提供alias集合到候选项;

  • -A arrayvar: 提供basharray类型变量名到候选项;

  • -A binding: bash Readline key binding names添加到候选项;

  • -A builtin | -b : bash内置指令添加到候选项;

  • -A command | -c : 将指令添加到候选项;

  • -A directory | -d: 将当前路径名加入候选项;

  • -A disabled : 将shell禁用的内置指令纳入候选项; 一般为空;

  • -A enabled : 将shell启用的内置指令纳入候选项;

  • -A export | -e : 将shell export的变量名纳入候选项;

  • -A file | -f: 将当前文件夹下的所有文件夹和文件纳入候选项;

  • -A function: 将当前环境所有可访问的shell函数纳入候选项; declar -f function_name查看代码; shopt -s extdebug;declare -F function_name查看函数定义的文件位置; https://www.cyberciti.biz/faq/how-to-find-bash-shell-function-source-code-on-linuxunix/

  • -A group | -g: 用户组名纳入候选项;

  • -A helptopic: 内置指令help支持的命令;

  • -A hostname: 查看当前houstname, 一般是HOSTFILE指定的文件;

  • -A job | -j: job控制的所有程序; 死的活的;

  • -A keyword | -k : bash内置的关键字, 如if之类的;

  • -A running: job控制的程序, 活的;

  • -A service | -s: 将当前所有服务名纳入候选项;

  • -A setopt: set指令可以通过-o指定的所有合法选项纳入候选项;

  • -A signal: 将当前系统信号名纳入候选项;

  • -A stopped: job控制死的程序;

  • -A user | -u: 当前系统所有用户名纳入候选项;

  • -A variable | -v: 将当前环境所有shell变量名纳入候选项;

-C command: 指令

  • shell中执行; 输入: $1:指令名; $2:git sta中的sta; $3: git stasta的前一个单词git; 输出按\n分词纳入候选集合; 集合不会被过滤;

-F function

  • -F, 输出通过COMPREPLY变量指定; 变量是array类型; 不会被git sta过v了;

-G globpat

  • 文件扩展, 具体参上; 不会被git stasta过滤;

-P prefix | -S suffix: 集合前都添加前缀, 后缀;

  • 一般是-P --, 即自动补齐;

-W wordlist

  • -W "hello world $(ls) ${var}", bash扩张处理后将结果按照IFS拆分并过滤;

-X filterpat

  • 专门对文件扩张结果进行过滤; 具体参数;

总结

本文主要讲了什么是规则; 规则怎么选择的; 候选项怎么生成的; 以及指令compgen模拟; complete定义; compopt动态修改; -F返回124重新加载;

案例

  • 官方案例

https://github.com/scop/bash-completion/

  • 文件夹处理

-F -W生成的路径无/, -A,-o开启的选项有;

  • 补全空格

function test_func
{
 # echo "$1:$2:$3:${COMP_WORDS[@]}:" >> log.txt
 local cur=${COMP_WORDS[1]}
 local param=$2
 case ${COMP_WORDS[1]} in
 sdk2app)
   if [[ "${param}" == "" ]] ;
   then
     COMPREPLY=($(compgen -W "front back" -- $param))
   else
     COMPREPLY=("sdk2app")
   fi;;
 *) COMPREPLY=($( compgen -W "sdk2app" $cur));;
 esac;
}
complete -F test_func todo
  • 案例三: 空白补全

$ complete -E -W todo
$ complete 
complete -W 'todo' -E
$ <tab>
  • 绝对或相对路径未匹配使用文件名

function _comp
{
echo $1:$2:$3 >> log.txt
COMPREPLY=( aaa bbb )
}
complete -F _comp test.sh

/aaa/bbb/test.sh的规则不存在, 使用test.sh的规则.

ch@ch:~/ch/shfile/complete$ source func.sh 
ch@ch:~/ch/shfile/complete$ complete 
complete -F _comp test.sh
ch@ch:~/ch/shfile/complete$ /aaa/bbb/test.sh 
aaa  bbb  
ch@ch:~/ch/shfile/complete$ /aaa/bbb/test.sh 
  • -A action的最先生成, 且会被候选词过滤;

ch@ch:~/ch/shfile/complete$ complete -r
ch@ch:~/ch/shfile/complete$ source func.sh 
ch@ch:~/ch/shfile/complete$ complete 
complete -A signal todo
ch@ch:~/ch/shfile/complete$ todo SIG
SIGABRT      SIGCONT      SIGINT       SIGKILL      SIGQUIT      SIGRTMAX-11  SIGRTMAX-2   SIGRTMAX-6   SIGRTMIN     SIGRTMIN+12  SIGRTMIN+2   SIGRTMIN+6   SIGSEGV      SIGTERM      SIGTTOU      SIGVTALRM
SIGALRM      SIGFPE       SIGIO        SIGPIPE      SIGRTMAX     SIGRTMAX-12  SIGRTMAX-3   SIGRTMAX-7   SIGRTMIN+1   SIGRTMIN+13  SIGRTMIN+3   SIGRTMIN+7   SIGSTKFLT    SIGTRAP      SIGURG       SIGWINCH
SIGBUS       SIGHUP       SIGJUNK(32)  SIGPROF      SIGRTMAX-1   SIGRTMAX-13  SIGRTMAX-4   SIGRTMAX-8   SIGRTMIN+10  SIGRTMIN+14  SIGRTMIN+4   SIGRTMIN+8   SIGSTOP      SIGTSTP      SIGUSR1      SIGXCPU
SIGCHLD      SIGILL       SIGJUNK(33)  SIGPWR       SIGRTMAX-10  SIGRTMAX-14  SIGRTMAX-5   SIGRTMAX-9   SIGRTMIN+11  SIGRTMIN+15  SIGRTMIN+5   SIGRTMIN+9   SIGSYS       SIGTTIN      SIGUSR2      SIGXFSZ
ch@ch:~/ch/shfile/complete$ todo SIGA
SIGABRT  SIGALRM  
ch@ch:~/ch/shfile/complete$ todo SIGA

指令如上: complete -A signal todo

  • -A action-A file | -f, -A directory | -d会收到变量FIGNORE的值影响;

ch@ch:~/ch/shfile/complete$ complete -r
ch@ch:~/ch/shfile/complete$ complete 
ch@ch:~/ch/shfile/complete$ complete -f todo
ch@ch:~/ch/shfile/complete$ ls
func.sh  log.txt  test.txt
ch@ch:~/ch/shfile/complete$ todo 
func.sh   log.txt   test.txt  
ch@ch:~/ch/shfile/complete$ export FIGNORE=.sh
ch@ch:~/ch/shfile/complete$ ls
func.sh  log.txt  test.txt
ch@ch:~/ch/shfile/complete$ todo 
log.txt   test.txt  
ch@ch:~/ch/shfile/complete$ todo 

先执行 complete -r清理所有规则; complete查看规则; ls查看文件, todo补齐显示所有文件; 定义变量FIGNORE设置过滤文件; 再次todo补全, 则无.sh文件;
注意: 支持过滤;

  • -G指定的文件扩张; 不过滤; 受FIGNORE影响;

ch@ch:~/ch/shfile/complete$ complete -r
ch@ch:~/ch/shfile/complete$ complete 
ch@ch:~/ch/shfile/complete$ complete -G "*.txt" todo
ch@ch:~/ch/shfile/complete$ todo 
log.txt   test.txt  
ch@ch:~/ch/shfile/complete$ todo lo
log.txt   test.txt  
ch@ch:~/ch/shfile/complete$ todo lo

根据执行结果可以看到不会过滤匹配lo的;这种不建议使用;同样受FIGNORE影响, 可自行尝试;

  • -W shellexpr : 一般是一个字符串, 但是也会进行bash扩张, 也就是说完全可以当成shell进行编程, 只是没有输入而已; 但是会对输出结果进行过滤;

ch@ch:~/ch/shfile/complete$ complete -r
ch@ch:~/ch/shfile/complete$ complete 
ch@ch:~/ch/shfile/complete$ complete -W "hello world $(echo good) $(for i in {1..9};do echo $i;done;) $((100+100))" todo
ch@ch:~/ch/shfile/complete$ todo 
1      2      200    3      4      5      6      7      8      9      good   hello  world  
ch@ch:~/ch/shfile/complete$ todo 2
2    200  
ch@ch:~/ch/shfile/complete$ todo 2

可以看到在-W中进行了很多的shell操作, bash会进行指令扩张, 然后使用${IFS}进行分词; 分词结果就是候选项; Readline会进行排序, 可以通过开关关闭排序; -o nosort;

ch@ch:~/ch/shfile/complete$ complete -o nosort -W "hello world $(echo good) $(for i in {1..9};do echo $i;done;) $((100+100))" todo
ch@ch:~/ch/shfile/complete$ todo 
hello  world  good   1      2      3      4      5      6      7      8      9      200
  • -F实现未选项参数补齐; 不过滤; 但是可以根据输入自行过滤;

function _comp
{
 echo $1:$2:$3:${2:2} >> log.txt
 COMPREPLY=( $(compgen -W "hello world" -- ${2:2}) )
 echo reply:${COMPREPLY[@]} >> log.txt
}
complete -F _comp -P "--" todo

执行过程

ch@ch:~/ch/shfile/complete$ complete -r
ch@ch:~/ch/shfile/complete$ source func.sh 
ch@ch:~/ch/shfile/complete$ complete 
complete -P '--' -F _comp todo
ch@ch:~/ch/shfile/complete$ todo --
--hello  --world  
ch@ch:~/ch/shfile/complete$ todo --hello 

需要注意: 生成的候选列可能会替换修改, $2 即补全单词; 导致一些小bug; 去掉代码中的${2:2}就可以看到;
不懂compgen什么作用的可以直接执行指令compgen -f && ls, 观察输出结果;

  • -X过滤

ch@ch:~/ch/shfile/complete$ complete -f -X '!&*' todo
ch@ch:~/ch/shfile/complete$ todo 
func.sh   log.txt   test.txt  tt.txt    
ch@ch:~/ch/shfile/complete$ todo t
test.txt  tt.txt    
ch@ch:~/ch/shfile/complete$ todo t

可以看到自行过滤一些;过滤匹配的, 前置!表示过滤不匹配的;这里是常规匹配;下面试一下func.sh不参与匹配;

ch@ch:~/ch/shfile/complete$ complete -f -X '!&*' -X "*.sh" todo
ch@ch:~/ch/shfile/complete$ todo 
log.txt   test.txt  tt.txt    
ch@ch:~/ch/shfile/complete$ todo 

可以看到sh结尾的被过滤掉了;
-X '!&*'为什么是单引号? 因为双引号会进行bash处理, !有特殊含义;

  • -P添加前缀, 选项参数

function _comp
{
 echo $1:$2:$3:${2:2} >> log.txt
 COMPREPLY=( $(compgen -W "hello world" -- ${2:2}) )
 echo reply:${COMPREPLY[@]} >> log.txt
}
complete -F _comp -P "--" todo

和上面的重复直接拷贝了; 直接看下面的执行结果;

ch@ch:~/ch/shfile/complete$ complete -r
ch@ch:~/ch/shfile/complete$ complete 
ch@ch:~/ch/shfile/complete$ source func.sh 
ch@ch:~/ch/shfile/complete$ complete 
complete -P '--' -F _comp todo
ch@ch:~/ch/shfile/complete$ todo --
--hello  --world  
ch@ch:~/ch/shfile/complete$ todo --hello 

可以看到为所有结果加了前缀--; 然后进行匹配;

  • 动态加载: 减少一次性加载耗时,规则搜索慢问题;这里简单介绍一下动态修改;

_COMPFLAG=124
function _comp
{
echo $1:$2:$3:${2:2} >> log.txt
COMPREPLY=( $(compgen -W "hello world" -- ${2:2}) )
echo reply:${COMPREPLY[@]} >> log.txt
compopt -o plusdirs $1
local temp=${_COMPFLAG}
_COMPFLAG=0
echo ${temp} >> log.txt
return ${temp}
}
complete -F _comp -P "--" todo
ch@ch:~/ch/shfile/complete$ complete 
ch@ch:~/ch/shfile/complete$ complete -r
ch@ch:~/ch/shfile/complete$ complete 
ch@ch:~/ch/shfile/complete$ source func.sh 
ch@ch:~/ch/shfile/complete$ complete
complete -P '--' -F _comp todo
ch@ch:~/ch/shfile/complete$ todo 
--hello  temp/    --world  
ch@ch:~/ch/shfile/complete$ complete 
complete -o plusdirs -P '--' -F _comp todo

可以看到前后变化; 和官方有点出入;如果不用_COMPFLAG机制, 会一直重新加载; 无任何生成;

_completion_loader()
{
. "/etc/bash_completion.d/$1.sh" >/dev/null 2>&1 && return 124
}
complete -D -F _completion_loader -o bashdefault -o default
 类似资料: