Linux 中给自己的命令添加 bash/zsh 补全
Linux 中给自己的命令添加 bash/zsh 补全
参考文档
- Bash文档:
/usr/share/doc/bash
- Programmable Completion
- Programmable Completion Builtins
关于 zsh
我本人使用的 shell 是 zsh,使用 oh-my-zsh
管理插件,添加了 incr
和 zsh-autosuggestions
插件。我最开始尝试基于这俩插件入手,但是这俩插件的源码感觉有点过于复杂,文档看了一圈也懵,后来发现只要能在 bash 上完成补全设置就能在 zsh 上运行,毕竟 zsh 算是基本兼容了 bash,很多地方实现方式不同但殊途同归。
关于 bash
一般 bash 按 tab 默认补全目录,但是很多命令存在子命令,比如 docker 的子命令 images
、ps
,或 git 子命令 clone
、push
等。另外还有很多命令的 options,比如 ls 的 -alh
;为了完成完整的补全,你可能需要安装 bash-completion
,比如:
sudo apt install bash-completion
实验环境
kali linux,基于 Debian,with zsh & bash
我自己的命令
我自己写了一个脚本用于辅助工作,脚本内容如下(可以跳过不看,我后边有简单总结):
#!/usr/bin/bash
# help
if [ $1 = "--help" -o $1 = "-help" -o $1 = "help" -o $1 = "h" -o $1 = "-h" ]; then
echo -e "\033[1mhelp \033[0mto show this help page"
echo -e "\033[1mbt %num \033[0mto adjust brightness"
echo -e "\033[1mgc \033[0mto copy git push template"
echo -e "\033[1msf \033[0mto set tty font"
echo -e "\033[1ml \033[0mto locate in a friendly way"
echo -e "\033[1mst \033[0mto show power and memory"
echo -e "\033[1mmdev \$mp \033[0mto mount /dev /dev/shm /proc /sys on mp"
echo -e "\033[1mmwin \033[0mto mount windows homapath on /mnt/win"
exit 0
fi
# adjust brightness
if [ $1 = "bt" ]; then
dirs="$(ls /sys/class/backlight/acpi_video*/brightness)"
if [ ! -n $2 ]; then
read -p "input brightness: " bright
else
bright=${2}
fi
for dir in ${dirs} ;do
sudo sh -c "echo \"${bright}\" >${dir} "
done
exit 0
fi
# copy git template
if [ $1 = "gc" ]; then
if [ ! -n $2 ] ; then
dir=$(pwd)
else
dir=${2}
fi
cp /home/kz25t/template/push.sh ${dir}
echo "copied to ${dir}."
exit 0
fi
# set font
if [ $1 = "sf" ]; then
setfont Lat2-Fixed16
exit 0
fi
# locate
if [ $1 = "l" ]; then
locate "${2}" | grep --color=never "${2}[^\/]*$"
exit ${?}
fi
# state
if [ $1 = "st" ]; then
echo -e "\033[1msudo tlp-stat -b:\033[0m"
sudo tlp-stat -b | egrep -i --color=never "status|energy_now|power_now|energy_full"
echo -e "\033[1mfree -h:\033[0m"
free -h
exit ${?}
fi
# mount dev
if [ $1 = "mdev" ]; then
if [ ! -n $2 ]; then
read -p "input your mount point: " mountp
else
mountp=${2}
fi
sudo mount -t proc proc ${mountp}/proc
sudo mount -t sysfs sys ${mountp}/sys
sudo mount -o bind /dev ${mountp}/dev
sudo mount --types tmpfs --options nosuid,nodev,noexec shm ${mountp}/dev/shm/
exit ${?}
fi
# mount windows
if [ $1 = "mwin" ]; then
sudo mount /dev/nvme0n1p4 /mnt/windows
exit ${?}
fi
# not a command
echo "input invalid. you may type \"mydef help\"."
exit 1
以上文件命名为 mydef,在 /usr/bin/mydef
,也就是说命令是 mydef
简单来说,我希望实现这样的补全:
mydef 按 tab 补全子命令
├── 输入 `-` 补全 -h 和 --help
├── 子命令 help/-help/--help/h/-h 后 无需补全
├── 子命令 bt 后 补全 1-12 的数字
├── 子命令 gc 后 补全目录
├── 子命令 sf 后 无需补全
├── 子命令 l 后 无需补全(需要后面输入任意字符串)
├── 子命令 st 后 无需补全
├── 子命令 mdev 后 补全目录
└── 子命令 mwin 后 无需补全
总的来说算是涵盖了常见的补全类型。
实验过程
第一步 补全第一级子命令
shell 内置一个命令叫 complete
,我们看一下 man page:
额,好在有 tldr:
图上那个链接里有更详细的解释,稍后会用到,但我都会解释。
不过我看一下这个 complete 似乎在 bash 和 zsh 里还不太一样:
在 bash 里 which 不出来,但它还真有这个命令,直接输入命令会显示一大堆 complete 过的函数。我也没查明白这是啥东西,但是不管是 bash 还是 zsh,RTFM 之后编程还是能实现效果的,所以先不管它。
可以看到文档里有如下说明:
看起来我们需要编写一个函数。我们按照蓝色链接编写一个。
于是我们首先创建一个临时目录来进行实验。
mkdir /tmp/code
cd /tmp/code
vim comp.sh # 任意命名
解决命令的子命令补全
首先输入以下内容:
complete -F _my_complete_func mydef
_my_complete_func() {
command_name="${COMP_WORDS[COMP_CWORD]}"
completion_txt="h -h help -help --help bt gc sf l mdev mwin"
COMPREPLY=($(compgen -W "${completion_txt}" -- ${command_name}))
return 0
}
经过一通查询,我对上面这几行做以上解释:
- COMP_WORDS 和 COMP_CWORD 是 shell 提供给编程的接口,第一个是数组,分割了当前命令行的每一项;第二个是当前在编辑命令行的第几项。
- 首先,
_my_complete_func
无固定格式,任何名字都可以,前提是可以作为函数名。但后边的命令名字必须写对,表示对mydef
命令使用这个函数进行绑定。 compgen
命令,-W
指定可能参选的文字列表,-- ${command_name}
用于从列表中筛选出来可以提供的补全方案。这个“筛选”,按下图来说,输入mydef -
的时候,command_name
是-
,也就筛选出-h
,--help
,-help
三个以-
开头的列表项目。筛选出来的表项套一个括号成为列表。在 bash 中,如果没有这一项,那么输入-
之后仍然给出全部表项,但 zsh 似乎更加智能,加不加这一项都可以完成筛选。(我没看 zsh 的文档,有看过的朋友可以留言)- COMPREPLY 变量用于提供给 shell,作为补全的表项。
效果(bash 和 zsh 都是这样):
这样我们对于一级子命令的补全就已经完成了。
第二步 补全一级子命令的参数
首先针对每个子命令都应该写一个单独的函数,此时修改 comp.sh
的第一部分为:
complete -F _my_complete_func mydef
_my_complete_func() {
command_name="${COMP_WORDS[COMP_CWORD]}"
case $COMP_CWORD in
0)
;;
1)
completion_txt="h -h help -help --help bt gc sf l mdev mwin"
COMPREPLY=($(compgen -W "${completion_txt}" -- "${command_name}"))
return 0
;;
2)
eval _my_comp_${COMP_WORDS[1]}
;;
esac
}
其中我们指定的函数不变,但是通过 COMP_CWORD
控制进行到哪个阶段。当其等于 2 时,意味着我们要输入了主命令和子命令,调用函数对子命令的参数进行补全。
首先我们讨论子命令 bt,bt 的补全参数是 0-12 的整数,我们可以暴力一点:
_my_comp_bt() {
command_name="${COMP_WORDS[COMP_CWORD]}"
completion_txt=""
for ((i=0; i<=12; i++)); do
completion_txt+="$i "
done
COMPREPLY=($(compgen -W "${completion_txt}" -- ${command_name}))
}
效果:
虽然有点丑,但总归是有了…
接下来是命令 gc,gc 的补全参数是一个目录。这有点困难,我本来参考的是 complete 函数,但是我花了一坤时读文档和测试都没搞好。灵机一动发现可以试试 compgen 直接生成目录,又搞了几分钟也没搞明白,只能补全一级目录还是当前目录……额,经过排查发现其实 bash 可以补全任意目录但 zsh 不行。代码如下:
_my_comp_gc() {
command_name="${COMP_WORDS[COMP_CWORD]}"
COMPREPLY=($(compgen -d -- "${command_name}"))
}
效果,zsh 虽然不是太好,但确实是有了:
bash 可以补全多级任意目录:
这个 -d
参数看起来很多地方没有提到,我也不知道 zsh 缺陷是不是少了某个参数…但 zsh 也没见文档提到。
如下给每个子命令都写好函数,添加到 comp.sh
内即可。
完成收尾设置
这一点网上很多地方都不够规范
这个只能每次 source 之后使用,为了给它加入常规的 bash 补全,我们可以做如下处理,比如扔进 /etc/profile
里去。但是这显然并不合适,所以需要给这个文件找个家。
阅读 /usr/share/bash-completion/bash_completion
:
local -a dirs=(${BASH_COMPLETION_USER_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion}/completions)
dirs+=($dir/bash-completion/completions)
dirs+=("${BASH_SOURCE%/*}/completions")
dirs+=(./completions)
可以发现,bash-completion 启动的时候加载一系列目录下文件,这些目录包括:
/usr/share/bash-completion/completions
:系统级,很多命令如 ssh 在这里,但 ls 不在。${HOME}/.local/share/bash-completion/completions
:用户级,默认没有这个目录,你可能需要新建一个
我们可以考虑把 /tmp/code/comp.sh
挪到上述位置即可生效:
cp /tmp/code/comp.sh ~/.local/share/bash-completion/completions/mydef
重启 bash 即可生效。但 zsh 无效,目前我还没研究明白,当然一个简单的 source xxx
放在 ~/.zshrc
结尾是一个暴力的方法。