写点什么

如何编写 bash 自动补全脚本

  • 2018-09-03
  • 本文字数:12524 字

    阅读完需:约 41 分钟

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

为什么要使用

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

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

开始上手

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

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

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

由于在后面的教程中将多次执行这个脚本,因此我建议将其放到系统 PATH 环境变量所指定的目录中,这样我们就能够直接输入dothis来执行它。

我将这个脚本安装到了我的$HOME/bin目录中:

install ./dothis ~/bin/dothis

如果您的系统中~/bin目录也在PATH环境变量中,也可以用这种方式安装。

现在让我们来验证脚本:

dothis

我们应该可以看见这样的输出:

$ dothis
No command number passed

搞定。

创建自动补全脚本

创建一个名为dothis-completion.bash的文件,为了方便描述,从现在开始称该文件为自动补全脚本。

一旦在该文件中添加了一些代码,我们都需要source它以生效。注意,后面每次修改文件之后,都需要source这个文件。

后续我们将讨论如何让这个自动补全脚本在 bash 每次打开时自动生效。

静态补全

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

  • now
  • tomorrow
  • never

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

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

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

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

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

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

source ./dothis-completion.bash

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

$ dothis <tab><tab>
never     now       tomorrow

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

$ dothis n<tab><tab>
never now

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

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

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

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

complete -A directory dothis

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

$ dothis <tab><tab>
dir1/ dir2/ dir3/

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

动态补全

本小节中,我们将实现带有以下逻辑的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 -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:当前命令行中已经输入的词数组。
  • 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

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 <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

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

$ 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> # 成功:此时没有触发自动补全

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

因为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)。

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

通过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

结尾,上猫照

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



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

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

感谢张婵对本文的审校。

2018-09-03 18:215678

评论

发布
暂无评论
发现更多内容

豆包模型能力大幅提升,中国版Vision Pro正式发布!今年大模型市场份额第一还会是百度吗?|AI日报

可信AI进展

人工智能

高性能桌面管理系统助力实现国产化生态!

上海锐起科技

RPA技术:基本概念和应用场景的全面指南

八爪鱼采集器︱RPA机器人

RPA 自动化 RPAxAI

MobPush推送查询

MobTech袤博科技

Java 开发者 产品动态

《 黑神话 · 悟空》视觉震撼背后的技术力量:如何用云桌面加速 CG 视觉创作 !

Finovy Cloud

游戏开发 游戏 黑神话悟空 黑神话

国家下达绿色转型目标!电子签章领域未来的发展趋势如何?

Geek_2a38d5

利用API返回值实现商品信息的自动化更新

技术冰糖葫芦

API Explorer API 测试 API 策略 pinduoduo API

公司最大的内卷,偷偷做单元测试

禅道项目管理

项目管理 程序员 软件测试 单元测试 测试人员

热仿真 散热仿真分析公司-CAE软件服务代做外包

Geek_2d6073

火山引擎VeDI实验平台助推企业量化决策能力升级

字节跳动数据平台

大数据 A/B 测试 对比实验 数字化增长

DPDK简介和原理

天翼云开发者社区

DPDK

金融企业区域集中库的设计构想和测试验证

TiDB 社区干货传送门

生成式AI已融入你的生活和工作了吗?

天津汇柏科技有限公司

人工智能 生成式AI 生成式 AI 应用

RPA实施的四大阶段:一步步的详细指南

八爪鱼采集器︱RPA机器人

RPA 自动化 机器人 RPAxAI

离奇问题,网络故障恢复后,无法重连到数据库?

中原银行

Java TCP 容器云 HikariCP 网络故障

国产RPA软件的优势:企业数字化转型中的关键作用详解

八爪鱼采集器︱RPA机器人

RPA 自动化 RPAxAI

Percona Toolkit 神器全攻略(开发类)

GreatSQL

Arbitrum STIP 陷尴尬局面:8500 万美元支出仅换回 1500 万美元收入

TechubNews

2024即刻职达人才生态合作大会于珠海横琴成功召开,共话数智时代人力资源新趋势

新消费日报

作业帮 & TiDB 7.5.x 使用经验

TiDB 社区干货传送门

7.x 实践

亿玛科技:TiDB 6.1.5 升级到 7.5.1 经验分享

TiDB 社区干货传送门

版本升级 7.x 实践

中国电信公布2024年中期业绩!

天翼云开发者社区

云计算 中国电信

RPA机器人流程自动化的5个必知关键点

八爪鱼采集器︱RPA机器人

RPA 自动化 RPAxAI

聊聊TiCDC

TiDB 社区干货传送门

7.x 实践

杭州百腾教育科技 TiDB 6.5 to 7.5 升级记录

TiDB 社区干货传送门

版本升级 7.x 实践

【喜讯】数业智能当选“广东省卫生信息网络协会”理事单位

心大陆多智能体

智能体 AI大模型 心理健康 数字心理

IPQ5332 vs. IPQ4019: The Best WiFi Solution for Oil and Gas Industries

wallyslilly

IPQ4019 IPQ5332

这是一款轻量存储黑科技!

天翼云开发者社区

云计算 天翼云 轻量存储

SDN网络技术在云计算中的应用

天翼云开发者社区

SDN网络

如何提高研发效能?思码逸 & 信通院告诉你

思码逸研发效能

团队管理 DevOps #研发效能

MobPush扩展业务功能设置

MobTech袤博科技

开发者 产品动态

如何编写bash自动补全脚本_语言 & 开发_Lazarus Lazaridis_InfoQ精选文章