Content-Type: text/x-zim-wiki Wiki-Format: zim 0.4 Creation-Date: 2011-04-04T20:39:29+08:00 ====== 为什么要在 Emacs 里面使用 Shell? ====== 杨 博华 (dove.young@gmail.com), 软件工程师, IBM 杨博华是 IBM 中国软件开发中心的软件测试工程师。负责 IBM Tivoli 软件产品的测试工作。 简介: Shell 是 Unix 系统管理员生活的一部分。早期的 Shell 直接运行在各种各样的 Terminal 里面,随着时间的推移,我们有了 X Window。大量的 Shell 开始运行 Xterm 里面。对于笔者来说呢,更加喜欢让 Shell 运行在 Emacs 里面。将 Shell 运行在 Emacs 里面与运行在 Xterm 里面到底有什么不同呢?相信我,将 Shell 从 Xterm 里面搬出来绝对不是一个赚取眼球,标新立异的决定。这篇文章介绍了将 Shell 从 Xterm 里面搬到 Emacs 里面带来的各种改变。这篇文章适合 Emacs 的初级与中级用户,需要读者具有一定的 Shell 使用经验。最好具有较长时间的 Shell 使用经历,以更加充分的体会到 Emacs 带来的变化。文章中的内容谨代表作者的个人观点。不代表 IBM 的任何官方观点。所附代码仅在运行于 Cygwin 环境下的 GNU Emacs 当中测试通过。 ===== Emacs 编辑环境 ===== 为什么要使用 Shell? 我不也太清楚,但是,在这样一个 Windows 横行的年代里,总是有着无数的 Unix 管理员在遍布全球的各个角落里默默奉献,保证着我们的世界继续运行。那么,为什么要在 Emacs 里面使用 Shell? 在 Emacs 里面使用 Shell 和在其他地方相比,比如说 xterm,甚至是 Windows 的命令行窗口里面使用 Shell 有什么不一样呢?这个问题,我个人认为不是一个技术问题,而是一个**生活舒适度**的问题。之所以选择在 Emacs 里面使用 Shell,就是因为在 Emacs 里面会使你的生活的更加舒适。 ===== 生活舒适度的问题 ===== 信不信由你,人们总是希望生活的更加舒适。那么,把 Shell 搬到 Emacs 环境里究竟能够带来那些生活舒适度的提高呢?在回答这个问题之前,让我们先进行一个简单的热身运动。 大家都知道,现代的 Bourn Again Shell(bash) 要比古老的 Bourn Shell(sh) 用起来舒服多了。因为 Bourn Again Shell(Bash) 引入了很多非常舒服的新的特性。比如说**历史** (History) 功能,比如说**命令行编辑**功能。这些都是大家非常喜欢的功能。不是吗?但是,问题是,如果你想在 Shell 的世界中生活的更加舒适,就会发现很多时候他都不能真正的让你满意。 通常来说,但凡有过在古老的 Bourn Shell(sh) 或者某些缺乏历史功能的 Korn Shell(ksh) 环境下的使用经验的人来说,Bourn Again Shell(bash) 的历史 (History) 功能都是一个非常可爱的功能。它提供了很多很好的重复历史命令的有趣方式,我们可以使用 ! 命令去全部或者部分的引用上一条刚刚执行过的命令,我们可以使用 ^^ 命令简单编辑上一条命令并且生成一条新的命令,我们可以也使用向上的箭头键去回朔查找许久以前曾经执行过的命令,然后使用命令行编辑功能随心所欲的编辑这条命令。但是这里面有一个非常隐蔽的问题:所有这一切,都是集中在“**命令行**”这个概念中的。__说白了,各种各样的不同的 Shell 版本提供给我们的就是一个命令解释器,外加一个命令行编辑环境__。 这样有什么不对吗?对,但是欲望总是没有止境的。为什么不能够**全屏幕编辑**呢? 让我们来看看一个很简单的例子。我有一个 Omegamon XE for Messaging 的安装。这是 IBM Tivoli 旗下的一款监控产品。通过与 IBM Tivoli Monitor 产品的集成,实现对 WebSphere MQ,WebSphere Message Broker 产品的运行监控,以及对 WebSphere MQ 产品的修改与配置。由于某种需要,现在我需要将一个 MQ Monitoring agent 停止下来。我不想将那个漫长的命令从头到尾输入一遍,因为我知道我可以使用 history | grep ‘itmcmd agent’ 命令从已经存在的命令历史里面找到曾经启动这个 agent 的命令,然后我所要做的,仅仅只是在原来命令的基础上将 start 参数修改为 stop 就可以了。但是……事实上 Shell 并没有给我一个修改这条命令的机会。 {{./image001.jpg}} 图 1 事实上我必须先用鼠标将屏幕上的第 437 条命令复制到剪贴板里面,然后把它粘贴到我的命令提示符后面,之后再使用 bash 的命令行编辑功能,将那个可爱的 start 参数修改为 stop。说实话,我的某些同事每天就是这样做的,另一些则是干脆将停止命令从头到尾再输入一遍。回答我一个问题,你有没有曾经也像我一样,在看到 history | grep 的输出的时候,曾经梦想过,能够直接在第 437 条命令上面改几个字母,然后就直接敲下回车呢?现在你可以了——在 Emacs 里面。在 Emacs 里面,你可以将你的 Shell“扩展”到全屏幕编辑。 让我们重新查找刚才提到的那条命令。这一次,我们不用 history | grep 的方法。是的,这个方法仍然管用。但是既然是在 Emacs 里面,我希望介绍一些**更加 Emacs 化的使用方法**,我们使用** Ctrl-r** 组合键。如果你已经使用过 Emacs,但是从来还没有在 Emacs 里面看到过 shell 提示符的话,不妨跟我一起来,在 Emacs 当中输入 ESC-x 组合键,然后在提示缓冲区里面输入 shell(全部字母小写)回车,然后你就会在 Emacs 的编辑缓冲区里面看到熟悉的 shell 提示符了。在接下来的例子里面我们假设你已经执行过启动 MQ Monitoring agent 的那条命令了,大约是在 4 个小时前或者别的什么时候。现在要做的,就是把这个特定的 agent 停止下来。跟我一起在 Emacs 里面输入 Ctrl-r 组合键,这个时候你会在提示缓冲区里面看到** I-search backward:** 这样的字样。没错,这个组合键对应的就是 Emacs 当中的**回朔查找**的功能。我们所要做的,就是像通常在一篇文档里面查找一个句子一样,在回朔查找提示符的后面输入 itmcmd agent,Emacs 非常迅速的就把光标定位到了曾经执行过的 bin/itmcmd agent –o QM_106 start mq 这一行命令上面。就在这里,让我们把 start 修改为 stop,然后,直接就在这里按下回车键——修改以后的命令会自动的被复制到正确的 shell 提示符后面并且被执行。无穷无尽的拷贝与粘贴终于被终结了。 更理想的情况下,如果你想修改的命令还仍然显示在你当前的屏幕范围内(这在我的工作当中覆盖了大多数情况),甚至可以省略使用 Ctrl-r 组合进进行查找的过程,直接移动光标到你需要的命令,修改,或者回车就是了。 {{./image002.jpg}} 图 2 -------------------------------------------------------------------------------- ===== 技术性击倒 ===== 上面提到的还只是一些很简单的事情。或者说是一些使用习惯方面的事情。每个人都有自己的生活习惯。我就见过一些人,他们甚至很少使用 History 功能。他们执行每一条命令都会从头到尾输入进去。如果中间有什么东西写错了,他们会停下来再输一遍。我只能承认我没有他们那么勤奋了。 下面我们要谈论一些真正技术性的问题。一个实际工作中的案例问题。我在上面提到过,我的日常工作的一部分就是使用 Omegamon XE for Messaging 产品,既然他是监控 WebSphere MQ 的一款产品,那么同样意味着我也会频繁使用 WebSphere MQ 产品。一天,我需要处理这样一个问题,我有 11 个处于停止状态的队列管理器(Queue Manager),他们全部需要启动起来。 {{./image003.jpg}} 图 3 通常在 shell 环境下,会有两个选择。第一个选择,一条一条的编写 11 条 strmqm 命令,将所有这些队列管理器一个一个地启动起来。第二个选择,我可以使用 **SED** 实用工具编写一些小脚本,利用 dspmq 命令产生的输出内容来生成这 11 条命令,并把它们输出到一个文件里面。是的,在使用 Emacs 之前我是使用这第二种方法的。现在,在 Emacs 里面,我可以使用第三种选择,一种所见即所得的选择。 Emacs 不仅提供了__全屏幕编辑__的全套功能,而且还提供了超越传统编辑方式的__矩形编辑__功能。使用这个功能,你可以把屏幕上的任何内容__作为一个矩形块进行剪切或粘贴操作__。现在跟我一起,把光标定位到 QM_100 的字母 Q 的位置,并输入** Ctrl-@ **组合键设置起始的标记位。接下来把光标移动到 QM_110 的数字 0 的后面,输入 __Ctrl-x r k __组合键(对,一共是 4 个按键。但是仍然非常容易记忆,只要记住中间的 r 是 rectangle 的缩写就行了,所有的矩形编辑命令都会有这个 r 字母加入)。看看屏幕上面发生了什么?是的,所有的队列管理器的名字全都不见了。他们被作为一个矩形块剪切进了 Emacs 的__删除环(king-ring)__当中。 {{./image004.jpg}} 图 4 接下来,让我们将这些剪切下来的队列管理器的名字粘贴到一个用来启动这些队列管理器的脚本文件中去。在这里我们使用最简单的 **here 文档**的功能 ** cat <strmqm.sh**,创建一个空白的脚本文件。然后,在 Emacs 当中输入** Ctrl-x r y **组合键,这个组合键的作用是将删除环里面的矩形文本块作为一个矩形,粘贴回去。下面的截图就是你在屏幕上所看到的结果。 {{./image005.jpg}} 图 5 没错,你注意到了,我们真正需要的并不是队列管理器的名字列表,而是一连串 strmqm QM_100 这样的 shell 命令。没问题,接下来让我们继续使用 Emacs 的矩形编辑功能来完成这些命令。再次把光标移动到 QM_100 的字母 Q 的位置,输入 Ctrl-@ 组合键设置标志,然后移动光标到 QM_110 的字母 Q 的位置,输入__ Ctrl-x r p__ 组合键。这个组合键的作用是在选定的**位置前面填充一个矩形区域**,填充的内容在提示缓冲区里面输入。现在在提示缓冲区里面 string insert rectangle: 的提示符后面输入 strmqm 加空格。下面的截图就是这个时候你在屏幕上所看到的结果。 {{./image006.jpg}} 图 6 这里面有一个小秘密需要提示一下,因为在 here 文档的提示符下事实上是不可能进行这样的修改的。但是,直到目前为止 Emacs **还没有**将编辑缓冲区里的内容传递给 here 文档提示符。以上所做的一切还都仅仅发生在 Emacs 内部。 现在输入一个回车键,已经编辑好的 11 条 strmqm QM_1xx 命令就已经传递给 here 文档提示符了。最后输入 eof 结束文档编辑。一个简单清晰的队列管理器启动脚本就已经完成了。只要在 shell 提示符下通过 bash strmqm.sh 命令运行它,就可以启动所有这 11 个队列管理器了。 {{./image007.jpg}} 图 7 -------------------------------------------------------------------------------- ====== 一些有趣的小技巧 ====== ===== 第一个小技巧:在 Emacs 里面同时打开多个 shell 会话 ===== 这听起来应该是理所当然的,既然 Emacs 可以同时运行多个窗口……但是即使是在我买的那本关于 Emacs 的书里面也没有提到如何能够真的打开多个 shell 会话。让我们试试看,输入 ESC-x shell 启动 shell 会话,在 shell 提示符下执行 cd /tmp 命令,然后输入 Ctrl-x 2 组合键将屏幕切分成上下两个窗口,然后再次输入 ESC-x shell 启动 shell 会话,你会发现 Emacs 不为所动。在上下两个窗口里面仍然还是 cd /tmp 命令执行后的结果,也就是说仍然还是原先的那个 shell 会话。说起来 O’reilly 公司应该退还我一部分书钱,因为他们没有将我想知道的问题讲明白。后来我终于发现了解决的办法。只要我将当前的 shell 会话所在的缓冲区重新命名,然后就可以再次启动一个新的 shell 会话。让我们来试试看。输入 ESC-x rename-buffer,在 Rename buffer (to new name): 提示符后输入一个新名字,例如 *shellA*。这个时候你会看到上下两个窗口中的缓冲区都被重命名为 *shellA* 了,是的,他们显示的是同一个缓冲区。现在在任意一个窗口里面输入 ESC-x shell 启动 shell 会话,这时你会看到一个新的 *shell* 缓冲区创建了出来,并且有了一个新的 shell 提示符。现在让我们在这个新的 shell 提示符下执行 cd / 命令,你会发现,缓冲区 *shellA* 当中的 shell 会话并没有受到新的命令的影响,因为他们是两个不同的 shell 会话。重复同样的方法,可以得到更多的 shell 会话,满足你的使用需要。 {{./image008.jpg}} 图 8 ===== 第二个小技巧:history | grep 的输出结果可以直接输入回车使用 ===== 让我们再回到当初提到过的第 437 条命令。如果需要再次运行这条命令,你只需要把光标定位到这条命令所在行,**删除掉命令前面的 437 这个号码**,然后直接输入回车,Emacs 会提你把它复制到正确的 shell 提示符后并且启动执行。似乎这个方式描述起来并没有 shell 本身提供的 !437 命令来的迅速,但是他毕竟提供了一个更加直观的操作可能。另外,如果希望在运行之前对于这条命令进行一些修改,我指的是删除,例如删除掉某个参数,或者单词,你也尽可以尽情删除,然后在最终的删除结果上面输入回车,告诉 Emacs 去复制运行就是了。这个状况目前似乎还没有发现 ! 命令有可以搞定的可能。另外,如果希望在运行之前对于这条命令进行一些添加性质的修改,例如添加一些新的参数,或者把第 437 条命令中的 start 单词删掉,添加上 stop 单词,在目前的 Emacs 里面还没有提供直接的支持。但是我们可以通过增加一步操作来实现它。第一步,让我们首先把光标定位到第 437 条命令的位置,然后输入回车,这个时候命令不会成功运行,一行提示信息 bash: 437: command not found 会显示在你的 shell 提示符下面。没关系,让我们稍微修改一下刚刚运行失败的这条命令。重新定位光标到刚刚执行过的这条命令所在的行,然后把命令中的 start 参数修改为 stop 参数,并且删除命令前面的 437 这个号码,再次输入回车,修改之后的命令就会成功运行了。这里增加的第一步操作实际上代替了通常情况下需要的拷贝粘贴操作,拷贝和粘贴事实上是交由 Emacs 替我们实现了。 {{./image009.jpg}} 图 9 ===== 第三个小技巧,整理你的屏幕 ===== 在前面讲到生活舒适度话题的时候,我们曾经提到过在 Emacs 里面使用 shell 的一个很常用的方法就是**在屏幕上已有的命令上面稍作修改,或者直接回车来复用已经执行过的历史命令**。但是如果你所感兴趣的命令因为种种原因已经不在当前屏幕显示的范围之内了,除了不断的通过使用 Ctrl-r 组合键进行回朔查找以外,还有没有什么更方便的办法呢?答案是有,而且还有不止一种选择。如果在你的当前提示符和你感兴趣的命令之间仅仅只是由于时间的关系隔着太多无关的命令记录,那么你尽可以简单的将这些多余的内容删除掉就行了。不要担心,这些内容仅仅只是在 *shell* 编辑缓冲区里的普通文本内容而已,将他们删除和将一篇文档里面的文本删除没有什么区别。如果这些中间的命令记录并不是完全无用了,仅仅只是在现在这段时间不感兴趣的话,你还可以选择使用__ hide-region-hide __命令将这些中间的命令记录作为一个文本块隐藏起来。这些隐藏起来的内容仅仅只是不再在屏幕上显示而已,他们仍然存留在原有的编辑缓冲区里面,以后当你再次需要他们的时候,可是使用 __hide-region-unhide __命令再把他们重新显示出来。还有一种情况就是仅仅因为你刚刚执行了一个产生大量输出的命令而已,比如说使用 cat 命令书出了一个较大的日志文件,从而导致你感兴趣的历史命令被日志文件的内容冲出了当前屏幕显示范围,这个时候只需要输入 __Ctrl-c Ctrl-o__ 组合键,将刚刚执行过的那条命令的屏幕输出清除掉就是了。通常我在每次 cat 日志文件或者列出一个含有较多文件的目录之后都会使用 Ctrl-c Ctrl-o 组合键来清理一下我的屏幕。 ===== 最后我们再来说一个非常有用的小技巧,使用缩写词 ===== Emacs 的强大的缩写词(abbrev)功能,这些缩写词功能同样可以应用到 Emacs 当中的 shell 环境里来。通常在使用 Emacs 便写文章的时候,我们会使用缩写词功能来减少频繁输入冗长的单词或者语句时候的击键次数,或者使用缩写词功能避免一些可能的拼写错误。当我们在 Emacs 里面使用 shell 的时候,我们同样可以使用缩写词功能来缩写一些冗长的命令,路径,和避免一些常见的输入错误,这样在每天的工作当中会为我节省大量的击键次数。有些读者看到这里一定会想到 shell 本身提供的别名(Alias)功能也能完成命令的缩写,但是,别名功能有很多局限性。首先,**别名功能缩写的命令只能通过别名单独引用,无法和其他 shell 命令一同组合运行**。例如我可以定义一个别名来进入常用的 IBM Tivoli Monitoring 产品的安装目录,alias cditm=’cd /opt/IBM/ITM’, 然后输入 cditm 执行这条命令来进入 IBM Tivoli Monitoring 产品的安装目录。但是我不能够定义一个别名来缩写这个目录本身,alias itmm=’/opt/IBM/ITM’, 然后输入 cd itmm 执行这条命令来进入这个目录,或者输入 itmm/bin/cinfo –r 来直接引用该目录下的可执行文件。还有一个问题,**通过别名定义的命令缩写只能够直接引用执行**,**没有可能对于很多相似的命令定义通用的一个“别名模板”**,然后每次使用的时候引用模板,然后稍作修改之后运行。例如启动 Omegamon XE for Messaging 产品中的 MQ Monitoring agent 的常用命令,bin/itmcmd agent –o QM_101 start mq。我可以把它定义为一个别名,alias strkmq_QM101=’bin/itmcmd agent –o QM_101 start mq’, 但是事实上每次在启动一个 MQ Monitoring Agent 的时候,其中的队列管理器(Queue Manager)的名称都是不同的,我不可能为每一个队列管理器都定义一个单独的别名。况且还有其他的 agent,这样的话别名的数量将会相当可观。 Emacs 的缩写词功能在这里提供了两步走的设计方法,避免了以上提到的所有问题。让我们还是以上面提到的目录和命令的两个例子来了解一下在 Emacs 里面如何使用缩写词功能来解决这些问题。首先是常用目录的缩写词定义。在 shell 提示符下输入 cd /opt/IBM/ITM 命令,不用专门为了缩写词去输入这条命令,就在你日常工作当中需要的时候输入就是了,只是记得先不要输入回车,然后,让我们数一数究竟有多少个单词将被包含在这个缩写词定义当中,在这里是 3 个单词,分别是 opt, IBM 和 ITM,现在输入__ Ctrl-u 3 Ctrl-x a l __组合键,稍微有点儿复杂,我在这里简单解释一下,Ctrl-u 在 Emacs 里面是一个非常有用的组合键,它代表一个通用前缀,通常在后面跟一个数字参数,表示随后输入的命令将被重复多少单元,在这个例子里是 3 个单元,后面的 Ctrl-x a l 组合键代表在当前主模式下添加缩写词定义(__add-mode-abbrev__)命令,其中的 l 代表将该缩写词的定义添加到当前的主模式中,如果希望这个缩写词定义在任何主模式下都可以使用,那么将组合键中的 l 替换成 g,即 Ctrl-x a g, 代表 __add-global-abbrev__ 命令。然后,在提示缓冲区确认定义无误之后,输入希望的缩写单词,itmm,即可完成缩写词定义。 {{./image010.jpg}} 图 10 接下来我们来使用这个缩写词。例如我们需要进入到 IBM Tivoli Monitoring 产品的安装目录,在 shell 提示符下输入__ cd /itmm__ 加空格,你会发现 Emacs 立刻将这条命令扩展成为 cd /opt/IBM/ITM 显示在刚才的输入位置。现在我们要执行 IBM Tivoli Monitoring 产品目录下的 cinfo 可执行程序,在 shell 提示符下输入 /itmm 加空格,Emacs 立刻将这条命令扩展成为 /opt/IBM/ITM 显示在刚才的输入位置,然后我们只需要输入一个退格键删掉刚才那个空格,继续在后面输入 bin/cinfo –r 就可以了。事实上由于在我的工作当中 , 对于 cinfo 命令来说,-r 这个参数非常常用,所以在我的 Emacs 里,bin/cinfo –r 已经被缩写成了 rcinfo 这个缩写词了。那么在我的 Emacs 里面我只需要输入 /itmm 空格键,退格键和 rcinfo 就可以实现完整的 /opt/IBM/ITM/bin/cinfo –r 这条命令了。 再下来我们来看看启动 MQ Monitoring Agent 的命令的缩写词。在 shell 提示符下输入 bin/itmcmd agent –o start mq 命令,注意,在–o 参数后面没有提供队列管理器的名字。因为每次执行这条命令的时候,队列管理器的名字都有可能变化,因此我们不把它定义到缩写词内部。输入 __Ctrl-u 6 Ctrl-x a l __组合键,然后,在提示缓冲区确认定义无误之后,输入希望的缩写单词,strkmq 即可完成缩写词定义。(在进行光标移动时字是由数字和字母组成的,其它的符号都被视为空格) {{./image011.jpg}} 图 11 现在让我们来使用这个缩写词。首先我们需要使用 itmm 这个缩写词进入到 IBM Tivoli Monitoring 产品的安装目录,然后,在 shell 提示符后面输入 strkmq 空格,Emacs 立刻将这条命令扩展成为 bin/itmcmd agent –o start mq 显示在刚才的输入位置,现在移动光标到–o 参数后面,输入队列管理器的名称并且回车,你就会看到希望的命令得到运行。 发明一个完美无缺的 shell 是一件相当困难的事情,但是将现有的资源整合起来却会产生很多意想不到的效果。这个世界已经有了很多现成的科技资源,如果我们在这些资源上面进行创新,整合,不仅很多问题都可能迎刃而解,而且还可以为我们自己再节省一些喝茶和咖啡的时间