如何创建 Bash 补全脚本

了解如何创建 Bash 脚本,以帮助用户更高效、更准确地工作。
403 位读者喜欢这篇文章。
What to like about COBOL

Rainer Gerhards。由 Opensource.com 修改。CC BY-SA 4.0

我最近为一个项目创建了一个 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-

dothis executable screen

您可以在 此教程在 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_CWORDCOMP_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

长文,猫片

让我向你介绍我的调试器

cat debugger

就这样,各位!

这篇文章最初发布在 Iridakos.com。经许可转载。

标签
User profile image.
我是一名软件开发人员。我曾在雅典经济与商业大学学习计算机科学,目前居住在希腊雅典。我通常使用 <strong>Ruby</strong> 编写代码,尤其是在 Rails 上,但我也精通 Java、Go、bash 和 C#。我热爱开源,喜欢编写教程以及创建工具和实用程序。

7 条评论

谢谢,这很有帮助 :)

哇,这很难理解...

不错,但看看 Fish。Fish 是一个用于现代系统的命令行 shell,专注于交互使用中的用户友好性、合理性和可发现性。它的工作方式类似于 Windows 中的 powershell。使用 Tab 键完成命令的下一部分。

非常棒且有用的文章。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 许可。
© . All rights reserved.