用Ruby制作Bash自动补全脚本
在Linux下的Bash有个很方便的功能就是bash completion,通过安装bash-completion的包就能得到大部分的CLI命令的自动补全功能。在Ubuntu/Debian系统上通过以下命令安装。
1 sudo apt-get install bash-completion
有大部分情况下会自动安装这个包,之后很多应用会自动添加相应的自动补全脚本来帮助CLI用户准确输入相应命令的参数。
我是个比较重度的CLI用户,平时也积累了不少脚本,命名规范比较混乱,造成有些脚本的应用很少,平时也容易忘记,所以一直考虑做一个统一的规范来维护他们。前些天看到ant有一个脚本能够列出当前目录下的所有XML文件,并对build.xml进行基本的分析寻找target来自动完成。我琢磨如果我做一个入口命令,然后其他命令都以这个命令来调用岂不快哉?
说干就干,首先我把所有的命令都重新定义名字,起名为:
XXX-XXX-XXX-XXX
这样的话可以把部分的命令按照目的进行归纳,如dev、sys等,用多级的方法也比较容易在同一个子类别里面按照不同目的分配,有那么一点naming space或者package的味道了。比如说dev-ctags-c是针对C的,dev-ctags-j是针对Java的。
接下来写了一个入口Bash脚本来调用这些命令,因为我把所有的脚本都放在同一个目录下$MY_SHELL这个环境变量里,所以脚本里直接使用了变量。
1 #!/bin/bash
2
3 #########################################################################
4 # jcmd is an entry of jian's cli script
5 # jcmd will list entire script avaiable in $MY_SHELL
6 #########################################################################
7
8 if [ -z "$1" ]
9 then
10 cd $MY_SHELL
11 chmod +x *
12 cd -
13 ls $MY_SHELL | less
14 else
15 cmd=""
16 for param in $*;
17 do
18 cmd="$cmd $param"
19 done
20 cmd=`echo $cmd | sed 's# #-#g'`
21 $MY_SHELL/$cmd
22 fi
这个脚本有一定的问题,暂时不支持参数,还需要少许修复。其功能就是jc dev ctags c变成调用dev-ctags-c,这使得jc可以用自动补全脚本了。但主要jc的目的是为了帮助我记忆一些不常用的脚本,这些脚本如不加参数会自动提示help,这样我就可以完整命令来加参数。
接下来我分析了complete-ant-cmd.pl这个Ant 1.8自带的补全脚本,有三个发现:
- 通过调用complete命令来指定命令调用的脚本,例如 complete -C $MY_SHELL/jc.rb jc
- 输入是通过$COMP_LINE来获取当前输入的完整命令,其包含所有参数
- 输出是通过标准输出完成的,所有备选是“X1\nX2\nX3"这种形式输出,而Bash会将其转为一个列表。当这个列表只有一个值时自动补全所有。
我最近也在研究Ruby,所以就用Ruby写了以下的脚本:
#!/usr/bin/env ruby $shell = ENV["MY_SHELL"] comp_line = ENV["COMP_LINE"] def excluded_files(filename) excluded_list = {'jc' => 1, 'jc.rb' => 1} # p excluded_list.has_key?(filename) return ! excluded_list.has_key?(filename) end #recrusive call def push_items_to_map(map, items) map[items[0]] = {} if (! map.has_key? items[0]) && items[0] push_items_to_map(map[items[0]], items[1..items.size]) if items[1..0] end def list_cmd() Dir.chdir($shell) pwd = Dir.pwd cmd_list = [] Dir.foreach(pwd){|entry| # cmd_list.push entry.gsub("-", " ") if File.executable?(entry) && File.file?(entry) && excluded_files(entry) cmd_list.push entry if File.executable?(entry) && File.file?(entry) && excluded_files(entry) } cmd_list.sort! cmd_map = {} cmd_list.each { |e| words = e.split("-") push_items_to_map(cmd_map, words) } return cmd_map end # list command from previous list def list(cmd, cmds) words = cmd.split(' ') map = cmds candidate = map.keys words[1..words.size].each { |word| if map[word] map = map[word] candidate = map.keys else #match with precommend candidate = [] map.keys.each { |key| candidate.push(key) if word.eql? key[0..word.length-1] } break end } if words.size > 1 return candidate end #run direct test comp_line = "jc" if ! comp_line cmds = list_cmd puts list(comp_line, cmds).join("\n") =begin # mock test m = {} push_items_to_map(m, "a b c d".split(' ')) push_items_to_map(m, "a b d e".split(' ')) p m =end
关键还是抛砖引玉,并且练习一下写Ruby程序,感觉还是很爽的。写的不好,大家多多指正。
Code可以到github上我的repo里面直接获取 https://github.com/genewoo/personal-config