我常用的补齐方法

我不知道自己写过多次篇关于补齐的文章了,但这篇将会是最后一篇,因为我决定毕其功于一役😁。

同志们哪,千万不要小看了补齐啊,什么是补齐?补齐就是让程序帮你写程序啊。本来你要自己费劲巴拉地打很多字,哎,有了补齐之后,可以少打很多字,一按Tab键,(程序)自动帮你补齐了你想写的程序。有一位大牛(貌似是Perl作者Larry Wall)说过,会写程序的程序是快乐的程序。

很多人都离不开各种各样的集成开发环境(IDE),因为他们觉得这些IDE提供的补齐功能实在是太方便了。比如说Visual Studio,一上来能帮你自动生成一整个project,其间只需要你回答几个简单的问题,比如应用的名字、窗口类的名字等。然后在写代码的过程中,它还能帮你结合上下文给出补齐提示,比如你有一个String的对象s,你打个s.,它就会列出String类的所有方法供你选择、过滤。这样的功能谁都喜欢,可是,IDE提供的方法总会有个极限,一旦你开始想要一些它的设计者没有想到的功能、改进,基本上就只能干瞪眼了。

不像Linux下,想自己实现一些自己更喜欢的补齐方式,实在是太简单了。

1 bash自带补齐

这个大家都知道了,比如最早的时候,在bash下输入 cd (空格) ,然后按tab键,你会得到当前目录下所有文件、目录的列表。如果你输入 cd s ,你会得到所有以 s 打头的文件、目录的列表,或者如果只有一个这样的文件的话,这个文件的名字就会直接上屏。

现在的bash智能很多了,默认的安装cd后面不会补齐文件,只会补齐目录,这个其实应该是用上了bash的可编程补齐的机制。

这种补齐方法有个缺陷,那就是你必须从头打字,比如你想cd过去的目录最后两个字符是 “-xxx”,你也没法输入 cd -xxx 然后按 Tab 键补齐,所以我想出了 re 这个自己扩展的补齐方法。

2 skeleton_compgen_word.pl: bash补齐和搜索的扩展

skeleton_compgen_word.pl 是我对bash补齐和历史命令机制的一个缺陷的补充。上面说了,bash补齐必须从头输入一个文件的名字,中间不允许打错、漏打字,否则就补不出来了;而bash的 Ctrl-R 历史命令搜索机制也是一样,一旦你开始输入之后,就必须保证你输入的字符串是你想要获取的历史命令的一个子串。

也就是说,想要获得 “hello world”,你必须输入 “h”,或者 “he”,但问题是这时候你有可能得到 “hell yeah”,而不是你想要的 “hello world”,所以你必须一直输入直到第5个字符o, “hell yeah” 才会被排除, “hello world”才能上屏,之前bash一直不停提示所有的匹配选项。

我的想法是,我可不可以输入 h.w ,直接跳过 “hell yeah”呢?如果有多个候选项,比如还有一个 “holy what the f*ck” 的话,我能不能输入 h.w.1 直接选第一个候选词上屏呢?

skeleton_compgen_word.pl 这个脚本让我可以做到这一点,比如我的bash历史里有这么些个 “apt-get install python” 相关的命令:

0: true;apt-get install python-suds             2: true;apt-get install python-pip              4: true;apt-get install python-ropemacs pymacs
1: true;apt-get install python-doc              3: true;apt-get install python-jedi             5: true;apt-get install python-m2crypto

那么,我只要输入 re apt.py.pi 然后按一下 Tab 键,就会补齐到第2条, apt-get install python-pipskeleton_compgen_word.pl 的工作原理就是以 . 为分隔符,把匹配目标串分割为多个子串,然后源文件(源字符串集)中包含所有子串的行(字符串)就是匹配成功的。如果只有一个匹配成功,就上屏,否则就用bash的提示机制。有一个特殊处理!如果分出来的多个子串最后一个是个数字N,而匹配成功的有多于N个串,那第N个串就直接上屏,而不需要继续输入匹配目标串。像上面的情况我输入 re apt-get python.2 再按 Tab 键也可以得到 apt-get install python-pip

2.1 re 的实现

bash有个补齐扩展机制,大致是这样用的: complete comp_func cmd1 cmd2 ,意思是说,当你的输入的命令是 CMD1或CMD2时,在你输入参数时按一下Tab键,bash会调起COMP_FUNC函数来帮你补齐参数。COMP_FUNC函数能获得你已经输入的所有单词,然后它只要对某个特殊bash数组变量赋值(COMPREPLY)就代表补齐的结果。所以我用来补齐 re 命令的 comp_re 函数就很简单了:

  1. 把所有的参数用 . 连接,比如上面两个例子, re apt.py.pi 只有一个参数,内部已经有 . ,连接起来得到得就是 apt.py.pire apt-get python.2 这个有两个参数,第二个内部有 . ,全部连接起来就是 apt-get.python.2
  2. 把这个连接子串交给 skeleton_compgen_word.pl ,同时传给它另一个参数,是我的 .bash_history 文件,它会把第一个参数用 . 切成N个子串,然后用这N个子串到 .bash_history 文件中找到能匹配这N个子串的每一个子串的行,这些行就是我想要的历史命令。
  3. 如果有多于1个匹配的历史命令,把找到的每个历史命令前面加个序号输出;但如果最后一个子串是一个数字M,那就当作只有1个(第M个)匹配的历史命令是匹配成功的。
  4. 如果只有一个匹配成功的命令(包含上述最后加数字选择的情况),就在该命令前加分号 ; 输出(下面会解释为什么加分号)
  5. bash读到这些输出,如果只有一个匹配,则直接上屏,否则会以“序号1:历史命令1 序号2:历史命令2…”这样的方式提示用户
  6. 上屏后,用户得到的补齐结果是: re 匹配参数1 匹配参数2 ;历史命令 这时候分号的作用就显示出来了,这时候你敲回车,bash会执行被分号隔开的两条命令!第一条是 re 匹配参数1 匹配参数2 ,第二条就是你想要的 历史命令

    相对于re自己来说,re这个命令函数它对应的补齐函数comp_re是非常复杂的,因为re自己啥也不干,它存在的唯一目的就是帮助补齐😁。

2.2 什么时候用 . ,什么时候用空格

因为re的特殊性(它会无视它自己的所有参数),所以你可以用空格,也可以用 . 来输入多个匹配子串。

但一些其他的命令补齐,就必须用 . 来保证被补齐的参数只有一个。比如我用来发邮件(mailx)、提review(gerrit-push-review)的联系人补齐,我想输入罗永浩的邮箱,必须用: mailx luo.hao 然后按Tab键补成 mailx luoyonghao@XXX ,如果我用空格 mailx luo hao 是不行的 ,首先,mailx的补齐函数不像comp_re,它是只看最后一个参数(hao)的,luo+hao能找得比单单一个hao更精准;其次,即使补齐了,也是错误的,因为我得到的是 mailx luo luoyonghao@XXX (前面多了个参数luo出来)。

3 被动式补齐:菜单选择

补齐函数写多了之后就觉得有点儿太费劲了,因为它要求你先写个很“难”的补齐函数。为什么说补齐函数难呢,因为它很难调试,必须和bash补齐机制放一起才能调试…

所以我换了个思路,比如 git checkout -B branch remote_branch 这样一条命令,REMOTE_BRANCH 这个参数你可以用bash自带(准确地说是bash_completion自带)的补齐机制来补齐它,但缺点是,就像之前一再提到的,你只能从头开始打它的名字,不能从中间你觉得好记的好区分的地方开始打…

而我实在不想写补齐函数了,所以这回我写了个命令行菜单选择命令:

  1. 给定一堆参数,我把它们打印到终端上,每行一个参数,
  2. 然后让你输入文字、数字来选择它。
  3. 如果你输入的是数字M,那我就认为你选择第M个参数。
  4. 如果你输入的是文字,那就把你的文字用空格、 . 隔开,匹配所有子串的参数留下,其他的扔掉,从头再来
  5. 如果只有一个参数,那就是它了

这个命令叫 select-args ,它的一个变体参考了xargs,叫select-output,给定一个命令,把输出的所有行当成参数,一行一个,然后调用select-args选参数。

于是 REMOTE_BRANCH 我给写成了这样: $(select-output git branch -a),先执行git branch -a得到所有的branch,然后从中选择。

后来又专门封装了一下,成了一个 git-choose-branch 的脚本。

后来干脆把 git 命令改了一下,以错为对,写了个名为git的函数,如果发现我的输入是 git checkout -B ,这个本来是个非法的git命令,是要出错的,但我赋给了它新的含义,先用 git-choose-branch 获得远程分支的名字REMOTE_BRANCH,然后根据远程分支名计算得到本地BRANCH名,最后组合成 command git checkout -B $BRANCH $REMOTE_BRANCH

如果你觉得这么瞎改git命令不对的话,请想想为什么bash有个alias机制允许你把 rm 改成 alias rm='rm -'i 来让它变得更安全呢?…

后来!我给select-args、select-output加入了简单的历史机制,于是上次你选的选项下次会排在最前面,直接回车就可以了…

我写了个 s 命令,它不是对参数进行补齐,而是对命令进行补齐,输入 s hello world ,它会让你选你要用哪个搜索引擎搜 hello world 。我已经封装了32个搜索引擎了😁。

4 Emacs的补齐机制

  • Emacs下有个yasnippet,极其强大
  • helm是一个递进式选择的插件,极其强大
  • 我有写过一个宏扩展机制,《给所有编程语言插上宏的翅膀》
  • Emacs按词补齐、按任意字符串补齐、按整行补齐,我写的 bbyac 插件。