如何创建 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

我们将在本教程中多次执行此脚本,因此我建议您将其放置在包含在您的 path 中的文件夹中,以便我们可以通过键入 dothis 从任何地方访问它。

我使用以下命令将其安装在我的 home 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 该文件

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

现在,我们希望看到来自命令历史记录的实际数字,而不是单词now、tomorrow、never

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 键完成命令的下一部分。

非常棒且有用的文章。

© . All rights reserved.