Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

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,该程序可以接收listgetstartstop等参数(通过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. 核心概念

  1. 补全函数 (Completion Function)

    • 一个 bash 函数,负责生成补全建议
    • 接收多个参数:命令名、当前词、上一个词等
    • 通过 COMPREPLY 数组输出补全建议
  2. 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.bash
# 保存到: /etc/bash_completion.d/jobmgr 或 ~/.bash_completion.d/jobmgr

_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() {
# 方法1:从静态文件读取
# cat /etc/jobmgr/services.conf 2>/dev/null || echo "default"

# 方法2:从运行的容器/进程获取
# docker ps --format "{{.Names}}" 2>/dev/null

# 方法3:调用jobmgr自身获取(如果jobmgr list命令存在)
# jobmgr list --names-only 2>/dev/null

# 这里使用静态列表示例
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补全脚本

_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
# -f 或 -c 可能需要参数
((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"

# get命令的子命令
local get_subcmds="config status logs info"

# 全局选项
local global_opts="-h --help -v --version --verbose --quiet --config="

# 动态获取服务列表
_get_service_list() {
# 尝试多种方式获取服务列表
local services

# 方式1:从配置文件读取
if [[ -f ~/.jobmgr/services ]]; then
services=$(cat ~/.jobmgr/services)
fi

# 方式2:从系统服务获取
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

# 方式3:默认列表
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
# get 后面的第一个参数:服务名或子命令
COMPREPLY=( $(compgen -W "$(_get_service_list) $get_subcmds" -- "$cur") )
elif [[ $COMP_CWORD -eq 3 ]] && [[ "$prev" =~ ^($(echo $get_subcmds | tr ' ' '|'))$ ]]; then
# get 子命令后的参数:服务名
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)
# 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
# 方法1:复制到系统补全目录(需要sudo)
sudo cp jobmgr-completion.bash /etc/bash_completion.d/jobmgr

# 方法2:复制到用户目录
mkdir -p ~/.bash_completion.d
cp jobmgr-completion.bash ~/.bash_completion.d/jobmgr
echo "source ~/.bash_completion.d/jobmgr" >> ~/.bashrc

# 方法3:直接source
source jobmgr-completion.bash

5.2. 测试补全功能

1
2
3
4
5
6
7
# 重新加载bash配置
source ~/.bashrc

# 测试补全
jobmgr [Tab] # 显示所有主命令
jobmgr get [Tab] # 显示服务列表
jobmgr start my[Tab] # 补全以"my"开头的服务

5.3. 高级配置选项

1
2
3
4
5
6
# 在complete命令中可用的选项:
-o bashdefault # 如果没有补全,使用bash默认补全
-o default # 如果没有补全,使用readline默认补全
-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命令的调试模式
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]}"

# 缓存服务列表(有效期60秒)
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=""

# 尝试从jobmgr命令本身获取(如果支持)
if command -v jobmgr &>/dev/null; then
# 假设jobmgr有一个list命令可以输出服务名
services=$(jobmgr list --name-only 2>/dev/null | head -50)
fi

# 如果失败,尝试其他方法
if [[ -z "$services" ]]; then
# 从Docker获取
if command -v docker &>/dev/null; then
services=$(docker ps --format "{{.Names}}" 2>/dev/null | head -20)
fi

# 从systemd获取
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. 常见问题和技巧

  1. 性能问题:动态获取服务列表可能较慢,使用缓存
  2. 多次Tab按下的处理:第二次Tab可以显示更多信息
  3. 区分大小写:默认不区分,可以通过 shopt -s nocaseglob 设置
  4. 特殊字符处理:使用 printf %q 处理包含空格的服务名
  5. Zsh兼容:如果需要支持zsh,需要额外的zsh补全脚本

这样实现的bash补全脚本可以完全满足你的需求,提供类似Git的智能补全体验。

评论