1. 需求描述 需求描述:
C++ 写的CLI命令行程序,如何实现通过Tab键进行关键字智能补齐?
举例说明:如git程序,我们在输入git bran时按Tab键会自动补齐为git branch. 输入git check时按Tab键会自动出现如下选择列表:
1 2 3 4 5 6 7 git check check-attr -- display gitattributes information check-ignore -- debug gitignore/exclude files check-mailmap -- show canonical names and email addresses of contacts check-ref-format -- ensure that a reference name is well formed checkout -- checkout branch or paths to working tree checkout-index -- copy files from index to working directory
现在自己用C++写了一个命令行程序叫jobmgr,该程序可以接收list、get、start、stop等参数(通过main函数参数来接收)来执行不同的子功能。如何实现类似git一样的命令参数自动补全功能,当输入jobmgr li时按Tab键会自动补齐为jobmgr list。
C++代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> #include <unordered_set> int main (int argc, char * argv[]) { std::unordered_set<std::string> job_map = { "list" , "get" , "start" , "stop" }; if (argc < 2 ) { std::cout << "Usage: " << argv[0 ] << " <job_name>" << std::endl; return 1 ; } auto cmd = std::string (argv[1 ]); if (!job_map.count (cmd)) { std::cout << "Unsupport command: " << cmd << std::endl; return 1 ; } std::cout << "Command name: " << cmd << std::endl; return 0 ; }
编译命令:
1 g++ ./job_mgr.cpp -o jobmgr
2. 问题分析 实现类似 Git 的命令行自动补全功能,主要是利用 Shell 的补全机制,创建 Bash 补全脚本,并完成安装。
3. Bash 补全机制原理 3.1. 核心概念
补全函数 (Completion Function)
一个 bash 函数,负责生成补全建议
接收多个参数:命令名、当前词、上一个词等
通过 COMPREPLY 数组输出补全建议
complete 命令
1 complete -F _completion_function command_name
-F:指定补全函数
-W:指定静态单词列表
-o:指定补全选项
3.2. 补全函数接收的变量 补全函数自动接收以下变量:
1 2 3 4 $COMP_WORDS $COMP_CWORD $COMP_LINE $COMP_POINT
3.3. 补全流程 1 2 3 4 5 6 7 8 9 10 11 12 13 用户输入 `jobmgr get m` + Tab ↓ Bash 调用 _jobmgr_completion 函数 ↓ 函数解析 $COMP_WORDS = ("jobmgr" "get" "m") ↓ 判断 $COMP_CWORD = 2(第三个词,从0开始) ↓ 根据前一个词 "get" 提供补全建议 ↓ 填充 COMPREPLY 数组 = ("myservice" "myapp") ↓ Bash 显示补全建议
4. jobmgr 的完整补全脚本实现 4.1. 版本1:基本静态补全 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #!/usr/bin/env bash _jobmgr_completion () { local cur prev opts main_opts services COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]} " prev="${COMP_WORDS[COMP_CWORD-1]} " main_opts="list get start stop version help" services="myservice myapp database webserver cache redis mysql nginx" case ${COMP_CWORD} in 1) COMPREPLY=( $(compgen -W "${main_opts} " -- "${cur} " ) ) ;; 2) case ${prev} in get|start|stop) COMPREPLY=( $(compgen -W "${services} " -- "${cur} " ) ) ;; list|version|help ) COMPREPLY=() ;; esac ;; *) COMPREPLY=() ;; esac return 0 } complete -F _jobmgr_completion jobmgr
4.2. 版本2:带描述的高级补全 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 #!/usr/bin/env bash _jobmgr_completion () { local cur prev words cword _init_completion || return declare -A cmd_desc=( [list]="列出所有服务" [get]="获取服务信息" [start]="启动服务" [stop]="停止服务" [version]="显示版本" [help ]="显示帮助" ) _get_services () { echo -e "myservice\nmyapp\ndatabase\nwebserver\ncache\nredis\nmysql\nnginx" } case ${cword} in 1) local options="" for opt in "${!cmd_desc[@]} " ; do options+="$opt " done COMPREPLY=( $(compgen -W "${options} " -- "${cur} " ) ) if [[ -n "$cur " ]]; then local IFS=$'\n' local descriptions=() for opt in "${!cmd_desc[@]} " ; do if [[ "$opt " == "$cur " * ]]; then descriptions+=("$opt : ${cmd_desc[$opt]} " ) fi done if [[ ${#descriptions[@]} -eq 1 ]]; then COMPREPLY=( $(compgen -W "${options} " -- "${cur} " ) ) elif [[ ${#descriptions[@]} -gt 1 ]]; then echo "" printf "%-20s %s\n" "命令" "描述" printf "%-20s %s\n" "----" "----" for desc in "${descriptions[@]} " ; do IFS=':' read -r cmd desc_text <<< "$desc " printf "%-20s %s\n" "$cmd " "$desc_text " done echo -ne "\njobmgr> $COMP_LINE " fi fi ;; 2) case ${words[1]} in get|start|stop) local services=$(_get_services) COMPREPLY=( $(compgen -W "${services} " -- "${cur} " ) ) if [[ ${#COMPREPLY[@]} -eq 1 ]]; then : elif [[ ${#COMPREPLY[@]} -gt 10 ]]; then echo -e "\n有 ${#COMPREPLY[@]} 个匹配项。显示全部?(y/n)" fi ;; *) COMPREPLY=() ;; esac ;; *) COMPREPLY=() ;; esac } complete -o bashdefault -o default -F _jobmgr_completion jobmgr
4.3. 版本3:支持子命令和选项的完整实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 #!/usr/bin/env bash _jobmgr_completion () { local i=1 cmd subcmd while [[ $i -lt ${#COMP_WORDS[@]} ]]; do if [[ $i -ne $COMP_CWORD ]]; then cmd="${COMP_WORDS[$i]} " if [[ "$cmd " == --* ]]; then case "$cmd " in --config|--log-level|--timeout ) ((i++)) ;; esac elif [[ "$cmd " == -* ]]; then if [[ "$cmd " =~ f$ ]] || [[ "$cmd " =~ c$ ]]; then ((i++)) fi fi fi ((i++)) done local cur="${COMP_WORDS[COMP_CWORD]} " local prev="${COMP_WORDS[COMP_CWORD-1]} " local main_cmds="list get start stop restart status version help" local get_subcmds="config status logs info" local global_opts="-h --help -v --version --verbose --quiet --config=" _get_service_list () { local services if [[ -f ~/.jobmgr/services ]]; then services=$(cat ~/.jobmgr/services) fi if [[ -z "$services " ]] && command -v systemctl &>/dev/null; then services=$(systemctl list-units --type =service --all --no-pager --no-legend | awk '{print $1}' | sed 's/\.service$//' | head -20) fi if [[ -z "$services " ]]; then services="web api db cache redis mongo nginx" fi echo "$services " } case "$prev " in --config|-c) COMPREPLY=( $(compgen -f -X '!*.@(json|yaml|yml|conf|cfg)' -- "$cur " ) ) return 0 ;; --log-level) COMPREPLY=( $(compgen -W "debug info warn error fatal" -- "$cur " ) ) return 0 ;; esac if [[ "$cur " == -* ]]; then COMPREPLY=( $(compgen -W "$global_opts " -- "$cur " ) ) return 0 fi case $cmd in jobmgr) COMPREPLY=( $(compgen -W "$main_cmds " -- "$cur " ) ) ;; get) if [[ $COMP_CWORD -eq 2 ]]; then COMPREPLY=( $(compgen -W "$(_get_service_list) $get_subcmds " -- "$cur " ) ) elif [[ $COMP_CWORD -eq 3 ]] && [[ "$prev " =~ ^($(echo $get_subcmds | tr ' ' '|' ))$ ]]; then COMPREPLY=( $(compgen -W "$(_get_service_list) " -- "$cur " ) ) fi ;; start|stop|restart|status) if [[ $COMP_CWORD -eq 2 ]]; then COMPREPLY=( $(compgen -W "$(_get_service_list) " -- "$cur " ) ) fi ;; list) if [[ $COMP_CWORD -eq 2 ]]; then COMPREPLY=( $(compgen -W "--all --running --stopped --format=" -- "$cur " ) ) fi ;; esac if [[ ${#COMPREPLY[@]} -eq 0 ]]; then compopt -o default COMPREPLY=() fi return 0 } complete -o nospace -o bashdefault -F _jobmgr_completion jobmgr
5. 安装和使用 5.1. 安装补全脚本 1 2 3 4 5 6 7 8 9 10 sudo cp jobmgr-completion.bash /etc/bash_completion.d/jobmgrmkdir -p ~/.bash_completion.dcp jobmgr-completion.bash ~/.bash_completion.d/jobmgrecho "source ~/.bash_completion.d/jobmgr" >> ~/.bashrcsource jobmgr-completion.bash
5.2. 测试补全功能 1 2 3 4 5 6 7 source ~/.bashrcjobmgr [Tab] jobmgr get [Tab] jobmgr start my[Tab]
5.3. 高级配置选项 1 2 3 4 5 6 -o bashdefault -o default -o nospace -o filenames -o plusdirs
5.4. 调试补全脚本 1 2 3 4 5 6 7 8 _jobmgr_completion () { echo "DEBUG: COMP_CWORD=$COMP_CWORD , COMP_WORDS=${COMP_WORDS[@]} " >&2 } complete -D -F _jobmgr_completion jobmgr
6. 动态获取服务列表的增强版 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #!/usr/bin/env bash _jobmgr_completion () { local cur prev cur="${COMP_WORDS[COMP_CWORD]} " prev="${COMP_WORDS[COMP_CWORD-1]} " local cache_file="/tmp/jobmgr_services_${USER} .cache" local cache_age=60 _get_services_dynamic () { if [[ -f "$cache_file " ]] && \ [[ $(($(date +%s) - $(stat -c %Y "$cache_file "))) -lt $cache_age ]]; then cat "$cache_file " return fi local services="" if command -v jobmgr &>/dev/null; then services=$(jobmgr list --name-only 2>/dev/null | head -50) fi if [[ -z "$services " ]]; then if command -v docker &>/dev/null; then services=$(docker ps --format "{{.Names}}" 2>/dev/null | head -20) fi if [[ -z "$services " ]] && command -v systemctl &>/dev/null; then services=$(systemctl list-units --type =service --all --no-pager --no-legend 2>/dev/null | awk '{print $1}' | sed 's/\.service$//' | head -20) fi if [[ -z "$services " ]]; then services="default" fi fi echo "$services " > "$cache_file " echo "$services " } case $COMP_CWORD in 1) COMPREPLY=( $(compgen -W "list get start stop restart status version help" -- "$cur " ) ) ;; 2) case "$prev " in get|start|stop|restart|status) COMPREPLY=( $(compgen -W "$(_get_services_dynamic) " -- "$cur " ) ) ;; *) COMPREPLY=() ;; esac ;; *) COMPREPLY=() ;; esac } complete -F _jobmgr_completion jobmgr
7. 常见问题和技巧
性能问题 :动态获取服务列表可能较慢,使用缓存
多次Tab按下的处理 :第二次Tab可以显示更多信息
区分大小写 :默认不区分,可以通过 shopt -s nocaseglob 设置
特殊字符处理 :使用 printf %q 处理包含空格的服务名
Zsh兼容 :如果需要支持zsh,需要额外的zsh补全脚本
这样实现的bash补全脚本可以完全满足你的需求,提供类似Git的智能补全体验。