Linux 中给自己的命令添加 bash/zsh 补全

Linux 中给自己的命令添加 bash/zsh 补全

参考文档

关于 zsh

我本人使用的 shell 是 zsh,使用 oh-my-zsh 管理插件,添加了 incrzsh-autosuggestions 插件。我最开始尝试基于这俩插件入手,但是这俩插件的源码感觉有点过于复杂,文档看了一圈也懵,后来发现只要能在 bash 上完成补全设置就能在 zsh 上运行,毕竟 zsh 算是基本兼容了 bash,很多地方实现方式不同但殊途同归。

关于 bash

一般 bash 按 tab 默认补全目录,但是很多命令存在子命令,比如 docker 的子命令 imagesps,或 git 子命令 clonepush 等。另外还有很多命令的 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 结尾是一个暴力的方法。

posted @ 2024-06-26 20:49  KZ25T  阅读(6)  评论(0编辑  收藏  举报