Emacs中Shell环境如何扩展和定制

58次阅读
没有评论

共计 8311 个字符,预计需要花费 21 分钟才能阅读完成。

这篇文章主要为大家展示了“Emacs 中 Shell 环境如何扩展和定制”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让丸趣 TV 小编带领大家一起研究并学习一下“Emacs 中 Shell 环境如何扩展和定制”这篇文章吧。

进入和退出 Shell Mode

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

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

Shell buffer 的进入

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

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

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

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

(global-set-key (kbd  C-c z) (quote shell))

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

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

(defun rename-buffer-in-ssh-login (cmd)
 Rename buffer to the destination hostname in ssh login 
(if (string-match  ssh [-_a-z0-9A-Z]+@[-_a-z0-9A-Z.]+[ ]*[^-_a-z0-9-A-Z]*$  cmd)
(let (( host (nth 2 (split-string cmd  [ @\n]  t) )))
(rename-buffer (concat  *  host)) ;
(add-to-list  shell-buffer-name-list (concat  *  host));
(message  %s  shell-buffer-name-list)
)

这个函数会分析提供给它的命令。如果匹配预先定义的正则表达式,则截取 @字符后面的机器名,然后使用 rename-buffer 命令修改当前 buffer   的名称。另外,由于在 GNU Emacs 的默认约定里将 Shell buffer 看作是一种临时 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   当中的每一条命令,当发现有类似 ssh msg@hostA.cn.ibm.com 或者 ssh  msg@hostB 这样的命令的时候,就会执行预定的操作。同时正则表达式的设计还避免了在类似 ssh msg@hostA.cn.ibm.com ls  /opt/IBM 这样不以登录为目的的远程命令上面出现误动作的机会。

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

Shell buffer 的退出

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

当用户退出 Shell 会话之后,Emacs 并不会删除这个 Shell buffer,而是把它留在那里,等待用户的进一步的处理。

dove@bash-4.1$exit
Process shell finished

如果用户这个时候再次执行 M-x shell 命令,Emacs 会再次复用这个 buffer。

dove@bash-4.1$
dove@bash-4.1$exit
Process shell finished
dove@bash-4.1$

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

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

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

下面就是这两个函数:

清单 2. 两个函数

(defun kill-shell-buffer(process event)
 The one actually kill shell buffer when exit.  
(kill-buffer (process-buffer process))
(defun kill-shell-buffer-after-exit()
 kill shell buffer when exit. 
(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 当中后,就可以在每次打开 Shell  buffer 的时候得到正确的进程信息了。

(add-hook  shell-mode-hook  kill-shell-buffer-after-exit t)

outline in Shell Mode

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

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

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

* Food
This is the body,
which says something about the topic of food.
** Delicious Food
This is the body of the second-level header.
** Distasteful Food
This could have
a body too, with
several lines.
*** Dormitory Food
* Shelter
Another first-level topic with its header line.

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

* Food...
* Shelter...

或者这样的形式

* Food...
** Delicious Food...
** Distasteful Food
* Shelter...

或者我们又可以将 Delicious Food 单独展开

* Food...
** Delicious Food
This is the body of the second-level header.
** Distasteful Food
* Shelter...

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

清单 3. 示例

dove@bash-4.1$ cd ~/org...
2 : 2001 : 11:23:10 : ~/org
dove@bash-4.1$ ls *.el
calendar-setup.el dove-ext.el org-mode.el settings.el
color-theme.el keybindings.el plugins.el
dove@bash-4.1$ ee work.org  ...
dove@bash-4.1$ Waiting for Emacs...
dove@bash-4.1$ ls...
dove@bash-4.1$ ee settings.el  ...
dove@bash-4.1$ Waiting for Emacs...
dove@bash-4.1$ cd~/...
dove@bash-4.1$ ls...
dove@bash-4.1$ ...

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

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

(setq outline-regexp  .*[bB]ash.*[#\$] )

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

输入 M-x hide-body 或者 M-x hide-all 命令折叠起 Shell buffer 里的所有命令

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

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

Enhanced outline in Shell Mode

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

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

清单 4. 设置 buffer 的函数

(defun set-outline-minor-mode-regexp ()
(let ((find-regexp
(lambda
(lst mode)
((innerList
(car lst)))
(if innerList
(string=
(car innerList)
mode)
(cdr innerList))
(progn
(pop lst)
(funcall find-regexp lst mode))))))))
(outline-minor-mode 1)
(make-local-variable  outline-regexp)
(setq outline-regexp (funcall find-regexp outline-minor-mode-list major-mode))))

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

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

清单 5. 示例

(add-hook  shell-mode-hook  set-outline-minor-mode-regexp t )
(add-hook  sh-mode-hook  set-outline-minor-mode-regexp t )
(add-hook  emacs-lisp-mode-hook  set-outline-minor-mode-regexp t )
(add-hook  perl-mode-hook  set-outline-minor-mode-regexp t )

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

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

(setq outline-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 函数。GNU Emacs   的很多主模式 (major modes) 在完成初始化之后都会尝试寻找并调用对应该模式的 hook 变量里面的 hook 函数。因此 hook 就成为定制  Emacs 过程中一个非常重要的机制。我们可以通过添加 hook 函数的方式轻松的定制或扩展 Emacs 的行为。

最简单的 hook 用法就是直接调用已有的 Emacs 函数,例如启动特定的子模式(minor modes):

(add-hook  shell-mode-hook  outline-minor-mode t)

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

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

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

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

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

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

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

(setq  shell-mode-hook nil)
(add-hook  shell-mode-hook  outline-minor-mode t)

以上是“Emacs 中 Shell 环境如何扩展和定制”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注丸趣 TV 行业资讯频道!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-08-16发表,共计8311字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)