如何为脚本添加 bash 补全功能

2025-06-07

如何为脚本添加 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-

Bash 补全图像
您可以在 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

  • 我们使用了-Wwordlist)选项来提供需要补全的单词列表。
  • 我们定义了这些完成词将用于哪个“程序”(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_WORDScompspec:在所属程序的名称后输入的所有单词的数组
  • 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

来源就在这里:

 $ 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

有用的链接

代码

您可以在GitHub找到本教程的代码

这篇文章最初发表在我的博客上。

长帖,猫的照片

让我向你介绍我的调试器。

我的调试器

就这样吧,伙计们!

文章来源:https://dev.to/iridakos/adding-bash-completion-to-your-scripts-50da
PREV
撰写更好的电子邮件(来自软件开发领域的示例)
NEXT
经验丰富的程序员给代码新手的建议