一直对shell的自动补全功能很感兴趣,趁此机会简单探究一下,一个成熟的应用程序究竟是怎么借助bash来完成自定义命令的补全的。
XrandR
简单介绍一下xrandr,它是一个命令行工具,用于管理和配置Linux系统中的显示器和屏幕分辨率。它是RandR(Resize and Rotate)扩展的一部分,该扩展允许动态调整显示器的分辨率、旋转方向和刷新率。
它的具体功能与本文内容无关,只要知道有这么一个工具就行了,可以自己在linux系统上随便敲几个xrandr的指令,感受一下它的自动补全功能。
可以看到,xrandr既能对自定义的固定命令(如 --output)进行补全,又能根据随时变化的分辨率列表对分辨率信息进行补全。
它是如何实现这一功能的呢?
附一份xrandr的源码的链接
来自gitlab freedesktop的xrandr源码
如果不方便下载,我也会上传一份。
bash_completion
百度一下,可以知道,bash下的自动补全功能基本都是借助bash-completion完成的。
bash-completion是一个Bash补全功能的框架,它可以为Bash命令提供自动补全的功能。
但是网上给出的bash-completion讲解都比较简单,主要是针对
complete、compgen之类函数的功能的介绍。比如:
【shell】命令行自动补全(compgen、complete、compopt)
参照网上实例写出来的脚本固然可以实现自动补全,但是和成熟的应用程序比起来还差很远,尤其是xrandr这种根据前面输入的选项和实时信息提供补全的功能。
补全功能脚本示例
对于此类命令的补全功能,Linux提供了一个统一的路径用来存放它们的补全脚本。
/usr/share/bash-completion/completions/
程序在安装时,它的配套脚本就会被放入该路径。系统启动时,上一级目录有名为bash_completion的脚本,将completions/下的补全脚本会被加载,对应的补全函数即会生效。
在此路径下,我们可以找到名为"xrandr"的脚本,现在把它拆开来看一看。
# bash completion for xrandr -*- shell-script -*-
_xrandr()
{
local cur prev words cword
_init_completion || return
case "$prev" in
-display|-d|-help|-s|--size|-r|--rate|--refresh|--screen|--fb|--fbmm|\
--dpi|--pos|--set|--scale|--transform|--crtc|--panning|--gamma|\
--newmode|--rmmode|--addmode|--delmode)
return
;;
--output|--left-of|--right-of|--above|--below|--same-as)
local outputs=$("$1" | awk '/connected/ {print $1}')
COMPREPLY=( $(compgen -W "$outputs" -- "$cur") )
return
;;
--mode)
local i output
for (( i=1; i < cword; i++ )); do
if [[ "${words[i]}" == --output ]]; then
output=${words[i+1]}
break
fi
done
if [[ $output ]]; then
local modes=$("$1" | command sed -e "1,/^$output / d" \
-e "/connected/,$ d" \
-e "s/\([^[:space:]]\)[[:space:]].*/\1/")
COMPREPLY=( $(compgen -W "$modes" -- "$cur") )
fi
return
;;
-o|--orientation)
COMPREPLY=( $(compgen -W 'normal inverted left right 0 1 2 3' -- \
"$cur") )
return
;;
--reflect)
COMPREPLY=( $(compgen -W 'normal x y xy' -- "$cur") )
return
;;
--rotate)
COMPREPLY=( $(compgen -W 'normal inverted left right' -- "$cur") )
return
;;
--setprovideroutputsource|--setprovideroffloadsink)
local providers=$("$1" --listproviders 2>/dev/null |
command sed -ne 's/.* name:\([^ ]*\).*/\1/p')
COMPREPLY=( $(compgen -W "$providers" -- "$cur") )
# TODO 2nd arg needed, is that a provider as well?
return
;;
esac
COMPREPLY=( $(compgen -W '$("$1" -help 2>&1 |
command sed -e "s/ or / /g" -e "s/<[^>]*>]//g" | _parse_help -)' \
-- "$cur") )
} &&
complete -F _xrandr xrandr
# ex: filetype=sh
此脚本的case分支可以找到与xrandr功能对应的关键词,如–display、–pos、–rate等。
脚本末尾一行
complete -F _xrandr xrandr
使用complete -F指令,该指令告诉bash,当输入xrandr需要自动补全时,则调用_xrandr函数。至此,一个完成的自动补全功能就被搭建完成,要想使其有更灵活的补全功能,只需要在_xrandr函数中进行实现即可。
xrandr如何根据不同接口补全分辨率?
在实际使用中,不同的视频接口会有不同的分辨率列表。补全脚本又是如何根据用户的输入,提供对应的补全列表的呢?
我们可以看脚本中对于"–output"指令的补全,该选项的作用是给xrandr指定要配置的是哪一路显示输出,因此它应该提供当前所有的接口名,作为补全选项。
--output|--left-of|--right-of|--above|--below|--same-as)
local outputs=$("$1" | awk '/connected/ {print $1}')
COMPREPLY=( $(compgen -W "$outputs" -- "$cur") )
return
;;
这部分脚本很容易,只有两行。第一行是获取补全单词,把它们赋值给output变量;第二行是通用写法,调用compgen指令,把output变量里保存的单词当作补全选项提供给用户。
试着敲一下第一行这个命令看看输出:
再按照正常使用方法,敲一个–output选项然后按tab看看bash给出的补全选项:
可以看到,二者给出的结果是一样的。
总结
至此,我们大概弄清楚了补全功能是怎么回事。总结一下一共是三点:
- 使用bash提供的complete系列函数构建一个补全脚本来完成补全功能;
- 将补全脚本放在/usr/share/bash-completion/completions/下,系统在启动时会自动加载;
- 开发者主要的工作是实现补全函数,要运用各种bash指令对文本进行处理,进而达到“智能”的补全效果。