Bash 自动补全是为了帮助用户能够更快、更容易输入命令的一项功能。它能够在用户输入命令时敲击tab
键后,提供可能的选项。
$ git
git git-receive-pack git-upload-archive
gitk git-shell git-upload-pack
$ git-s
$ git-shell
Bash 补全脚本是一段使用 bash 内置命令 command 的代码,用于定义哪些补全建议可以对特定的可执行程序显示。这些补全建议既可以是简单的静态内容,也可以是高度复杂的。
自动补全功能能够为用户提供以下便利:
当可以自动完成时,帮助用户减少文本输入;
让用户知道输入的命令后续可以有哪些可选的参数;
避免输入错误,同时通过用户已经输入的内容隐藏或者展示可选项以提高用户体验。
下面我们将开始一个演示。
首选,我们将会创建一个名为dothis
的模拟可执行脚本。该脚本接受一个参数,表示用户执行历史中的序号,并执行序号对应的历史命令。例如,以下命令将会执行用户历史命令中序号为235
的命令(我电脑上对应的是ls -a
命令):
dothis 235
然后,我们将创建一个 bash 自动补全脚本,用以展示用户历史命令信息,并和dothis
命令“绑定”起来。
$ dothis
215 ls
216 ls -la
217 cd ~
218 man history
219 git status
220 history | cut -c 8-
读者可以在位于 GitHub 上的本教程代码仓库中看见 gif 演示动图:https://github.com/iridakos/bash-completion-tutorial
现在让我们开始吧。
在工作目录中创建名为dothis
的文件,并添加以下代码:
if [ -z "$1" ]; then
echo "No command number passed"
exit 2
fi
exists=$(fc -l -1000 | grep ^$1 -- 2>/dev/null)
if [ -n "$exists" ]; then
fc -s -- "$1"
else
echo "Command with number $1 was not found in recent history"
exit 2
fi
注意:
脚本首先检查调用时是否跟随这一个参数。
检查输入的数字是否在最近 1000 个命令中:
如果存在则使用fc
命令执行对应的命令;
如果不存在则显示错误信息。
使用以下命令给脚本添加可执行权限:
chmod +x ./dothis
由于在后面的教程中将多次执行这个脚本,因此我建议将其放到系统 PATH 环境变量 (http://www.linfo.org/path_env_var.html) 所指定的目录中,这样我们就能够直接输入dothis
来执行它。
我将这个脚本安装到了我的 $HOME/bin 目录中:
install ./dothis ~/bin/dothis
如果您的系统中 ~/bin 目录也在 PATH 环境变量中,也可以用这种方式安装。
现在让我们来验证脚本:
dothis
我们应该可以看见这样的输出:
$ dothis
No command number passed
搞定。
创建一个名为dothis-completion.bash
的文件,为了方便描述,从现在开始称该文件为自动补全脚本。
一旦在该文件中添加了一些代码,我们都需要source
它以生效。注意,后面 每次修改文件 之后,都需要source
这个文件。
后续我们将讨论如何让这个自动补全脚本在 bash 每次打开时自动生效。
假设dothis
应用支持一系列子命令,例如:
now
tomorrow
never
我们可以使用 bash 内置的complete
命令来注册这个补全列表。用专业术语来说,我们通过complete
命令为我们的应用定义了一个补全规范(completion specification,compspec)。
将以下内容添加到自动补全脚本中:
#/usr/bin/env bash
complete -W "now tomorrow never" dothis
上述内容使用complete
命令定义了:
通过-W
参数提供了补全词列表;
指定该补全词列表适用的应用程序(这里作为dothis
命令参数)。
前面提到过,每次编辑补全脚本后,都需要 source 该文件:
source ./dothis-completion.bash
现在让我们尝试在命令行中敲击两次 tab 键:
$ dothis
never now tomorrow
再来试下输入字母 n 之后的效果:
$ dothis n
never now
神奇!补全列表自动过滤出了只以字母 n 开头的选项。
注意:补全参数列表显示的顺序和我们在补全脚本中定义的顺序不同,它们已经经过自动排序。
除了这里使用的-W
参数之外,command 命令还有许多其他参数。大部分参数都以固定的方式生成补全列表,这意味着我们无法动态干预过滤它们的输出结果。
例如,如果我们想将当前目录下的子目录名作为dothis
应用程序的补全列表,可以将 complete 命令做如下修改:
complete -A directory dothis
此时,在 dothis 命令之后敲 tab 键,我们可以获取当前目录下子目录的列表:
$ dothis
dir1/ dir2/ dir3/
更多关于complete
命令的参数参见这里: https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html#Programmable-Completion-Builtins
本小节中,我们将实现带有以下逻辑的dothis
可执行程序的自动补全:
如果用户在命令后面直接按 tab 键,将显示用户执行历史中的最近 50 个命令。
如果用户在输入一个能够从执行历史中匹配到多个命令的数字后按 tab 键,将显示这些命令以及它们的序号。
如果用户在输入一个从执行历史中只能匹配到一个命令的数字后按 tab 键,将自动补全这个数字,而不显示命令内容(如果这个描述有些迷糊,看了后面的内容会能够有更好的理解,放心)。
让我们从定义一个每次dothis
命令补全时都会调用的函数。将补全脚本改成这样:
#/usr/bin/env bash
_dothis_completions()
{
COMPREPLY+=("now")
COMPREPLY+=("tomorrow")
COMPREPLY+=("never")
}
complete -F _dothis_completions dothis
对该脚本的一些说明:
我们使用complete
命令的-F
参数定义 _dothis_completions
函数为dothis
命令提供补全功能。
COMPREPLY
是一个存储补全列表的数组,自动补全机制使用该变量来显示补全内容。
现在让我们重新 source 下补全脚本,验证下补全功能:
$ dothis
never now tomorrow
完美,补全脚本能够输出和之前一样的补全词列表。等等,好像不是?再来试下:
$ dothis nev
never now tomorrow
我们可以看到,虽然我们在输入了 nev 字母后再触发了自动补全,显示的补全列表和之前的一样并没有做自动过滤,这是为什么呢?
COMPREPLY
变量的内容总是会显示,补全函数需要自己处理其中的内容。
如果COMPREPLY
变量中只有一个元素,那么这个词会自动补全到命令之后。由于目前的实现总是返回相同的三个词,不会触发这个功能。
使用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 自动补全功能提供了相关变量以支撑这个自动补全。这里是一些比较重要的变量:
COMP_WORDS
:当前命令行中已经输入的词数组。
COMP_CWORD
:当前光标所处词位于 COMP_WORDS 数组中的索引值。既当按下 tab 键时光标所处词的索引。
COMP_LINE
:当前命令行。
为了获取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
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
$ 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
$ dothis 623 ls -la # 成功:此时没有触发自动补全
当前脚本还有一个不尽如人意的地方。我们希望展示历史记录序号给用户的同时展示对应的命令,以帮助用户决定选择哪个历史命令。但是当补全建议中有且只有一个时候,应该能够通过自动补全机制自动选择,而 不要追加命令文本。
因为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 /dothis-completion.bash
如果我们需要为机器上的所有用户启动这个自动补全脚本,可以将该脚本复制到/etc/bash_completion.d/
目录中,这样 bash 会自动加载。
为了有更好的展示效果,额外增加几个步骤:)
在我实际工作中编写的 bash 自动补全脚本中,补全建议也由两部分组成。我希望能够将第一部分用默认颜色展示,而第二部分用灰色展示,以告知用户这仅仅是帮助文本。以本教程为例,应该把数字用默认颜色展示,而命令文本用另一个不那么花哨的颜色展示。
不幸的是,目前为止这个功能还无法实现,因为自动补全项仅仅以纯文本方式展示,而不会处理其中的颜色指令(例如:\e[34mBlue
)。
因此这里我们对于提升用户体验(也有可能没有提升:D)的方法是将每一个补全项换行显示。这个方案实现起来也没有那么方便,因为我们无法简单的通过在每个COMPREPLY
项后追加换行符来实现。为了实现这个功能,这里采用了 hach 的方式 (https://unix.stackexchange.com/questions/166908/is-there-anyway-to-get-compreply-to-be-output-as-a-vertical-list-of-words-instea) 将补全建议文本填充到控制台的宽度。
通过printf
命令可以实现将字符串填充到指定长度。如果需要这项功能,将自动补全脚本做如下修改:
#/usr/bin/env bash
_dothis_completions()
{
if [ "${#COMP_WORDS[@]}" != "2" ]; then
return
fi
local IFS=$'\n'
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
...
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=$'\n'
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
505 clear
506 source ./dothis-completion.bash
507 dothis clear
508 clear
509 export DOTHIS_COMPLETION_COMMANDS_NUMBER=5
一些有用的链接:
Git 的自动补全脚本:https://github.com/git/git/blob/master/contrib/completion/git-completion.bash
我自己编写的脚本 goto 的补全脚本:https://github.com/iridakos/goto/blob/master/goto.sh
Bash 参考手册:可编程自动补全:https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html#Programmable-Completion
Bash 参考手册:可编程自动补全内置支持:https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html#Programmable-Completion-Builtins
Bash 参考手册:可编程自动补全示例:https://www.gnu.org/software/bash/manual/html_node/A-Programmable-Completion-Example.html#A-Programmable-Completion-Example
Bash 参考手册:Bash 变量:https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html#Bash-Variables
本教程源码位于 GitHub。任何反馈、评论、勘误请在代码仓库中提交 issue:https://github.com/iridakos/bash-completion-tutorial
结尾,上猫照
让我来介绍下我的调试器。
(译者注:原作者特意嘱咐我们别忘了上猫照 ^_^)
原文链接:https://shimo.im/docs/nibI7cQIB4oelwBZ/
活动推荐
现在业务越来越复杂,用户的需求也越来越多样化,AIOps、DevOps、云服务、AI 算法、Kubernetes、SRE 等技术将成为运维人的核心竞争力。
CNUTCon 全球运维技术大会邀请了来自 Twitter、百度、阿里、腾讯、华为、京东、美团、网易、eBay、ThoughtWorks 等公司的技术专家,分享智能时代下运维的新趋势、新思路、新技术和实战经验,向你系统阐述在落地 AIOps 等相关技术的实践中,遇到的问题和对应的解决方案。
目前,大会 8 折限时优惠,立减 720 元,团购更优惠!有任何问题欢迎咨询票务经理 Joy,电话:13269078023(微信同号)。