我最近为一个项目创建了一个 Bash 补全脚本,并且非常享受这个过程。在这篇文章中,我将尝试让你熟悉创建 Bash 补全脚本的过程。
什么是 Bash 补全?
Bash 补全是一种功能,Bash 通过它帮助用户更快、更轻松地键入命令。它通过在用户键入命令时按下 Tab 键时显示可能的选项来实现这一点。
$ git<tab><tab>
git git-receive-pack git-upload-archive
gitk git-shell git-upload-pack
$ git-s<tab>
$ git-shell
工作原理
补全脚本是使用内置 Bash 命令 complete
的代码,用于定义可以为给定的可执行文件显示的哪些补全建议。补全选项的性质各不相同,从简单的静态到高度复杂的都有。
为什么要费心?
此功能通过以下方式帮助用户:
- 在可以自动补全时,节省他们键入文本的时间
- 帮助他们了解命令的可用延续
- 通过根据他们已键入的内容隐藏或显示选项,防止错误并改善他们的体验
动手实践
这是我们将在本教程中执行的操作
我们将首先创建一个名为 dothis
的虚拟可执行脚本。它所做的只是执行用户历史记录中作为参数传递的数字所对应的命令。例如,以下命令将简单地执行 ls -a
命令,假设它在历史记录中的编号为 235
dothis 235
然后,我们将创建一个 Bash 补全脚本,该脚本将显示用户历史记录中的命令及其编号,并将它“绑定”到 dothis
可执行文件。
$ dothis <tab><tab>
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>/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
我们将在本教程中多次执行此脚本,因此我建议你将其放在包含在你的 路径 中的文件夹中,以便我们可以通过键入 dothis
从任何位置访问它。
我使用以下命令将其安装在我的 home bin 文件夹中
install ./dothis ~/bin/dothis
假设你有一个 ~/bin
文件夹并且它包含在你的 PATH
变量中,你可以执行相同的操作。
检查它是否正常工作
dothis
你应该看到这个
$ dothis
No command number passed
完成。
创建补全脚本
创建一个名为 dothis-completion.bash
的文件。从现在开始,我们将使用术语补全脚本来指代此文件。
一旦我们向其中添加一些代码,我们将 source
它以使补全生效。每次更改其中的内容时,我们都必须 source
此文件。
在本教程的后面,我们将讨论在每次打开 Bash shell 时注册此脚本的选项。
静态补全
假设 dothis
程序支持命令列表,例如
现在
明天
从不
让我们使用 complete
命令注册此列表以进行补全。为了使用正确的术语,我们说我们使用 complete
命令为我们的程序定义补全规范 (compspec)。
将此添加到补全脚本。
#/usr/bin/env bash
complete -W "now tomorrow never" dothis
以下是我们使用上面的 complete
命令指定的
- 我们使用
-W
(wordlist) 选项来提供用于补全的单词列表。 - 我们定义了这些补全词将用于哪个“程序”(
dothis
参数)
source 该文件
source ./dothis-completion.bash
现在尝试在命令行中按两次 Tab 键,如下所示
$ dothis <tab><tab>
never now tomorrow
在键入 n
后再次尝试
$ dothis n<tab><tab>
never now
神奇!补全选项会自动过滤,只匹配以 n
开头的选项。
注意: 选项不是按照我们在单词列表中定义的顺序显示的;它们是自动排序的。
还有许多其他选项可以代替我们在本节中使用的 -W
。大多数选项都以固定的方式生成补全,这意味着我们不会动态干预来过滤它们的输出。
例如,如果我们希望将目录名称作为 dothis
程序的补全词,我们将把 complete 命令更改为以下内容
complete -A directory dothis
在 dothis
程序之后按 Tab 键将获得一个目录列表,这些目录位于我们执行脚本的当前目录中
$ dothis <tab><tab>
dir1/ dir2/ dir3/
在 Bash 参考手册 中查找可用标志的完整列表。
动态补全
我们将使用以下逻辑生成 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 <tab><tab>
never now tomorrow
完美。我们生成了与上一节中使用单词列表相同的补全。或者不是?试试这个
$ dothis nev<tab><tab>
never now tomorrow
正如你所看到的,即使我们键入 nev 然后请求补全,可用的选项始终相同,并且没有任何内容被自动补全。为什么会这样?
COMPREPLY
变量的内容始终显示。该函数现在负责从中添加/删除条目。- 如果
COMPREPLY
变量只有一个元素,那么该单词将在命令中自动补全。由于当前实现始终返回相同的三个单词,因此这不会发生。
输入 compgen
:一个内置命令,用于生成补全,支持 complete
命令的大多数选项(例如 -W
用于单词列表,-d
用于目录),并根据用户已键入的内容对其进行过滤。
如果你感到困惑,请不要担心;一切都会在稍后变得清晰。
在控制台中键入以下内容以更好地理解 compgen
的作用
$ 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 补全工具提供了与正在进行的补全相关的 Bash 变量。以下是更重要的变量
COMP_WORDS
:一个数组,包含程序名称后键入的所有单词,compspec
属于该程序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
现在,我们希望看到的不是单词现在、明天、从不,而是命令历史记录中的实际数字。
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 <tab><tab>
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
还不错。
但是,我们确实遇到了一个问题。尝试键入你在补全列表中看到的数字,然后再次按键。
$ dothis 623<tab>
$ 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<tab>
$ dothis 623 ls -la<tab> # SUCCESS: nothing happens here
还有一件事我们不喜欢。我们确实希望显示数字以及相应的命令,以帮助用户决定想要哪个命令,但是当只有一个补全建议并且它被补全机制自动选中时,我们不应该也附加命令字面量。
换句话说,我们的 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 <path-to-your-script>/dothis-completion.bash
如果你想为所有用户启用补全,你可以将脚本复制到 /etc/bash_completion.d/
下,它将由 Bash 自动加载。
微调补全脚本
以下是一些额外的步骤,可以获得更好的结果
在新行中显示每个条目
在我正在处理的 Bash 补全脚本中,我也必须呈现由两部分组成的建议。我希望以默认颜色显示第一部分,以灰色显示第二部分,以将其区分为帮助文本。在本教程的示例中,最好以默认颜色呈现数字,并以不太花哨的颜色呈现命令字面量。
不幸的是,这是不可能的,至少目前是这样,因为补全显示为纯文本,并且颜色指令未被处理(例如:\e[34mBlue
)。
我们可以做些什么来改善用户体验(或不改善)是在新行中显示每个条目。此解决方案不是很明显,因为我们不能只是在每个 COMPREPLY
条目中附加换行符。我们将遵循一种相当 hackish 的方法,并将建议字面量填充到填充终端的宽度。
输入 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 <tab><tab>
...
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 <tab><tab>
505 clear
506 source ./dothis-completion.bash
507 dothis clear
508 clear
509 export DOTHIS_COMPLETION_COMMANDS_NUMBER=5
有用的链接
代码和评论
你可以在 GitHub 上找到本教程的代码。
对于反馈、评论、错别字等,请在仓库中打开一个 issue。
长文,猫片
让我向你介绍我的调试器

就这样,各位!
这篇文章最初发布在 Iridakos.com。经许可转载。
7 条评论