本文共 10650 字,大约阅读时间需要 35 分钟。
Bash自动补全是为了帮助用户能够更快、更容易输入命令的一项功能。它能够在用户输入命令时敲击tab键后,提供可能的选项。
\\\$ git\u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\git git-receive-pack git-upload-archive \gitk git-shell git-upload-pack \$ git-s\u0026lt;tab\u0026gt;\$ git-shell\\
Bash补全脚本是一段使用bash内置命令command的代码,用于定义哪些补全建议可以对特定的可执行程序显示。这些补全建议既可以是简单的静态内容,也可以是高度复杂的。
\\自动补全功能能够为用户提供以下便利:
\\下面我们将开始一个演示。
\\首选,我们将会创建一个名为dothis的模拟可执行脚本。该脚本接受一个参数,表示用户执行历史中的序号,并执行序号对应的历史命令。例如,以下命令将会执行用户历史命令中序号为235的命令(我电脑上对应的是ls -a命令):
\\\dothis 235\\
然后,我们将创建一个bash自动补全脚本,用以展示用户历史命令信息,并和dothis命令“绑定”起来。
\\\$ dothis \u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\215 ls\216 ls -la\217 cd ~\218 man history\219 git status\220 history | cut -c 8-\\
读者可以在位于GitHub上的中看见gif演示动图。
\\现在让我们开始吧。
\\在工作目录中创建名为dothis的文件,并添加以下代码:
\\\if [ -z \"$1\" ]; then\ echo \"No command number passed\"\ exit 2\fi\\exists=$(fc -l -1000 | grep ^$1 -- 2\u0026gt;/dev/null)\\if [ -n \"$exists\" ]; then\ fc -s -- \"$1\"\else\ echo \"Command with number $1 was not found in recent history\"\ exit 2\fi\\
注意:
\\使用以下命令给脚本添加可执行权限:
\\\chmod +x ./dothis\\
由于在后面的教程中将多次执行这个脚本,因此我建议将其放到系统所指定的目录中,这样我们就能够直接输入dothis来执行它。
\\我将这个脚本安装到了我的$HOME/bin目录中:
\\\install ./dothis ~/bin/dothis\\
如果您的系统中~/bin目录也在PATH环境变量中,也可以用这种方式安装。
\\现在让我们来验证脚本:
\\\dothis\\
我们应该可以看见这样的输出:
\\\$ dothis\No command number passed\\
搞定。
\\创建一个名为dothis-completion.bash的文件,为了方便描述,从现在开始称该文件为自动补全脚本。
\\一旦在该文件中添加了一些代码,我们都需要source它以生效。注意,后面每次修改文件之后,都需要source这个文件。
\\后续我们将讨论如何让这个自动补全脚本在bash每次打开时自动生效。
\\假设dothis应用支持一系列子命令,例如:
\\我们可以使用bash内置的complete命令来注册这个补全列表。用专业术语来说,我们通过complete命令为我们的应用定义了一个补全规范(completion specification,compspec)。
\\将以下内容添加到自动补全脚本中:
\\\#/usr/bin/env bash\complete -W \"now tomorrow never\" dothis\\
上述内容使用complete命令定义了:
\\前面提到过,每次编辑补全脚本后,都需要source该文件:
\\\source ./dothis-completion.bash\\
现在让我们尝试在命令行中敲击两次tab键:
\\\$ dothis \u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\never now tomorrow\\
再来试下输入字母n之后的效果:
\\\$ dothis n\u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\never now\\
神奇!补全列表自动过滤出了只以字母n开头的选项。
\\注意:补全参数列表显示的顺序和我们在补全脚本中定义的顺序不同,它们已经经过自动排序。
\\除了这里使用的-W参数之外,command命令还有许多其他参数。大部分参数都以固定的方式生成补全列表,这意味着我们无法动态干预过滤它们的输出结果。
\\例如,如果我们想将当前目录下的子目录名作为dothis应用程序的补全列表,可以将complete命令做如下修改:
\\\complete -A directory dothis\\
此时,在dothis命令之后敲tab键,我们可以获取当前目录下子目录的列表:
\\\$ dothis \u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\dir1/ dir2/ dir3/\\
更多关于complete命令的参数参见。
\\本小节中,我们将实现带有以下逻辑的dothis可执行程序的自动补全:
\\让我们从定义一个每次dothis命令补全时都会调用的函数。将补全脚本改成这样:
\\\#/usr/bin/env bash\_dothis_completions()\{\ COMPREPLY+=(\"now\")\ COMPREPLY+=(\"tomorrow\")\ COMPREPLY+=(\"never\")\}\ \complete -F _dothis_completions dothis\\
对该脚本的一些说明:
\\现在让我们重新source下补全脚本,验证下补全功能:
\\\$ dothis \u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\never now tomorrow\\
完美,补全脚本能够输出和之前一样的补全词列表。等等,好像不是?再来试下:
\\\$ dothis nev\u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\never now tomorrow\\
我们可以看到,虽然我们在输入了nev字母后再触发了自动补全,显示的补全列表和之前的一样并没有做自动过滤,这是为什么呢?
\\使用compgen命令:它是一个用于生成补全列表的内置命令,支持complete命令的大部分参数(例如-W参数指定补全词列表,-d参数补全目录),并能够基于用户已经输入的内容进行过滤。
\\如果有些迷惑也不用着急,下面通过一些命令及其输出来展示它的使用:
\\\$ compgen -W \"now tomorrow never\"\now\tomorrow\never\$ compgen -W \"now tomorrow never\" n\now\never\$ compgen -W \"now tomorrow never\" t\tomorrow\\
通过这些示例,我们已经可以使用该命令了,不过在此之前,还需要了解为获取dothis命令已经输入的内容。bash自动补全功能提供了相关以支撑这个自动补全。这里是一些比较重要的变量:
\\为了获取dothis命令后面的词,我们可以使用COMP_WORDS[1]的值。
\\再次修改自动补全脚本:
\\\#/usr/bin/env bash\_dothis_completions()\{\ COMPREPLY=($(compgen -W \"now tomorrow never\" \"${COMP_WORDS[1]}\"))\}\\complete -F _dothis_completions dothis\\
source该文件查看效果:
\\\$ dothis\never now tomorrow \$ dothis n\never now\\
现在,让我们抛开now、never、tomorrow这些词,从命令执行历史中抓取真实的数字。
\\fc -l命令后面增加一个负数-n可以显示最近执行过的n条命令。因此我们将会使用:
\\\fc -l -50\\
命令来显示执行历史中的最近50条命令以及它们的序号。这里我们唯一需要处理的是将原始命令输出的制表符替换成空格,以便于更好的展示。这个工作由sed来完成。
\\将自动补全脚本做如下改动:
\\\#/usr/bin/env bash\_dothis_completions()\{\ COMPREPLY=($(compgen -W \"$(fc -l -50 | sed 's/\\t//')\" -- \"${COMP_WORDS[1]}\"))\}\\complete -F _dothis_completions dothis\\
在控制台中source该脚本并验证:
\\\$ dothis \u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\632 source dothis-completion.bash 649 source dothis-completion.bash 666 cat ~/.bash_profile\633 clear 650 clear 667 cat ~/.bashrc\634 source dothis-completion.bash 651 source dothis-completion.bash 668 clear\635 source dothis-completion.bash 652 source dothis-completion.bash 669 install ./dothis ~/bin/dothis\636 clear 653 source dothis-completion.bash 670 dothis\637 source dothis-completion.bash 654 clear 671 dothis 6546545646\638 clear 655 dothis 654 672 clear\639 source dothis-completion.bash 656 dothis 631 673 dothis\640 source dothis-completion.bash 657 dothis 150 674 dothis 651\641 source dothis-completion.bash 658 dothis 675 source dothis-completion.bash\642 clear 659 clear 676 dothis 651\643 dothis 623 ls -la 660 dothis 677 dothis 659\644 clear 661 install ./dothis ~/bin/dothis 678 clear\645 source dothis-completion.bash 662 dothis 679 dothis 665\646 clear 663 install ./dothis ~/bin/dothis 680 clear\647 source dothis-completion.bash 664 dothis 681 clear\648 clear 665 cat ~/.bashrc\\
效果不错。但是还存在一个问题,当我们输入一个数字之后再按tab键,会出现:
\\\$ dothis 623\u0026lt;tab\u0026gt;\$ dothis 623 ls 623 ls -la\...\$ dothis 623 ls 623 ls 623 ls 623 ls 623 ls -la\\
出现这个问题是因为在自动补全脚本中,我们使用了${COMP_WORDS[1]}来获取dothis命令之后的第一个词(在上述代码片段中为623)。因此当tab键按下时,相同的自动补全列表会一再出现。
\\要修复这个问题,我们将在已经输入了至少一个参数之后,不再允许继续进行自动补全。因此需要在函数中增加对COMP_WORDS数组大小的前置判断:
\\\#/usr/bin/env bash\_dothis_completions()\{\ if [ \"${#COMP_WORDS[@]}\" != \"2\" ]; then\ return\ fi\\ COMPREPLY=($(compgen -W \"$(fc -l -50 | sed 's/\\t//')\" -- \"${COMP_WORDS[1]}\"))\}\\complete -F _dothis_completions dothis\\
source脚本并重试:
\\\$ dothis 623\u0026lt;tab\u0026gt;\$ dothis 623 ls -la\u0026lt;tab\u0026gt; # 成功:此时没有触发自动补全\\
当前脚本还有一个不尽如人意的地方。我们希望展示历史记录序号给用户的同时展示对应的命令,以帮助用户决定选择哪个历史命令。但是当补全建议中有且只有一个时候,应该能够通过自动补全机制自动选择,而不要追加命令文本。
\\因为dothis命令实际只接受一个表示执行历史序号的参数,并且没有对多余参数进行校验。当我们的自动补全函数计算出只有一个结果时,应该去除序号后面的命令文本,只返回命令序号。
\\为了实现这个功能,我们需要将compgen命令的返回值保存到数组变量中,并且检查当其大小,当大小为1时,去除这个唯一的值数字后面跟随的文本;否则直接返回这个数组。
\\将自动补全脚本修改成:
\\\#/usr/bin/env bash\_dothis_completions()\{\ if [ \"${#COMP_WORDS[@]}\" != \"2\" ]; then\ return\ fi\\ # keep the suggestions in a local variable\ local suggestions=($(compgen -W \"$(fc -l -50 | sed 's/\\t/ /')\" -- \"${COMP_WORDS[1]}\"))\\ if [ \"${#suggestions[@]}\" == \"1\" ]; then\ # if there's only one match, we remove the command literal\ # to proceed with the automatic completion of the number\ local number=$(echo ${suggestions[0]/%\\ */})\ COMPREPLY=(\"$number\")\ else\ # more than one suggestions resolved,\ # respond with the suggestions intact\ COMPREPLY=(\"${suggestions[@]}\")\ fi\}\\complete -F _dothis_completions dothis\\
如果我们希望将自动补全脚本应用到个人账户,可以在.bashrc文件中source这个脚本:
\\\source \u0026lt;path-to-your-script\u0026gt;/dothis-completion.bash\\
如果我们需要为机器上的所有用户启动这个自动补全脚本,可以将该脚本复制到/etc/bash_completion.d/目录中,这样bash会自动加载。
\\为了有更好的展示效果,额外增加几个步骤:)
\\在我实际工作中编写的bash自动补全脚本中,补全建议也由两部分组成。我希望能够将第一部分用默认颜色展示,而第二部分用灰色展示,以告知用户这仅仅是帮助文本。以本教程为例,应该把数字用默认颜色展示,而命令文本用另一个不那么花哨的颜色展示。
\\不幸的是,目前为止这个功能还无法实现,因为自动补全项仅仅以纯文本方式展示,而不会处理其中的颜色指令(例如:\\e[34mBlue)。
\\因此这里我们对于提升用户体验(也有可能没有提升:D)的方法是将每一个补全项换行显示。这个方案实现起来也没有那么方便,因为我们无法简单的通过在每个COMPREPLY项后追加换行符来实现。为了实现这个功能,这里采用了将补全建议文本填充到控制台的宽度。
\\通过printf命令可以实现将字符串填充到指定长度。如果需要这项功能,将自动补全脚本做如下修改:
\\\#/usr/bin/env bash\_dothis_completions()\{\ if [ \"${#COMP_WORDS[@]}\" != \"2\" ]; then\ return\ fi\\ local IFS=$'\'\ local suggestions=($(compgen -W \"$(fc -l -50 | sed 's/\\t//')\" -- \"${COMP_WORDS[1]}\"))\\ if [ \"${#suggestions[@]}\" == \"1\" ]; then\ local number=\"${suggestions[0]/%\\ */}\"\ COMPREPLY=(\"$number\")\ else\ for i in \"${!suggestions[@]}\"; do\ suggestions[$i]=\"$(printf '%*s' \"-$COLUMNS\" \"${suggestions[$i]}\")\"\ done\\ COMPREPLY=(\"${suggestions[@]}\")\ fi\}\\complete -F _dothis_completions dothis\\
source并验证:
\\\dothis \u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\...\499 source dothis-completion.bash \500 clear\... \503 dothis 500\\
在我们的之前的自动补全脚本中,将补全项数量写死了最后50个执行历史。这在实际使用中不太友好。我们应该让每个用户能够有自己的选择余地,如果他们没有选择,再使用默认值50。
\\为了实现这个功能,我们将检查是否设置了环境变量DOTHIS_COMPLETION_COMMANDS_NUMBER。
\\最后一次修改自动补全脚本:
\\\#/usr/bin/env bash\_dothis_completions()\{\ if [ \"${#COMP_WORDS[@]}\" != \"2\" ]; then\ return\ fi\\ local commands_number=${DOTHIS_COMPLETION_COMMANDS_NUMBER:-50}\ local IFS=$'\'\ local suggestions=($(compgen -W \"$(fc -l -$commands_number | sed 's/\\t//')\" -- \"${COMP_WORDS[1]}\"))\\ if [ \"${#suggestions[@]}\" == \"1\" ]; then\ local number=\"${suggestions[0]/%\\ */}\"\ COMPREPLY=(\"$number\")\ else\ for i in \"${!suggestions[@]}\"; do\ suggestions[$i]=\"$(printf '%*s' \"-$COLUMNS\" \"${suggestions[$i]}\")\"\ done\\ COMPREPLY=(\"${suggestions[@]}\")\ fi\}\\complete -F _dothis_completions dothis\\
source并验证:
\\\export DOTHIS_COMPLETION_COMMANDS_NUMBER=5\$ dothis \u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\505 clear\506 source ./dothis-completion.bash\507 dothis clear\508 clear\509 export DOTHIS_COMPLETION_COMMANDS_NUMBER=5\\
本教程源码位于。任何反馈、评论、勘误请在代码仓库中提交。
\\让我来介绍下我的调试器。
\\
\\(译者注:原作者特意嘱咐我们别忘了上猫照^_^)
\\查看英文原文:
\\感谢对本文的审校。
转载地址:http://jtino.baihongyu.com/