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

当 Shell 遇见 Emacs -- 大话 Emacs Shell Mode


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


第一回 引子

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

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

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

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

 (setq shell-file-name "/bin/bash") 

或者

 export EMACSSHELL=/usr/bin/zsh 

另外如果你希望使用一个支持 ANSI color 的 Shell 进程,那么最好在你的 Emacs 配置文件里面加入下面两行,以便在执行 ls – color=auto 命令的时候输出的色彩信息能够被 Emacs 正确解析。

 (autoload 'ansi-color-for-comint-mode-on "ansi-color" nil t) 
 (add-hook 'shell-mode-hook 'ansi-color-for-comint-mode-on t) 

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

第二回 初识 Shell mode -- 窗口篇

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

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

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

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

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

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

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

输入 Ctrl-x 0可以关闭当前光标所在的窗口。

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

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

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

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

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

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

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

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

技巧一

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

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

 (global-set-key (kbd "C-c z") 'shell) 
 (global-set-key (kbd "<f10>") 'rename-buffer) 

技巧二

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

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

只需要在你的 Emacs 配置文件里面加入下面几行

 (when (fboundp 'winner-mode) 
  (winner-mode) 
  (windmove-default-keybindings)) 

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

第三回 甜蜜约会 -- buffer 篇

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

输入

我在开头的引子部分曾说过,在 Shell mode 中工作的时候,用户实际上接触的是一个文本缓冲区,实际上并没有直接的跟 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 -1 23.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 -1 23.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.el 的)以文本的形式添加到这个缓冲区的末尾。

这个例子并不仅仅是列一个目录那么简单,事实上他提供了一个更加强大的工作方式 —— 曾经只能用来阅读的命令输出现在也可以被用来构造新的命令了。让我们再来看一个新的例子,在这个例子中我们将把这种能力与 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 
 head auctex.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$ head auctex.el 
 ;;; auctex.el 
 ;; 
 ;; This can be used for starting up AUCTeX.  The following somewhat 
 ;; strange trick causes tex-site.el to be loaded in a way that can be 
 ;; safely undone using (unload-feature 'tex-site). 
 ;; 
 (autoload 'TeX-load-hack 
  (expand-file-name "tex-site.el" (file-name-directory load-file-name))) 
 (TeX-load-hack) 


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

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

小技巧

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



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

对 Shell 环境的扩展和定制


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


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

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

进入和退出 Shell Mode

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

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

Shell buffer 的进入

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

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

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

接受默认的 buffer 名还有一个附加的好处——当你打开大量的 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 
 exit 

 Process shell finished 

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

 dove@bash-4.1$ 
 dove@bash-4.1$exit 
 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) 
             ""
             (let 
                 ((innerList 
                   (car lst))) 
               (if innerList 
                   (if 
                       (string= 
                        (car innerList) 
                        mode) 
                       (car 
                        (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 Mode 第 3 部分

定制 Emacs 环境下的 Shell 窗口

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


窗口篇再叙

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


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

 (defun split-window-4() 
 "Splite window into 4 sub-window"
 (interactive) 
 (if (= 1 (length (window-list))) 
     (progn (split-window-vertically) 
	    (split-window-horizontally) 
	    (other-window 2) 
	    (split-window-horizontally) 
	    ) 
   ) 
 ) 

 (global-set-key (kbd "C-x 4 4") 

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

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

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


清单 2. 临时放大当前这个窗口
				
 +----------+-----------+                 +----------------------+                      
 |          |           |    /       \    |                      |                      
 |          |           |   /+-------+\   |                      |                      
 +----------+-----------+   \+-------+/   |                      | 
 |          |           |    \       /    |                      | 
 |          |           |                 |                      |                      
 +----------+-----------+                 +----------------------+  

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

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


三窗口模式

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

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


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

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

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

让窗口旋转

横向到竖向的旋转

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


清单 4. 函数的功能
				
;  +----------------------+                 +------------+-----------+
;  |                      |           \     |            |           |
;  |                      |   +-------+\    |            |           |
;  +----------+-----------+   +-------+/    |            +-----------+
;  |          |           |           /     |            |           |
;  |          |           |                 |            |           |
;  +----------+-----------+                 +------------+-----------+

  (defun split-v-3 () 
  "Change 3 window style from horizontal to vertical"
  (interactive) 

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

	      (delete-other-windows) 
	      (split-window-horizontally) 
	      (set-window-buffer nil 1stBuf) 
	      (other-window 1) 
	      (set-window-buffer nil 2ndBuf) 
	      (split-window-vertically) 
	      (set-window-buffer (next-window) 3rdBuf) 
	      (select-window (get-largest-window)) 
	    )))) 

竖向到横向的旋转

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


清单 5. 转换成横向布局
				
;  +------------+-----------+                  +----------------------+
;  |            |           |            \     |                      |
;  |            |           |    +-------+\    |                      |
;  |            +-----------+    +-------+/    +----------+-----------+
;  |            |           |            /     |          |           |
;  |            |           |                  |          |           |
;  +------------+-----------+                  +----------+-----------+


 (defun split-h-3 () 
  "Change 3 window style from vertical to horizontal"
  (interactive) 

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

		 (delete-other-windows) 
		 (split-window-vertically) 
		 (set-window-buffer nil 1stBuf) 
		 (other-window 1) 
		 (set-window-buffer nil 2ndBuf) 
		 (split-window-horizontally) 
		 (set-window-buffer (next-window) 3rdBuf) 
		 (select-window (get-largest-window)) 
	      )))) 

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

1. 首先移动到最大窗口

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

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

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

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

让 Emacs 自己选择旋转

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


清单 6. 让 Emacs 自己选择旋转函数
				
;  +------------+-----------+                 +------------+-----------+
;  |            |           |            \    |            |           |
;  |            |           |    +-------+\   |            |           |
;  +------------+-----------+    +-------+/   +------------+           |
;  |                        |            /    |            |           |
;  |                        |                 |            |           |
;  +------------+-----------+                 +------------+-----------+
;  +------------+-----------+                 +------------+-----------+
;  |            |           |            \    |            |           |
;  |            |           |    +-------+\   |            |           |
;  |            +-----------+    +-------+/   +------------+-----------+
;  |            |           |            /    |                        |
;  |            |           |                 |                        |
;  +------------+-----------+                 +------------+-----------+

 (defun change-split-type-3 () 
  "Change 3 window style from horizontal to vertical and vice-versa"
  (interactive) 

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

                  (split-3 
                   (lambda(1stBuf 2ndBuf 3rdBuf split-1 split-2) 
                     "change 3 window from horizontal to vertical and vice-versa"
                     (message "%s %s %s" 1stBuf 2ndBuf 3rdBuf) 

                     (delete-other-windows) 
                     (funcall split-1) 
                     (set-window-buffer nil 2ndBuf) 
                     (funcall split-2) 
                     (set-window-buffer (next-window) 3rdBuf) 
                     (other-window 2) 
                     (set-window-buffer nil 1stBuf)))         

                  (split-type-1 nil) 
                  (split-type-2 nil) 
                  ) 
              (if (= (window-width) (frame-width)) 
                  (setq split-type-1 'split-window-horizontally 
                        split-type-2 'split-window-vertically) 
                (setq split-type-1 'split-window-vertically  
		       split-type-2 'split-window-horizontally)) 
              (funcall split-3 1stBuf 2ndBuf 3rdBuf split-type-1 split-type-2) 

 )))) 

现在我只需要把这个函数绑定在 C-x 4 c组合键上,就可以方便、快捷的在三窗口模式的两种布局之间进行方便的切换。

 (global-set-key (kbd "C-x 4 c") (quote change-split-type-3)) 

让 Buffer 旋转

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

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


清单 7. 进行顺时针旋转的功能
				
;  +------------+-----------+                   +------------+-----------+
;  |            |     C     |            \      |            |     A     |
;  |            |           |    +-------+\     |            |           |
;  |     A      |-----------|    +-------+/     |     B      |-----------|
;  |            |     B     |            /      |            |     C     |
;  |            |           |                   |            |           |
;  +------------+-----------+                   +------------+-----------+
;
;  +------------------------+                   +------------------------+
;  |           A            |           \       |           B            |
;  |                        |   +-------+\      |                        |
;  +------------+-----------+   +-------+/      +------------+-----------+
;  |     B      |     C     |           /       |     C      |     A     |
;  |            |           |                   |            |           |
;  +------------+-----------+                   +------------+-----------+
      
  
  (defun roll-v-3 (&optional arg) 
    "Rolling 3 window buffers (anti-)clockwise"
    (interactive "P") 
    (select-window (get-largest-window)) 
    (if (= 3 (length (window-list))) 
        (let ((winList (window-list))) 
          (let ((1stWin (car winList)) 
                (2ndWin (car (cdr winList))) 
                (3rdWin (car (last winList)))) 
            (let ((1stBuf (window-buffer 1stWin)) 
                  (2ndBuf (window-buffer 2ndWin)) 
                  (3rdBuf (window-buffer 3rdWin))) 
              (if arg (progn                                
 ; anti-clockwise
                        (set-window-buffer 1stWin 3rdBuf) 
                        (set-window-buffer 2ndWin 1stBuf) 
                        (set-window-buffer 3rdWin 2ndBuf)) 
                (progn                                      ; clockwise
                  (set-window-buffer 1stWin 2ndBuf) 
                  (set-window-buffer 2ndWin 3rdBuf) 
                  (set-window-buffer 3rdWin 1stBuf)) 
                )))))) 

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

 (global-set-key (kbd "C-x 4 r")  (quote roll-v-3)) 

延伸阅读

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


清单 8. 代码
				
 (delete-other-windows) 	                 
 (funcall split-1) 	      
 (set-window-buffer nil 2ndBuf) 	      
 (funcall split-2) 	      
 (set-window-buffer (next-window) 3rdBuf) 
 (other-window 2) 			      
 (set-window-buffer nil 1stBuf))) 

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

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

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

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

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

3. 设置窗口一的缓冲区

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

5. 设置窗口二的缓冲区

6. 跳至下一个窗口

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

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

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

小技巧

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

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


参考资料

关于作者

杨博华,10 年以上的计算机行业从业经验,最近 5 年在 IBM 从事软件测试工程师工作。具有 10 年以上 Open Source 软件的使用经验。



  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值