用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自带的补全脚本,有三个发现:

  1. 通过调用complete命令来指定命令调用的脚本,例如 complete -C $MY_SHELL/jc.rb jc
  2. 输入是通过$COMP_LINE来获取当前输入的完整命令,其包含所有参数
  3. 输出是通过标准输出完成的,所有备选是“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

posted @ 2011-06-09 21:34  程序木匠  阅读(601)  评论(0)    收藏  举报