最新版本的 Vault 中的一项可用性改进是,它支持在 bash 和 zsh 上对选项和已保存的服务名称进行制表符补全。事实证明这真的很容易做到,我希望更多的 Node/Ruby/whatever 命令行应用程序能够支持它,所以下面就是具体方法。
(我从 rbenv 抄袭了几乎所有我所知道的关于这个的内容。有时,编写 shell 脚本最难的事情就是知道要用 Google 搜索什么,所以如果有疑问,可以查阅你经常使用的程序的脚本。)
Bash 和 zsh 具有不同的制表符补全系统,具有大量功能,你可以对它们进行相当花哨的操作(如果你不相信我,请阅读 git 的补全脚本)。但对于真正基本的使用,它们让你做的事情基本相同。当用户尝试用制表符补全程序的参数时,你注册一个要调用的函数。Vault 的脚本如下所示,首先是 bash:
# completion.bash
_vault_complete() {
COMPREPLY=()
local word="${COMP_WORDS[COMP_CWORD]}"
local completions="$(vault --cmplt "$word")"
COMPREPLY=( $(compgen -W "$completions" -- "$word") )
}
complete -f -F _vault_complete vault
And zsh:
# completion.zsh
_vault_complete() {
local word completions
word="$1"
completions="$(vault --cmplt "${word}")"
reply=( "${(ps:\n:)completions}" )
}
compctl -f -K _vault_complete vault
The important hooks are these lines:
# bash
complete -f -F _vault_complete vault
# zsh
compctl -f -K _vault_complete vault
它们基本上做同样的事情:它们表示当命令行中的第一个单词是 vault 时,调用函数 _vault_complete 来执行补全(bash 为 -F,zsh 为 -K),并且还允许将文件名用作补全(-f 标志)。
这就是您注册补全函数的方式,但它们是如何工作的呢?首先,它们获取当前的 shell 单词:在 bash 中为 "${COMP_WORDS[COMP_CWORD]}",在 zsh 中为 "$1"。它们将这个单词传递给 Vault,作为 vault --cmplt WORD。这只是 vault 可执行文件的一个特殊参数,它接受一个部分完成的单词并将其可能的补全列表打印到 stdout,以换行符分隔。您可以按照自己想要的方式实现这一点;Vault 读取您的配置文件并反映其自己的命令行标志以生成补全。您甚至不必亲自过滤与输入词匹配的可能结果,shell 可以为您完成此操作。(如果自己进行过滤可以减少查找输入的补全所需的时间,那么这可能是有利的。)
vault --cmplt 的输出存储在变量 completions 中,然后进行一些后处理。在 bash 中我们这样做:
COMPREPLY=( $(compgen -W "$completions" -- "$word") )
compgen 会过滤补全列表,找出与输入词真正匹配的补全,bash 期望最终的补全结果存储在特殊变量 COMPREPLY 中。最终出现的结果将用于补全用户的输入。
在 zsh 中,我们有以下内容:
reply=( "${(ps:\n:)completions}" )
这表示,在换行符上拆分“completions”的值,并将结果列表存储在“reply”中,这是 zsh 期望完成的最终位置。
这就是完成基本完成的全部内容。您需要为用户提供一种方便地将这些函数加载到其 shell 中的方法。Vault 使用脚本来执行此操作,该脚本可检测您正在使用的 shell 并加载正确的钩子:
# init.sh
if [ -n "$BASH_VERSION" ]; then
root="$(dirname "${BASH_SOURCE[0]}")"
source "$root/completion.bash"
elif [ -n "$ZSH_VERSION" ]; then
root="$(dirname "$0")"
source "$root/completion.zsh"
fi
此脚本和两个完成脚本并排存在于 Vault 源代码树中。最后一个粘合点是 Vault 有一个名为“vault --initpath”的命令,它返回“init.sh”的完整路径。这是因为找到已安装库的路径可能很棘手,因此更容易让可执行文件能够告诉您其脚本的位置,这在 Node 中可以使用“__dirname”变量轻松完成。
这允许用户将其放入他们的配置文件中以加载您的完成脚本:
which vault > /dev/null && . "$( vault --initpath )"
看到了吗?再简单不过了。如果你正在构建一个命令行程序,这是一个非常简单的可用性优势,它使与程序的交互更加愉快。
发布于 2013 年 2 月 12 日。此条目发布于 Ruby、JavaScript、Node、Vault。将 永久链接 加入书签。
# 示例代码:
function _complete() {
COMPREPLY=($(compgen -W "ab cd ef" "${COMP_WORDS[1]}"))
}
function func() {
echo $1
}
complete -F _complete func
# 改格式:
_vault_complete() {
COMPREPLY=()
local word="${COMP_WORDS[COMP_CWORD]}"
local completions="$(vault --cmplt "$word")"
COMPREPLY=( $(compgen -W "$completions" -- "$word") )
}
complete -f -F _vault_complete vault
# 执行效果:
$ func <tab><tab>
ab cd ef
$ func a<tab>
ab
complete [-abcdefgjksuv] [-o comp-option] [-A action] [-G globpat] [-W wordlist] [-P prefix] [-S suffix] [-X filterpat] [-Ffunction] [-C command] name [name ...]
-W wordlist: 自动补全使用的wordlist, 使用IFS分割,会和当前用户输入的Word做前缀比较,提示那些匹配的word list.
-S suffix: 向每个自动补全word后添加suffix 后缀.
-P prefix: 向每个自动补全word后添加prefix 前缀.
-X filterpat: 对于文件名,将匹配pattern的文件名从completion list中移除(exclude), pattern中使用!表示否定
-G globpat: 对于文件名,将匹配pattern的文件名作为可能的completion. 与-X刚好相反,-X “!*.foo” 与 -G “*.foo”作用相同
-C command: 将command命令的执行结果作为可能的completion.
-F function: 执行shell function,在function中对COMPREPLY这个数组复制,作为可能的completion
-p [name]: 打印当前自定义的complete
-r [name]: 删除当前自定的complete
-A action : 表示生成可能的completion的方式,包括alias, file, directory等,具体请参看文后的参看资料
One of the usability improvements in the latest version of Vault is that it supports tab-completion for options and saved service names, on both bash and zsh. Turns out this is really easy to do, and I’d like more Node/Ruby/whatever command-line apps to support it, so here’s how.
(I cribbed almost everything I know about this from rbenv. Sometimes the hardest thing about shell scripting is knowing what to google for, so when in doubt: dig through the scripts of a program you use a lot.)
Bash and zsh have different tab-completion systems with a large array of features, and you can get quite fancy with them (read the completion scripts for git if you don’t believe me). But for really basic use they let you do basically the same thing. You register a function to be called when the user tries to tab-complete an argument to your program. Here’s what Vault’s scripts for this look like, first bash:
# completion.bash
_vault_complete() {
COMPREPLY=()
local word="${COMP_WORDS[COMP_CWORD]}"
local completions="$(vault --cmplt "$word")"
COMPREPLY=( $(compgen -W "$completions" -- "$word") )
}
complete -f -F _vault_complete vault
And zsh:
# completion.zsh
_vault_complete() {
local word completions
word="$1"
completions="$(vault --cmplt "${word}")"
reply=( "${(ps:\n:)completions}" )
}
compctl -f -K _vault_complete vault
The important hooks are these lines:
# bash
complete -f -F _vault_complete vault
# zsh
compctl -f -K _vault_complete vault
These do basically the same thing: they say that when the first word in the command-line is vault, call the function _vault_complete to perform completion (-F for bash, -K for zsh), and also allow filenames to be used as completions (the -f flag).
So that’s how you register completion functions, but how do they work? Well, first they get the current shell word: this is "${COMP_WORDS[COMP_CWORD]}" in bash, and "$1" in zsh. They pass this word to Vault, as vault --cmplt WORD. This is just a special argument to the vault executable that takes a partially-complete word and prints a list of possible completions of it to stdout, separated by newlines. You can implement this however you want; Vault reads your config file and reflects on its own command-line flags to generate completions. You don’t even have to filter the possible results yourself for those that match the input word, the shell can do this for you. (It may be advantageous if doing filtering yourself reduces the time it takes to find completions for the input.)
The output of vault --cmplt gets stored in the variable completions, and then a little post-processing takes place. In bash we do this:
COMPREPLY=( $(compgen -W "$completions" -- "$word") )
compgen is what filters your list of completions for those that actually match the input word, and bash expects the final completion result to be stored in the special variable COMPREPLY. Whatever ends up there is what will be used to complete the user’s input.
In zsh we have this:
reply=( "${(ps:\n:)completions}" )
This is saying, split the value of completions on newlines, and store the resulting list in reply, which is where zsh expects completions to end up.
So that’s all there is to doing basic completion. You’ll need to provide a way for the user to actually load these functions into their shell conveniently. Vault does this using a script that detects which shell you’re using and loads the right hooks:
# init.sh
if [ -n "$BASH_VERSION" ]; then
root="$(dirname "${BASH_SOURCE[0]}")"
source "$root/completion.bash"
elif [ -n "$ZSH_VERSION" ]; then
root="$(dirname "$0")"
source "$root/completion.zsh"
fi
This script, and the two completion scripts, live side-by-side in the Vault source tree. The final bit of glue is that Vault has a command called vault --initpath, which returns the full path to init.sh. This is because finding the path to an installed library can be tricky, so it’s easier to just have the executable be able to tell you where its scripts are, which in Node can easily be done using the __dirname variable.
This lets the user drop this in their profile to load your completion scripts:
which vault > /dev/null && . "$( vault --initpath )"
See? Couldn’t be easier. If you’re building a command-line program this is a really easy usability win that makes interacting with the program far more pleasant.
Posted on February 12, 2013. This entry was posted in Ruby, JavaScript, Node, Vault. Bookmark the permalink.