如何为脚本添加 bash 补全功能
我最近在编写一个 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
。
我使用以下命令将它安装在我的主 bin文件夹中:
install ./dothis ~/bin/dothis
~/bin
如果您有一个文件夹并且它包含在您的PATH
变量中,您可以执行相同的操作。
尝试看看它是否有效:
dothis
你应该看看这个。
$ dothis
No command number passed
完毕。
创建完成脚本
创建一个名为 的文件dothis-completion.bash
。从现在开始,我们将使用术语“完成脚本”来指代此文件。
一旦我们添加了一些代码,我们就会source
让它生效。每次修改这个文件时,
我们都必须修改它。source
稍后我们将讨论在打开 bash shell 时注册此脚本的选项。
静态完成
假设dothis
程序支持一系列命令,例如:
now
tomorrow
never
让我们使用complete
命令来注册这个列表以进行补全。更确切地说,我们说使用命令来为我们的程序complete
定义一个补全规范(compspec )。
将其添加到完成脚本。
#/usr/bin/env bash
complete -W "now tomorrow never" dothis
我们用上面的命令指定了什么complete
:
- 我们使用了
-W
(wordlist)选项来提供需要补全的单词列表。 - 我们定义了这些完成词将用于哪个“程序”(
dothis
参数)
获取文件来源:
source ./dothis-completion.bash
现在尝试在命令行中按两次 Tab 键,如下所示:
$ dothis <tab><tab>
never now tomorrow
输入以下内容后重试n
:
$ dothis n<tab><tab>
never now
太神奇了!补全选项会自动过滤,只匹配以 开头的选项n
。
注意:
选项不是按照我们在单词列表中定义它们的顺序显示的,而是自动排序的。
除了本节中使用的选项外,还有很多其他选项可以替代-W
。它们中的大多数以固定的方式生成补全,这意味着我们不会动态干预来过滤它们的输出。
例如,如果我们想使用目录名称作为dothis
程序的完成词,我们可以将完整命令更改为以下内容:
complete -A directory dothis
按下dothis
程序后的 Tab 键,我们将获得当前目录中执行脚本的目录列表:
$ dothis <tab><tab>
dir1/ dir2/ dir3/
在此处找到可用标志的完整列表。
动态完成
dothis
我们将按照以下逻辑完成可执行文件:
- 如果用户在命令后立即按下 Tab 键,我们将显示最后执行的 50 个命令及其在历史记录中的编号
- 如果用户在输入与历史记录中的多个命令匹配的数字后按下 Tab 键,我们将仅显示这些命令及其在历史记录中的编号
- 如果用户在与历史记录中的一个命令完全匹配的数字后按下 Tab 键,我们会自动完成该数字而不附加命令的文字(如果它令人困惑,您以后会更好地理解,不用担心)
让我们首先定义一个函数,该函数将在每次用户请求完成dothis
命令时执行。将完成脚本更改为以下内容:
#/usr/bin/env bash
_dothis_completions()
{
COMPREPLY+=("now")
COMPREPLY+=("tomorrow")
COMPREPLY+=("never")
}
complete -F _dothis_completions dothis
请注意以下事项:
- 我们
-F
在完整命令中使用了标志,定义了将提供可执行文件_dothis_completions
完成的函数dothis
COMPREPLY
是用于存储完成的数组变量 - 完成机制使用此变量将其内容显示为完成
现在获取脚本并完成:
$ 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
来源就在这里:
$ 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
在控制台中获取源代码并测试:
$ 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
来源并重试。
$ 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 <path-to-your-script>/dothis-completion.bash
如果您想为所有用户启用完成功能,您只需复制下面的脚本/etc/bash_completion.d/
,它将被 Bash 自动加载。
微调完成脚本
为了获得更好的结果,需要采取一些额外的步骤:)
在新行中显示每个条目
嗯,在我编写的 Bash 补全脚本中,我也需要呈现由两部分组成的建议。我希望第一部分用默认颜色显示,第二部分用灰色显示,以指出这只是帮助文本。在本教程的示例中,最好用默认颜色显示数字,而用另一种不那么花哨的颜色显示命令文字。
不幸的是,至少目前这是不可能的,因为完成显示为纯文本并且不处理颜色指令(例如\e[34mBlue
:)。
不过,为了提升用户体验(或者说不提升用户体验 :D),我们可以将每个条目显示在新行中。这个解决方案不太明显,因为我们不能直接在每个COMPREPLY
条目后添加一个新行符。我们将采用一种比较hack 的方式,将建议字面量填充到能够填满终端宽度的宽度。
输入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
来源及测试:
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
来源及测试:
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
有用的链接
- Git 的完成脚本
goto
我的一个 shell 脚本的 Bash 补全- Bash 参考手册:可编程补全
- Bash 参考手册:可编程补全内置函数
- Bash 参考手册:可编程补全示例
- Bash 参考手册:Bash 变量
代码
您可以在GitHub上找到本教程的代码。
这篇文章最初发表在我的博客上。
长帖,猫的照片
让我向你介绍我的调试器。
就这样吧,伙计们!
文章来源:https://dev.to/iridakos/adding-bash-completion-to-your-scripts-50da