您的位置:首页 > 运维架构 > Shell

当 Shell 遇见 Emacs -- 大话 Emacs Shell Mode(第 1-3 部分 全)

2012-11-28 14:24 267 查看


当Shell遇见Emacs--大话EmacsShellMode

简介:GNUEmacs有很多“神奇”的功能。常言说“每一个hacker都有一个自己的GNUEmacs”。这个事实在很大程度上得益于人们能够按照完全自我的方式去使用GNUEmacs。将Shell运行在GNUEmacs里面就是众多的用法之一。在GNUEmacs里面运行Shell有很多种不同的方法。包括各种各样的终端模拟。但是在笔者的工作当中更多的使用的是Shell-mode的方式。在这种方式下,可以最大限度的利用GNUEmacs所具有的各种神奇能力,让日常工作变得前所未有的轻松、有趣。

第一回引子

GNUEmacs是一个非常强大的编辑器,这个编辑器不仅可以用来写文章,写程序,更重要的是,他可以和一些原本看似没有明显关系的应用程序在一起,合作创造出一些新的“不可思议”的应用。比如说可以在GNUEmacs里面运行你的Shell。

通常来说人们在Linux或者Unix上面工作的时候,不论是在本机工作,还是登录到地球另一头的远端机器,都是使用各种各样的终端或者终端模拟器来运行Shell。最常见的例如xterm,rxvt,以及Putty之类的终端模拟器。与此对应,GNUEmacs也有自己的终端模拟器,例如ansi-term,multi-term等等。这些终端模式,使得你可以像在在其他终端当中一样工作,甚至可以在Emacs的终端里面运行Vim。

但是,今天要和大家分享的是另外一种使用方式——Shellmode。这是一种完全不同的工作方式。这种方式和大家常用的工作方式最大的一个区别,就是在这里完全没有任何terminal的存在。用户实际上是工作在一个Emacs的文本缓冲区里面,并不直接和Shell进行交互。一切的命令输入都是写入到这个文本缓冲区当中,经由
comint.el
从缓冲区中读取,然后转交给后台的Shell进程。Shell产生的输出再由
comint.el
进行收集,然后写入到用户所用的这个缓冲区当中来。这个缓冲区在
Emacs当中叫做Shell缓冲区(Shellbuffer)。

启动一个Shell缓冲区并且进入shellmode的过程非常简单。只需要在Emacs当中按下
Meta-x
组合键(在现在的键盘上通常是Alt-x组合键),然后输入命令
shell
并回车,Emacs就会启动一个Shell进程并且打开一个与之关联的Shell缓冲区。Shell缓冲区的名字通常会是
*shell*
。具体启动什么样的
Shell进程通过Emacs配置文件里的
shell-file-name
变量指定,或者由用户的环境变量
SHELL
EMACSSHEL
来指定。通常的写法是
(setqshell-file-name"/bin/bash")

或者
exportEMACSSHELL=/usr/bin/zsh

另外如果你希望使用一个支持ANSIcolor的Shell进程,那么最好在你的Emacs配置文件里面加入下面两行,以便在执行
ls–color=auto
命令的时候输出的色彩信息能够被Emacs正确解析。
(autoload'ansi-color-for-comint-mode-on"ansi-color"nilt)
(add-hook'shell-mode-hook'ansi-color-for-comint-mode-ont)

说了这么多了,这种工作方式究竟能有什么好处呢?我为什么要离开熟悉的Xterm,把我的Shell搬到Emacs当中来呢?

第二回初识Shellmode--窗口篇

下面我们就来谈谈好处。事实上不仅仅是好处,在相当程度上甚至是不可替代性。

第一个明显的好处就是多窗口的工作模式。

通常在人们的工作当中都会打开多个终端,同时进行几份工作。在这个时候就需要对这些终端窗口进行排列和管理(在这里假设你工作在图形化环境之下)。而且通常需要频繁的使用鼠标在不同的窗口之间切换焦点。为了避免窗口之间相互遮盖,你也许会通过精心编辑的
.Xdefaults
文件使得两个或四个终端窗口恰到好处的平铺在整个屏幕当中。但是仍然需要使用鼠标在不同的窗口进行切换,在不同的窗口之间复制粘贴信息……这些窗口维护的工作在任务繁忙的时候会很繁重。并且如果这时候需要的不止
4个窗口,或者你还需要进行额外的文字编辑的工作……最终窗口还是会要么被覆盖起来,要么被挤到别的虚拟桌面。

在这种时候最好来试试GNUEmacs。GNUEmacs天生具有完善的窗口管理功能,并且完全不依赖于XWindow。这是因为GNUEmacs的诞生要远远早于XWindow的历史。在GNUEmacs里面你只需要按下
Ctrl-x2
组合键就可以把当前窗口切分成上下两个等分的窗口,
+----------------------+
||
||
+----------------------+
||
||
+----------------------+

按下
Ctrl-x3
组合键又可以把当前窗口切分成左右两个等分的窗口。这些切分可以一直进行下去。
+----------+-----------+
|||
|||
+----------+-----------+
||
||
+----------------------+

输入
Ctrl-x0
可以关闭当前光标所在的窗口。
+----------------------+
||
||
+----------------------+
||
||
+----------------------+

输入
Ctrl-x1
组合键则可以关闭其他所有窗口,并使当前光标所在的窗口成为最大的窗口。
+----------------------+
||
||
||
||
||
+----------------------+

当你使用两个或以上的窗口的时候,可以使用
Ctrl-xo
(注意是小写字母o)组合键在各个窗口进行移动。通过给
Ctrl-xo
组合键加上数字前缀,例如
Ctrl-u3Ctr-xo
或者更加简洁的
Meta-3
Ctrl-xo
就可以在多个窗口之间快速的移动。

当然,当你启动了太多各种缓冲区的时候,总归是要把其中的一些覆盖掉的。因为保证工作窗口具有足够的可视面积才是真正有意义的事。在这种时候可以通过
Ctrl-xb
组合键在所有缓冲区之间方便的切换。或者通过
Ctrl-x
Ctrl-
b
组合键得到所有缓冲区的列表。

这种缓冲区的切换和XWindow窗口或者虚拟桌面之间的切换最大的不同在于——如果你有任意两个或者多个缓冲区的工作需要相互参照(这样的需要会非常常见),甚至就是信息的复制粘贴,这个时候相关的工作窗口最好能分布在同一个屏幕上。在GNUEmacs当中你将很容易把这些需要参照的缓冲区切换到同一个屏幕的窗口当中去。而在图形终端的工作方式下,这些需要参照的窗口常常要么恰好是相互覆盖的,要么恰好是处在不同的虚拟桌面之中,频繁的拖拽移动将会变得非常繁琐。

还有一种情况,由于工作的原因恰好需要对同一个Shell进程当中的内容进行上下文参照……通常绝大多数终端都不提供这种功能。但是在Emacs里面,同一个缓冲区显示在两个独立的窗口里面完全不成问题。

另外如果你很喜欢多个虚拟桌面的工作方式,可以使用
make-frame
命令生成多个
frame
(也许可以叫做“窗框”)
,把他们放到多个虚拟桌面当中去。而且即使是在这种情况下,仍然可以使用
Ctrl-x
b
组合键在任何一个
frame
中的任何一个窗口中切换到任何一个被遮盖的缓冲区。不需要进行任何XWindow当中的窗口移动和桌面切换,包括进行上下文参照。

技巧一

如何在GNUEmacs当中启动多个Shell进程及其对应的Shell缓冲区?

我在上文当中提到了那么多的窗口,但是如果你在
minibuffer
当中第二次输入
Meta-xshell
命令,GNUEmacs会把你带到已经存在的那个名叫
*shell*
的Shell
缓冲区,而不是创建一个新的。解决的方法非常简单——你只需要使用
rename-buffer
命令为现有的Shell缓冲区重新安排一个名字,然后再执行shell命令,GNUEmacs就会为你创建一个新的名叫*shell*的Shell缓冲区了。因为这两个命令在我的工作中用的非常频繁,所以我把它们绑定到了两个快捷键上面
(global-set-key(kbd"C-cz")'shell)
(global-set-key(kbd"<f10>")'rename-buffer)

技巧二

如何
undo
到我刚刚离开的窗口设置?

上文提到过,
Ctrl-x0,Ctrl-x1,Ctrl-x2,Ctrl-x3能够快速的更改GNUEmacs的窗口设置,但是如果我在用过Ctrl-x1之后希望能够快速“退回”到“刚才”使用过的窗口设置,而不是把它再做一遍,有没有办法做呢?
GNUEmacs有一个叫做winner-mode的minormode
可以帮你完成这个愿望。


只需要在你的Emacs配置文件里面加入下面几行
(when(fboundp'winner-mode)
(winner-mode)
(windmove-default-keybindings))

然后就可以使用Ctrl-c←(对,就是向左的箭头键)组合键,退回你的上一个窗口设置。

第三回甜蜜约会--buffer篇

上文描述了在GNUEmacs里面通过简单的窗口管理优化Shell工作的方法,是不是开始对Shell从终端里面搬到Emacs里面开始有了一点点的心动了呢?别着急,这还只是个开始,目前你看到的都还只是外表。接下来让我们和Emacs来一个甜蜜的约会吧。

输入

我在开头的引子部分曾说过,在Shellmode中工作的时候,用户实际上接触的是一个文本缓冲区,实际上并没有直接的跟Shell进程打任何交道。这也是和通常的终端模式的工作方法的一个非常大的区别。虽然这个区别看起来似乎不是那么显著(那是因为这个Shell缓冲区被设计成了看起来很像一个图形终端的样子),但是实际上这点区别将会带来一些不可替代的优势。让我们来先看一个简单的例子:

让我们在Shell提示符前输入这样一行命令
2:2037:13:04:40:~
dove@bash-4.1$cd/usr/share/emacs

2:2038:13:05:05:/usr/share/emacs
dove@bash-4.1$ls-1
23.1
site-lisp
site-lisp.tar

2:2039:13:05:09:/usr/share/emacs
dove@bash-4.1$

这个时候让我们把光标移动到23.1的前面,输入
ls-1
加空格,
2:2037:13:04:40:~
dove@bash-4.1$cd/usr/share/emacs

2:2038:13:05:05:/usr/share/emacs
dove@bash-4.1$ls-1
ls-123.1
site-lisp
site-lisp.tar

2:2039:13:05:09:/usr/share/emacs
dove@bash-4.1$

然后回车。接下来就会看到这样的输出结果出现在缓冲区里面。
2:2040:13:08:55:/usr/share/emacs
dove@bash-4.1$ls-123.1
etc
leim
lisp
site-lisp

2:2041:13:09:06:/usr/share/emacs
dove@bash-4.1$

这是一件很有意思的事情,因为我们并没有像在终端当中常见的那样在Shell提示符的后面进行命令输入,而是在一个看起来非常随意的地方。神奇的是他居然被正确地执行了。事情的真相其实很简单。

因为现在我们是在一个被称作Shell缓冲区的文本缓冲区里面。这就是一个很普通的文本缓冲区,它具有所有其他文本缓冲区所具有的一切特性。你可以在任何时候,任何位置,对这个缓冲区里的任何文本内容进行任何编辑。因为他就是文本。直到某一刻,你在其中一个文本行上面按下了回车,这时
comint.el
就会负责把当前光标所在行的内容提取出来,发送给Shell去执行,然后将Shell执行的结果以及一个提示符(这个提示符实际上也是由
Shell输出给
comint.e
l
的)以文本的形式添加到这个缓冲区的末尾。

这个例子并不仅仅是列一个目录那么简单,事实上他提供了一个更加强大的工作方式——曾经只能用来阅读的命令输出现在也可以被用来构造新的命令了。让我们再来看一个新的例子,在这个例子中我们将把这种能力与Bash的历史命令引用的能力结合起来
2:2044:15:16:17:/usr/share/emacs/23.1
dove@bash-4.1$ls-1
etc
leim
lisp
cd../site-lisp&&!!

2:2045:15:16:49:/usr/share/emacs/23.1
dove@bash-4.1$cd../site-lisp&&!!
cd../site-lisp&&ls-1
auctex
auctex.el
autoconf
autoconf-mode.el
autotest-mode.el
bashdb.el
bashdb.elc
bbdb

如果这时候我需要列出
auctex.el
文件的内容,我只需要在在各文件名前面输入
head
,然后回车就行了
2:2045:15:16:49:/usr/share/emacs/23.1
dove@bash-4.1$cd../site-lisp&&!!
cd../site-lisp&&ls-1
auctex
headauctex.el
autoconf
autoconf-mode.el
autotest-mode.el
bashdb.el
bashdb.elc
bbdb

2:2046:15:17:16:/usr/share/emacs/site-lisp
dove@bash-4.1$headauctex.el
;;;auctex.el
;;
;;ThiscanbeusedforstartingupAUCTeX.Thefollowingsomewhat
;;strangetrickcausestex-site.eltobeloadedinawaythatcanbe
;;safelyundoneusing(unload-feature'tex-site).
;;
(autoload'TeX-load-hack
(expand-file-name"tex-site.el"(file-name-directoryload-file-name)))
(TeX-load-hack)

2:2047:15:23:53:/usr/share/emacs/site-lisp
dove@bash-4.1$

这看起来已经不太像是在运行Shell了,倒象是在与某人合作编写一篇巨大的文章了,而Shell就是你的合作者。如果你真这么想的话,那就非常好了。至少你已经从枯燥乏味的日常工作当中找到些许的乐趣了。

小技巧

如果你登录在一台远程机器上工作,cat一个文件后,需要把这个文件的内容保存到本地来,那么完全不需要启动一个FTPsession去下载这个文件。你只需要选中缓冲区里面的文件内容,按下
Meta-x
组合键,输入
write-region
命令就可以把选中的内容保存在本地文件当中。


当Shell遇见Emacs--大话EmacsShellMode,第2部分

对Shell环境的扩展和定制

简介:在《大话EmacsShellMode》的第1部分里面介绍了一些Shell环境下的日常操作如何在GNUEmacs的Shell-mode模式下变得轻松愉快。在接下来的这个部分里面,我将介绍一些针对Shell环境的扩展和定制。通过对
Emacs的扩展和定制,将会使Emacs当中的Shell操作变成一种更加舒适的享受。

第四回我爱我家——装修篇

在《大话EmacsShellMode》的第一部分里面介绍了一些Shell环境下的日常操作如何在GNUEmacs的Shell-mode模式下变得轻松愉快。在接下来的这个部分里面,我将介绍一些针对Shell环境的扩展和定制。通过对Emacs的扩展和定制,将会使Emacs当中的Shell操作变成一种更加舒服的享受。

进入和退出ShellMode

轻轻的我走了,正如我轻轻的来;我轻轻的招手,作别西天的云彩。

但是在EmacsShellMode的缺省设计里面,没有能够让我们如此轻松和优雅的进入与退出。这就是在这一节当中我们要进行定制和扩展的地方。

Shellbuffer的进入

首先是进入。在本文的第一部分有一个小技巧,介绍了在GNUEmacs中如何打开多个Shellbuffer——我们需要将现有的Shellbuffer重命名,然后才能再次打开一个叫做
*shell*
的Shellbuffer。这是Emacs创建Shellbuffer时使用的默认名称。

这是一个很不优雅的行为。这样的细节工作应该由Emacs事先料理好,我所需要的只是优雅的进入。实现这个目的有两种做法,一种是在创建Shellbuffer的时候就把它修改成一个独特的名字;另外一种做法是在创建出Shellbuffer之后,根据用户的使用情况来自动修改Shellbuffer的名称。由于工作特点的关系,我选择的是第二种方案。

在我的工作环境当中,绝大多数时间都要登录到远程的机器上去工作。所以我非常希望Shellbuffer的名称能够被自动修改成我所登录的目标机器的名称,这样在我登录大量的机器进行操作的时候,就可以方便的通过buffer名称来进行分辨。这就是我选择第二套方案的原因。我首先接受Emacs创建出来的默认buffer,然后在我登录远程机器的时候Emacs会自动为我改名。如果我没有登录远程机器,那么它将保持默认的名称,或者由我主动的修改buffer名称。

接受默认的buffer名还有一个附加的好处——当你打开大量的buffer进行工作的时候,如果要回到这个默认的Shellbuffer,你不必在长长的buffer列表里面进行切换,只需要执行一个打开Shell的命令,也就是
M-xshell
,Emacs就会立刻把你带到这个默认的Shellbuffer中来。为了能够更加方便的打开Shell,我把这个命令绑定到了
C-c
z
组合键上:
(global-set-key(kbd"C-cz")(quoteshell))

现在让我们看一看Emacs是如何在我登录远程机器的时候自动修改Shellbuffer的名称的。实现这样的功能首先需要编写一个
rename-buffer-in-ssh-login
函数:

清单1.rename-buffer-in-ssh-login函数


(defunrename-buffer-in-ssh-login(cmd)
"Renamebuffertothedestinationhostnameinsshlogin"
(if(string-match"ssh[-_a-z0-9A-Z]+@[-_a-z0-9A-Z.]+[]*[^-_a-z0-9-A-Z]*$"cmd)
(let((host(nth2(split-stringcmd"[@\n]"t))))
(rename-buffer(concat"*"host));
(add-to-list'shell-buffer-name-list(concat"*"host));
(message"%s"shell-buffer-name-list)
)
)
)

这个函数会分析提供给它的命令。如果匹配预先定义的正则表达式,则截取
@
字符后面的机器名,然后使用
rename-buffer
命令修改当前buffer的名称。另外,由于在GNUEmacs的默认约定里将Shellbuffer看作是一种临时buffer,而临时buffer的名称通常会以一个
*
字符开头,在这里仍然遵循这样的命名约定,在机器名称的前面添加一个了
*
前缀。

要让这个函数工作,我们需要把它加入到一个hook变量
comint-input-filter-functions
当中。
(add-hook'comint-input-filter-functions'rename-buffer-in-ssh-login)

comint-input-filter-functions
是一个comint-mode的hook。Shell-mode实际上是由comint-mode派生出来的,所以comint-mode的hook在Shell-mode里面也能够工作。

comint-mode或者Shell-mode在将输入到buffer中的命令传递给后台进程(在这里是Shell进程)去执行之前,会首先运行
comint-input-filter-functions
hook当中的函数,同时将输入的命令作为参数传递给该中的函数。所以我们的
rename-buffer-in-ssh-login
函数就可以跟踪输入到
buffer当中的每一条命令,当发现有类似
sshmsg@hostA.cn.ibm.com
或者
sshmsg@hostB
这样的命令的时候,就会执行预定的操作。同时正则表达式的设计还避免了在类似
sshmsg@hostA.cn.ibm.com
ls/opt/IBM
这样不以登录为目的的远程命令上面出现误动作的机会。

看到这里细心的读者也许注意到了一个细节,就是上面的代码里面被注释掉了两行内容。尤其是其中的第一行将截取下来的机器名加入到了一个
shell-buffer-name-list
的列表里面。实际上这段代码的存在是为了跟踪Shellbuffer名称的变化过程,然后配合另外一个函数
rename-buffer-in-ssh-exit
,在退出每一次
ssh登录的时候将Shellbuffer的名称再改回来原来的样子。但是由于实际应用的复杂性,目前为止还没有找到一个十分满意的实现方案。有兴趣的读者可以尝试自己实现这个函数。

Shellbuffer的退出

进入的问题解决了,下面让我们来看一看退出的时候会有哪些问题。

当用户退出Shell会话之后,Emacs并不会删除这个Shellbuffer,而是把它留在那里,等待用户的进一步的处理。
dove@bash-4.1$exit
exit

Processshellfinished

如果用户这个时候再次执行
M-xshell
命令,Emacs会再次复用这个buffer。
dove@bash-4.1$
dove@bash-4.1$exit
exit

Processshellfinished

dove@bash-4.1$

首先这其实是一个非常正确的设计。因为Shellbuffer里面的内容通常是非常重要的。甚至于有些时候我会在结束一天的工作之后把某一些Shellbuffer保存成文件,以备日后查阅。这里面不仅仅有这一天以来执行过的所以命令的记录,还有所有这些命令的输出信息,甚至当我先后登录了几台不同的机器进行了不同的操作,所有这些工作也都记录在这个Shellbuffer当中,可以说这个buffer就是我这一天以来所有足迹的记录。试想想,还有什么地方能够提供这么完整、详细的工作记录?另外还有什么地方能够提供如此方便的搜索功能?甚至连命令的输出信息都可以随意搜索?

但是,很快我就习惯了正确处理我的Shellbuffer。对于主要的buffer我已经习惯在退出之前就把它保存好了,那么这个时候是不是可以告诉Emacs不用这么拘谨了呢?事实上这个事情还真不好办。我曾经试图用
comint-output-filter-functions
hook去捕捉
Processshellfinished
这样的信息,但是这样的信息是在
comint-mode已经退出以后才由Emacs输出的,因此在这个hook里面完全捕捉不到。

直到有一天在翻看Emacs源代码的时候突然看到了
set-process-sentinel
这个函数才找到了解决方案。
set-process-sentinel
函数可以对一个特定的进程设置一个“哨兵”,当这个进程的状态发生变化的时候(比如说进程结束的时候),“哨兵”就会通知Emacs调用相应的函数来完成预定的工作。有了这个方案,我们只需要把删除
Shellbuffer的函数关联到正确的进程上就行了。

下面就是这两个函数:

清单2.两个函数


(defunkill-shell-buffer(processevent)
"Theoneactuallykillshellbufferwhenexit."
(kill-buffer(process-bufferprocess))
)

(defunkill-shell-buffer-after-exit()
"killshellbufferwhenexit."
(set-process-sentinel(get-buffer-process(current-buffer))
#'kill-shell-buffer)
)

其中
kill-shell-buffer
的作用是删除进程对应的buffer;
kill-shell-buffer-after-exit
函数的作用就是把
kill-shell-buffer
函数关联到正确的进程上去。然后当我们把这个函数加入到
shell-mode-hook
当中后,就可以在每次打开
Shellbuffer的时候得到正确的进程信息了。
(add-hook'shell-mode-hook'kill-shell-buffer-after-exitt)

outlineinShellMode

这一节我们谈outline-mode。Outline-mode是GNUEmacs的一个非常好用的写作模式。使用outline-mode可以轻松方便的操作结构化文档,可以将文档内容分级展开,或者逐级隐藏,既能总揽全局,又可深入细节。outline-mode是如此精彩,以至于CarstenDominik教授在此基础上开发出了强大的orgmode。

在这一节当中我们将要讨论一下如何将outline-mode的强大功能应用到Shell-mode当中。在进入细节之前,让我们先对Outline-mode进行一个简单的介绍。

Outlinemode当中,文档中的内容被分成两种结构,一种是“标题”,一种是“内容”。其中的“标题”又可以根据需要分成大小不同的级别。在对文档的内容进行折叠和展开操作的时候就是以这些“标题”的级别为依据的。例如下面这段摘自GNUEmacsManual的示例:
*Food
Thisisthebody,
whichsayssomethingaboutthetopicoffood.

**DeliciousFood
Thisisthebodyofthesecond-levelheader.

**DistastefulFood
Thiscouldhave
abodytoo,with
severallines.

***DormitoryFood

*Shelter
Anotherfirst-leveltopicwithitsheaderline.

当我们折叠起这段文档的时候,分别可以折叠成这样的形式
*Food...
*Shelter...

或者这样的形式
*Food...
**DeliciousFood...
**DistastefulFood
*Shelter...

或者我们又可以将
DeliciousFood
单独展开
*Food...
**DeliciousFood
Thisisthebodyofthesecond-levelheader.

**DistastefulFood
*Shelter...

那么这些示例和Shellmode又有什么关系呢?如果我们把Shellbuffer里的*命令*看作outline-mode的“标题”,将命令产生的输出看作是“内容”,那么是不是就可以像折叠起一篇普通的结构化文档那样将所有的Shell命令都折叠起来呢?就像下面这个示例所展示的这样:

清单3.示例


dove@bash-4.1$cd~/org...
2:2001:11:23:10:~/org
dove@bash-4.1$ls*.el
calendar-setup.eldove-ext.elorg-mode.elsettings.el
color-theme.elkeybindings.elplugins.el

dove@bash-4.1$eework.org&...
dove@bash-4.1$WaitingforEmacs...
dove@bash-4.1$ls...
dove@bash-4.1$eesettings.el&...
dove@bash-4.1$WaitingforEmacs...
dove@bash-4.1$cd~/...
dove@bash-4.1$ls...
dove@bash-4.1$...

当我们把Shellbuffer里面的内容全部折叠起来,我们就看到了一条时间线。既能够于一瞥之间总览全部的历史,又可以随时深入任何一条命令的细节。相比与仅能告诉我们曾经做过什么的
history
命令来说,这样的场景更像是一部“时间机器”。

那么该怎样实现这样的梦想呢?其中的关键就是要让outline-mode能够认出我们的“标题”。在outline-mode里面缺省的“标题”是一个
*
,这个
*
从文本行的第一个字符开始匹配,匹配上的,就是“标题”,匹配不上的,就是“内容”,匹配的次数越多,“标题”的级别越低。我们可以通过设置
outline-regexp
变量的值来定义我们自己的“标题”。在
Shellmode里面一个可行的办法就是将Shell提示符的内容定义为“标题”。如同下面的示例这样:
(setqoutline-regexp".*[bB]ash.*[#\$]")

设置标题以后,在Shellmode里面输入
M-xoutline-minor-mode
就可以享受outline-mode带来的便利了。例如上文示例中所示的结果使用一下三个操作就可以实现:

输入
M-xhide-body
或者
M-xhide-all
命令折叠起Shellbuffer里的所有命令

移动光标到
ls*.el
所在的行

使用
M-xshow-entry
或者
M-xshow-subtree
命令展开
ls*.el
命令

EnhancedoutlineinShellMode

在上一节里面讲述了通过设置
outline-regexp
变量,使
outline-minor-mode
可以在shell-mode中工作的方法,但是这样简单的设置很难避免会有一些负面的影响。因为
outline-regexp
变量是一个全局变量,所以对
outline-regexp
的值势必改变其他模式中的
outline-minor-mode
的行为方式,而这肯定不是你所希望的。

所以我在工作当中实际使用的是另外一种相对复杂一些的方法:使用一个函数为每一个buffer设置分别的
outline-regexp
,并且把
outline-regexp
变量修改为特定buffer范围内的局部变量。下面就是这个函数:

清单4.设置buffer的函数


(defunset-outline-minor-mode-regexp()
""
(let((find-regexp
(lambda
(lstmode)
""
(let
((innerList
(carlst)))
(ifinnerList
(if
(string=
(carinnerList)
mode)
(car
(cdrinnerList))
(progn
(poplst)
(funcallfind-regexplstmode))))))))
(outline-minor-mode1)
(make-local-variable'outline-regexp)
(setqoutline-regexp(funcallfind-regexpoutline-minor-mode-listmajor-mode))))

这个函数首先定义了一个匿名函数,存储在
find-regexp
变量中,这个函数通过递归的方式遍历一个嵌套列表,直至找到与给定模式对应的值;然后启动
outline-minor-mode
,修改
outline-regexp
为局部变量,然后调用上述的匿名函数设置正确的
outline-regexp


要让这个函数能够工作,我们就需要把他加入到各个主模式的hook之中,如同下面的示例所示:

清单5.示例


(add-hook'shell-mode-hook'set-outline-minor-mode-regexpt)
(add-hook'sh-mode-hook'set-outline-minor-mode-regexpt)
(add-hook'emacs-lisp-mode-hook'set-outline-minor-mode-regexpt)
(add-hook'perl-mode-hook'set-outline-minor-mode-regexpt)

但是细心的读者应该看到了,这个
set-outline-minor-mode-regexp
函数并没有接受任何参数,这是因为这些主模式在调用hook函数的时候是不会向它们传递任何参数的。那么我们需要的的数据从哪里来呢?显然这里需要一个全局变量
outline-minor-mode-list
来存储
set-outline-minor-mode-regexp
函数所需的所有数据。

清单6.全局变量outline-minor-mode-list


(setqoutline-minor-mode-list
(list'(emacs-lisp-mode"(defun")
'(shell-mode".*[bB]ash.*[#\$]")
'(sh-mode"function.*{")
'(perl-mode"sub")

))

有了这些扩展,Emacs就可以在创建一个新的buffer的时候,为这个buffer设置正确的
outline-regexp
值了。

延伸阅读hook

一些读者可能注意到,在本文的叙述中多次提到了hook这一概念,那么hook究竟是什么东西?他在Emacs里面有起到什么作用呢?在这里我给大家做一个简要的介绍。

简单来讲,hook就是一个存储函数列表的Lisp变量,该列表里的每一个函数被称作这个hook的一个hook函数。GNUEmacs的很多主模式(majormodes)在完成初始化之后都会尝试寻找并调用对应该模式的hook变量里面的hook函数。因此hook就成为定制Emacs过程中一个非常重要的机制。我们可以通过添加hook函数的方式轻松的定制或扩展Emacs的行为。

最简单的hook用法就是直接调用已有的Emacs函数,例如启动特定的子模式(minormodes):
(add-hook'shell-mode-hook'outline-minor-modet)

更加复杂的用法就如上文所示,编写自己的hook函数。

关于hook有几个细节需要注意

绝大多数普通hook变量的名称都是在主模式的名称后面加上
-hook
后缀来构成的

但是,并不是所有hook变量都是这样命名的

绝大多数普通hook函数被调用的时候是不会向它传递任何参数的,同时也不会理会函数的返回结果的

但是,并不是所有hook函数都是这样调用的

已经装入的hook函数将无法通过再次执行
add-hook
来进行覆盖或修改。实际的结果将会装入该hook函数的多个版本。解决的办法之一是清除hook变量,然后再次装入:

(setq'shell-mode-hooknil)
(add-hook'shell-mode-hook'outline-minor-modet)


大话EmacsShellMode第3部分

定制Emacs环境下的Shell窗口

简介:这是《大话EmacsShellMode》的最后一篇。这一篇中介绍了GNUEmacs下窗口操作的一些高级技巧和扩展函数的编写方法。通过这些扩展,可以对Emacs环境下的窗口与缓冲区进行灵活自如的配置,甚至旋转,由此打造一个更加灵活,舒适的Shell工作环境。

窗口篇再叙

在《大话EmacsShellMode》的第一部分里面介绍了GNUEmacs多窗口工作模式的一些特点。简单、便捷而又富于变化的多窗口工作模式使得在GNUEmacs当中进行上下文参照和进行多任务处理成为很方便的事情。比如按下组合键
Ctrl-x2
或者
Ctrl-x3
就是最常见的两窗口模式,多按几下这些组合键就是经典的四窗口模式。或者,还可以更简单一些,编写一个函数,把四窗口模式的创建绑定到一个组合键上去。

清单1.四窗口模式的创建绑定到一个组合键上去


;+----------+-----------+
;|||
;|||
;+----------+-----------+
;|||
;|||
;+----------+-----------+

(defunsplit-window-4()
"Splitewindowinto4sub-window"
(interactive)
(if(=1(length(window-list)))
(progn(split-window-vertically)
(split-window-horizontally)
(other-window2)
(split-window-horizontally)
)
)
)

(global-set-key(kbd"C-x44")

这样是不是比起在
.Xdefaults
文件当中精心计算每一个窗口的geometry要舒服多了?虽然
.Xdefaults
文件的设置看似可以“一劳永逸”,但是想一想某天心血来潮要调整一下Xterminal里的字体大小的时候会发生什么?还有一点不要忘记,在GNUEmacs里面窗口模式与里面的内容是相互“分离”的,他们并不是绑在一起的。所以可以随时将同一个Shell
buffer里面的内容展示在两个不同的窗口里面进行上下文的参照,这个优势是Xterminal的窗口方式完全无法提供的了。

常见的多窗口模式存在的问题

当然,事情并非表面上看起来那么完美。当我们仔细审视就会发现,现有的多窗口工作方式依然存在一些不完善的地方。两窗口的模式显然很明显了,窗口太少,多任务处理的时候明显不够使。四窗口(或者更多的六窗口、九窗口,如果你有更大的显示器的话)模式虽然在实践当中是一种比较常见的使用方式,但是呢,窗口越多,尺寸越小,很多时候都会遇到面积不够用的情况,比如说打开一个较大的日志文件,或者就是简单的执行了一条长模式的Shell命令(
ls
-l
或者
ps-ef
……),这个时候狭小的显示面积就会成为每一个小窗口无法释怀的痛。人们通常的做法常常是临时放大当前这个窗口,阅读完毕之后再恢复回去。

清单2.临时放大当前这个窗口


+----------+-----------++----------------------+
|||/\||
|||/+-------+\||
+----------+-----------+\+-------+/||
|||\/||
|||||
+----------+-----------++----------------------+

在GNUEmacs里面可以使用
C-x1
组合键放大窗口,然后使用
C-c<left>
组合键[1]。(如果你使用的是Xterminal,在Gnome桌面环境下可以使用
Alt-F10
组合键放大窗口,
Alt-F5
组合键还原窗口
)

但是,一旦这种切换变得非常频繁的时候(想想
ls-l
是一个多么常用的命令吧),就会成为一个恼人的负担。优秀的工具应该用起来是舒心的。切换即使不能完全避免,至少也应该尽可能的减少。如果一个窗口需要频繁运行长模式的命令,为什么不干脆给它一个大尺寸呢?说的没错。既要有大尺寸,又要有多窗口,真正平衡的选择实际上应该是三窗口模式。

三窗口模式

三窗口模式是大尺寸和多窗口的一个很好的平衡。总有一个大窗口,要么长度是全尺寸的,要么高度是全尺寸的,将需要较大显示面积的任务放到这里运行,尽量减少切换的次数,让我们能够更加关注于自己的工作。

当然了,针无两头尖,新方案的引入也同时带来了新问题。三窗口模式有两种不同的表现方式,究竟选择哪一种就成了一个问题。

清单3.三窗口模式有两种不同的表现方式


+----------+-----------++-----------+-----------+
||||||
||||||
+----------+-----------+|+-----------+
|||||
|||||
+----------------------++-----------+-----------+

很显然,横向排布的大窗口更适合运行长模式命令的工作状况,而竖向排列的大窗口更适合检查日志文件时的工作状况。这样的工作状况在现实中是不断的交替出现的,根本就没有一个非黑即白的状况。如果可以两者兼得,而不是必须作出选择,那是多么美妙的事情啊!

这就是GNUEmacs充分发挥灵活性的优势的地方了。只需要编写两个简单的函数就让GNUEmacs在这两种表现方式之间自由切换,就不必非要做出选择了。

让窗口旋转

横向到竖向的旋转

下面这个函数是把横向布局的三窗口模式转变成竖向布局和三窗口模式。代码的注释部分的示意图形象地说明了函数的功能。

清单4.函数的功能


;+----------------------++------------+-----------+
;||\|||
;||+-------+\|||
;+----------+-----------++-------+/|+-----------+
;|||/|||
;||||||
;+----------+-----------++------------+-----------+

(defunsplit-v-3()
"Change3windowstylefromhorizontaltovertical"
(interactive)

(select-window(get-largest-window))
(if(=3(length(window-list)))
(let((winList(window-list)))
(let((1stBuf(window-buffer(carwinList)))
(2ndBuf(window-buffer(car(cdrwinList))))
(3rdBuf(window-buffer(car(cdr(cdrwinList))))))
(message"%s%s%s"1stBuf2ndBuf3rdBuf)

(delete-other-windows)
(split-window-horizontally)
(set-window-buffernil1stBuf)
(other-window1)
(set-window-buffernil2ndBuf)
(split-window-vertically)
(set-window-buffer(next-window)3rdBuf)
(select-window(get-largest-window))
))))

竖向到横向的旋转

这个函数是把竖向布局的三窗口模式转换成横向布局。

清单5.转换成横向布局


;+------------+-----------++----------------------+
;|||\||
;|||+-------+\||
;|+-----------++-------+/+----------+-----------+
;|||/|||
;||||||
;+------------+-----------++----------+-----------+

(defunsplit-h-3()
"Change3windowstylefromverticaltohorizontal"
(interactive)

(select-window(get-largest-window))
(if(=3(length(window-list)))
(let((winList(window-list)))
(let((1stBuf(window-buffer(carwinList)))
(2ndBuf(window-buffer(car(cdrwinList))))
(3rdBuf(window-buffer(car(cdr(cdrwinList))))))
(message"%s%s%s"1stBuf2ndBuf3rdBuf)

(delete-other-windows)
(split-window-vertically)
(set-window-buffernil1stBuf)
(other-window1)
(set-window-buffernil2ndBuf)
(split-window-horizontally)
(set-window-buffer(next-window)3rdBuf)
(select-window(get-largest-window))
))))

这两个函数很简单。他们实际上只做了三件事:

1.首先移动到最大窗口

2.判断当前是否是三窗口布局,如果是,保存当前活动缓冲区的名称

3.生成新的窗口布局,同时将保存的活动缓冲区放回对应的窗口中。

就是这么简单,其实只是将手工操作的过程交由GNUEmacs去做了而已。如果我们把这两个函数绑定到两个组合键上就可以交工了。

只是,这样做显然有点不够精彩。尤其是还需要用户去操心应该在什么状况下调用哪一个函数,这是一件很讨厌的事情。应该把这两个函数结合起来,然后让GNUEmacs去操心在什么时候应该做什么,这样才舒服嘛。

让Emacs自己选择旋转

上节说的就是下面这个函数。在这个函数当中GNUEmacs去操心当前究竟是哪种模式(比较一下窗口的宽高就知道了嘛),以及应该旋转成哪种模式(当然是“另一种”模式啦,一共就两种嘛)。

清单6.让Emacs自己选择旋转函数


;+------------+-----------++------------+-----------+
;|||\|||
;|||+-------+\|||
;+------------+-----------++-------+/+------------+|
;||/|||
;|||||
;+------------+-----------++------------+-----------+
;+------------+-----------++------------+-----------+
;|||\|||
;|||+-------+\|||
;|+-----------++-------+/+------------+-----------+
;|||/||
;|||||
;+------------+-----------++------------+-----------+

(defunchange-split-type-3()
"Change3windowstylefromhorizontaltoverticalandvice-versa"
(interactive)

(select-window(get-largest-window))
(if(=3(length(window-list)))
(let((winList(window-list)))
(let((1stBuf(window-buffer(carwinList)))
(2ndBuf(window-buffer(car(cdrwinList))))
(3rdBuf(window-buffer(car(cdr(cdrwinList)))))

(split-3
(lambda(1stBuf2ndBuf3rdBufsplit-1split-2)
"change3windowfromhorizontaltoverticalandvice-versa"
(message"%s%s%s"1stBuf2ndBuf3rdBuf)

(delete-other-windows)
(funcallsplit-1)
(set-window-buffernil2ndBuf)
(funcallsplit-2)
(set-window-buffer(next-window)3rdBuf)
(other-window2)
(set-window-buffernil1stBuf)))

(split-type-1nil)
(split-type-2nil)
)
(if(=(window-width)(frame-width))
(setqsplit-type-1'split-window-horizontally
split-type-2'split-window-vertically)
(setqsplit-type-1'split-window-vertically
split-type-2'split-window-horizontally))
(funcallsplit-31stBuf2ndBuf3rdBufsplit-type-1split-type-2)

))))

现在我只需要把这个函数绑定在
C-x4c
组合键上,就可以方便、快捷的在三窗口模式的两种布局之间进行方便的切换。
(global-set-key(kbd"C-x4c")(quotechange-split-type-3))

让Buffer旋转

现在窗口可以自由切换了,那么假如我想保持窗口不动,仅仅只是旋转一下窗口里的内容,可不可以呢?比如说某个较小窗口里的Shell缓冲区将会运行一些长模式的Shell命令,我现在希望把它切换到较大的窗口里来,以避免频繁的窗口放大,这样可不可以呢?

实际上这个功能的比起上文所示的窗口旋转要简单的多了。因为窗口不动了,只需要把当前活动缓冲区的名称存储起来,再重新安放一下就行了。下面这个函数就提供了将三窗口布局中的缓冲区进行顺时针旋转的功能。

清单7.进行顺时针旋转的功能


;+------------+-----------++------------+-----------+
;||C|\||A|
;|||+-------+\|||
;|A|-----------|+-------+/|B|-----------|
;||B|/||C|
;||||||
;+------------+-----------++------------+-----------+
;
;+------------------------++------------------------+
;|A|\|B|
;||+-------+\||
;+------------+-----------++-------+/+------------+-----------+
;|B|C|/|C|A|
;||||||
;+------------+-----------++------------+-----------+

(defunroll-v-3(&optionalarg)
"Rolling3windowbuffers(anti-)clockwise"
(interactive"P")
(select-window(get-largest-window))
(if(=3(length(window-list)))
(let((winList(window-list)))
(let((1stWin(carwinList))
(2ndWin(car(cdrwinList)))
(3rdWin(car(lastwinList))))
(let((1stBuf(window-buffer1stWin))
(2ndBuf(window-buffer2ndWin))
(3rdBuf(window-buffer3rdWin)))
(ifarg(progn
;anti-clockwise
(set-window-buffer1stWin3rdBuf)
(set-window-buffer2ndWin1stBuf)
(set-window-buffer3rdWin2ndBuf))
(progn;clockwise
(set-window-buffer1stWin2ndBuf)
(set-window-buffer2ndWin3rdBuf)
(set-window-buffer3rdWin1stBuf))
))))))

通过把这个函数绑定在
C-x4r
组合键上,就可以方便、快捷的在让窗口里面的内容进行顺时针旋转,同时保持窗口布局不变。如果你想要逆时针旋转的话,只需要在组合键前面加上一个任意的数字前缀(
C-u1
或者
M-2
)就行了。
(global-set-key(kbd"C-x4r")(quoteroll-v-3))

延伸阅读

在上文的让Emacs自己选择旋转一节中,细心的读者可能注意到了
change-split-type-3
函数并没有把
split-v-3
split-h-3
的代码简单的合并在一个
if
else
结构里面,事实上在
change-split-type-3
函数里面和那两个函数一样,都只有一套清理环境并且生成新窗口的代码。

清单8.代码


(delete-other-windows)
(funcallsplit-1)
(set-window-buffernil2ndBuf)
(funcallsplit-2)
(set-window-buffer(next-window)3rdBuf)
(other-window2)
(set-window-buffernil1stBuf)))

为什么用一套代码可以完成两件不同的任务呢?这个就是Lisp语言的强大的特性之一。在Lisp语言当中函数数据这两种对象被使用一种的数据结构(
list
)来表示和存储,由此使得函数可以像数据一样作为参数在函数间传递。换句话说,Lisp语言当中的函数除了可以接受数据作为参数以外,还可以接受指令(
函数)作为参数。

基于这个特性,我们就可以对所执行的任务在更高的逻辑层次上进行抽象。例如上文所述的两种转换窗口布局的任

务在这个层次上被抽像为下面的任务:

1.删除当前窗口之外的所有窗口

2.对当前窗口进行第一次切分

3.设置窗口一的缓冲区

4.对当前窗口进行第二次切分

5.设置窗口二的缓冲区

6.跳至下一个窗口

7.设置最后一个窗口的缓冲区

这个时候,两件不同的任务就变成了同一类任务了。唯一的区别仅仅只是第一次切分和第二次切分的时候究竟是竖着切还是横着切。这一点点区别就交给指令去处理了。传给它什么样的切分指令,它就会按照什么样的指令进行切分。这样不仅极大的简化了函数的代码,而且使函数的逻辑更加接近于现实世界中的行为方式了。

在今天,这样的语言特性已经不是Lisp独有了。但是实现这些特性的前提都是要能够用相同的数据结构来表示函数与数据。例如在Perl语言当中通过使用对函数的引用(reference),来实现指令的传递。因为引用和普通数据一样,在Perl语言当中都是scalar数据结构。

小技巧

1.以上所述的多窗口工作方式即使是在字符终端的环境下依然可以工作。因为GNUEmacs的窗口功能要远早于XWindow的出现,所以不依赖于XWindow的存在。

2.上文代码中的示意图是用GNUEmacs的artist-mode绘制的。当你启动这种模式之后,整个缓冲区就变成了一块画布,以鼠标作笔就可以进行任意的挥洒了。

参考资料

生活在Emacs中:developerWorks中的一篇Emacs入门教程。也是作者本人的Emacs入门教程。

GNUEmacsManual:来自GNU.org网站的GNUEmacs官方文档。

GNUEmacsLispReferenceManual:来自GNU.org网站的GNUEmacsLisp参考手册。

EmacsWiki:汇聚大量Emacs爱好者贡献的Emacs扩展代码的维基百科。

EmacsWikipedia中的Emacs词条。包括GNUEmacs,XEmacs以及其他各种Emacs实现。

MulticsEmacs:TheHistory,DesignandImplementation这是一篇简短的历史介绍,讲述了Emacs最初诞生的历史过程。

FreeasinFreedom:是Emacs的作者之一,RichardStallman的自由软件奠基之作。

GNU工程和自由软件基金会:从其网站了解关于GNU工程和自由软件基金会的更多信息。

RichardStallman'sPersonalHomePageGNUEmacs之父,也是GNU工程和自由软件奠基人RichardStallman的个人网站。

关于作者

杨博华,10年以上的计算机行业从业经验,最近5年在IBM从事软件测试工程师工作。具有10年以上OpenSource软件的使用经验。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: