如何编写bash自动补全脚本

什么是bash的自动补全

\\

Bash自动补全是为了帮助用户能够更快、更容易输入命令的一项功能。它能够在用户输入命令时敲击tab键后,提供可能的选项。

\\
\$ git\u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\git                 git-receive-pack    git-upload-archive  \gitk                git-shell           git-upload-pack     \$ git-s\u0026lt;tab\u0026gt;\$ git-shell
\\

工作原理

\\

Bash补全脚本是一段使用bash内置命令command的代码,用于定义哪些补全建议可以对特定的可执行程序显示。这些补全建议既可以是简单的静态内容,也可以是高度复杂的。

\\

为什么要使用

\\

自动补全功能能够为用户提供以下便利:

\\
  • 当可以自动完成时,帮助用户减少文本输入;\\t
  • 让用户知道输入的命令后续可以有哪些可选的参数;\\t
  • 避免输入错误,同时通过用户已经输入的内容隐藏或者展示可选项以提高用户体验。\

开始上手

\\

下面我们将开始一个演示。

\\

首选,我们将会创建一个名为dothis的模拟可执行脚本。该脚本接受一个参数,表示用户执行历史中的序号,并执行序号对应的历史命令。例如,以下命令将会执行用户历史命令中序号为235的命令(我电脑上对应的是ls -a命令):

\\
\dothis 235
\\

然后,我们将创建一个bash自动补全脚本,用以展示用户历史命令信息,并和dothis命令“绑定”起来。

\\
\$ dothis \u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\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\u0026gt;/dev/null)\\if [ -n \"$exists\" ]; then\  fc -s -- \"$1\"\else\  echo \"Command with number $1 was not found in recent history\"\  exit 2\fi
\\

注意:

\\
  • 脚本首先检查调用时是否跟随这一个参数。\\t
  • 检查输入的数字是否在最近1000个命令中:\\t
    • 如果存在则使用fc命令执行对应的命令;\\t\t
    • 如果不存在则显示错误信息。\\t
    \

使用以下命令给脚本添加可执行权限:

\\
\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每次打开时自动生效。

\\

静态补全

\\

假设dothis应用支持一系列子命令,例如:

\\
  • now\\t
  • tomorrow\\t
  • never\

我们可以使用bash内置的complete命令来注册这个补全列表。用专业术语来说,我们通过complete命令为我们的应用定义了一个补全规范(completion specification,compspec)。

\\

将以下内容添加到自动补全脚本中:

\\
\#/usr/bin/env bash\complete -W \"now tomorrow never\" dothis
\\

上述内容使用complete命令定义了:

\\
  • 通过-W参数提供了补全词列表;\\t
  • 指定该补全词列表适用的应用程序(这里作为dothis命令参数)。\

前面提到过,每次编辑补全脚本后,都需要source该文件:

\\
\source ./dothis-completion.bash
\\

现在让我们尝试在命令行中敲击两次tab键:

\\
\$ dothis \u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\never     now       tomorrow
\\

再来试下输入字母n之后的效果:

\\
\$ dothis n\u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\never now
\\

神奇!补全列表自动过滤出了只以字母n开头的选项。

\\

注意:补全参数列表显示的顺序和我们在补全脚本中定义的顺序不同,它们已经经过自动排序。

\\

除了这里使用的-W参数之外,command命令还有许多其他参数。大部分参数都以固定的方式生成补全列表,这意味着我们无法动态干预过滤它们的输出结果。

\\

例如,如果我们想将当前目录下的子目录名作为dothis应用程序的补全列表,可以将complete命令做如下修改:

\\
\complete -A directory dothis
\\

此时,在dothis命令之后敲tab键,我们可以获取当前目录下子目录的列表:

\\
\$ dothis \u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\dir1/ dir2/ dir3/
\\

更多关于complete命令的参数参见这里

\\

动态补全

\\

本小节中,我们将实现带有以下逻辑的dothis可执行程序的自动补全:

\\
  • 如果用户在命令后面直接按tab键,将显示用户执行历史中的最近50个命令。\\t
  • 如果用户在输入一个能够从执行历史中匹配到多个命令的数字后按tab键,将显示这些命令以及它们的序号。\\t
  • 如果用户在输入一个从执行历史中只能匹配到一个命令的数字后按tab键,将自动补全这个数字,而不显示命令内容(如果这个描述有些迷糊,看了后面的内容会能够有更好的理解,放心)。\

让我们从定义一个每次dothis命令补全时都会调用的函数。将补全脚本改成这样:

\\
\#/usr/bin/env bash\_dothis_completions()\{\  COMPREPLY+=(\"now\")\  COMPREPLY+=(\"tomorrow\")\  COMPREPLY+=(\"never\")\}\ \complete -F _dothis_completions dothis
\\

对该脚本的一些说明:

\\
  • 我们使用complete命令的-F参数定义_dothis_completions函数为dothis命令提供补全功能。\\t
  • COMPREPLY是一个存储补全列表的数组,自动补全机制使用该变量来显示补全内容。\

现在让我们重新source下补全脚本,验证下补全功能:

\\
\$ dothis \u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\never now tomorrow
\\

完美,补全脚本能够输出和之前一样的补全词列表。等等,好像不是?再来试下:

\\
\$ dothis nev\u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\never     now       tomorrow
\\

我们可以看到,虽然我们在输入了nev字母后再触发了自动补全,显示的补全列表和之前的一样并没有做自动过滤,这是为什么呢?

\\
  • COMPREPLY变量的内容总是会显示,补全函数需要自己处理其中的内容。\\t
  • 如果COMPREPLY变量中只有一个元素,那么这个词会自动补全到命令之后。由于目前的实现总是返回相同的三个词,不会触发这个功能。\

使用compgen命令:它是一个用于生成补全列表的内置命令,支持complete命令的大部分参数(例如-W参数指定补全词列表,-d参数补全目录),并能够基于用户已经输入的内容进行过滤。

\\

如果有些迷惑也不用着急,下面通过一些命令及其输出来展示它的使用:

\\
\$ 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自动补全功能提供了相关变量以支撑这个自动补全。这里是一些比较重要的变量:

\\
  • COMP_WORDS:当前命令行中已经输入的词数组。\\t
  • COMP_CWORD:当前光标所处词位于COMP_WORDS数组中的索引值。既当按下tab键时光标所处词的索引。\\t
  • 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、never、tomorrow这些词,从命令执行历史中抓取真实的数字。

\\

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 \u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\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
\\

效果不错。但是还存在一个问题,当我们输入一个数字之后再按tab键,会出现:

\\
\$ dothis 623\u0026lt;tab\u0026gt;\$ 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\u0026lt;tab\u0026gt;\$ dothis 623 ls -la\u0026lt;tab\u0026gt; # 成功:此时没有触发自动补全
\\

当前脚本还有一个不尽如人意的地方。我们希望展示历史记录序号给用户的同时展示对应的命令,以帮助用户决定选择哪个历史命令。但是当补全建议中有且只有一个时候,应该能够通过自动补全机制自动选择,而不要追加命令文本

\\

因为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 \u0026lt;path-to-your-script\u0026gt;/dothis-completion.bash
\\

如果我们需要为机器上的所有用户启动这个自动补全脚本,可以将该脚本复制到/etc/bash_completion.d/目录中,这样bash会自动加载。

\\

最后调优

\\

为了有更好的展示效果,额外增加几个步骤:)

\\

在新行中展示每个条目

\\

在我实际工作中编写的bash自动补全脚本中,补全建议也由两部分组成。我希望能够将第一部分用默认颜色展示,而第二部分用灰色展示,以告知用户这仅仅是帮助文本。以本教程为例,应该把数字用默认颜色展示,而命令文本用另一个不那么花哨的颜色展示。

\\

不幸的是,目前为止这个功能还无法实现,因为自动补全项仅仅以纯文本方式展示,而不会处理其中的颜色指令(例如:\\e[34mBlue)。

\\

因此这里我们对于提升用户体验(也有可能没有提升:D)的方法是将每一个补全项换行显示。这个方案实现起来也没有那么方便,因为我们无法简单的通过在每个COMPREPLY项后追加换行符来实现。为了实现这个功能,这里采用了hach的方式将补全建议文本填充到控制台的宽度。

\\

通过printf命令可以实现将字符串填充到指定长度。如果需要这项功能,将自动补全脚本做如下修改:

\\
\#/usr/bin/env bash\_dothis_completions()\{\  if [ \"${#COMP_WORDS[@]}\" != \"2\" ]; then\    return\  fi\\  local IFS=$'\'\  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 \u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\...\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=$'\'\  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 \u0026lt;tab\u0026gt;\u0026lt;tab\u0026gt;\505 clear\506 source ./dothis-completion.bash\507 dothis clear\508 clear\509 export DOTHIS_COMPLETION_COMMANDS_NUMBER=5
\\

有用的链接

\\

源码和评论

\\

本教程源码位于GitHub。任何反馈、评论、勘误请在代码仓库中提交issue

\\

结尾,上猫照

\\

让我来介绍下我的调试器。

\\

b0921e909d40a3c70897f32c4b954898.jpg

\\

(译者注:原作者特意嘱咐我们别忘了上猫照^_^)

\\

查看英文原文:Creating a bash completion script

\\

感谢张婵对本文的审校。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值