溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Emacs中Shell環(huán)境如何擴(kuò)展和定制

發(fā)布時(shí)間:2021-12-18 10:00:48 來源:億速云 閱讀:185 作者:小新 欄目:系統(tǒng)運(yùn)維

這篇文章主要為大家展示了“Emacs中Shell環(huán)境如何擴(kuò)展和定制”,內(nèi)容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領(lǐng)大家一起研究并學(xué)習(xí)一下“Emacs中Shell環(huán)境如何擴(kuò)展和定制”這篇文章吧。

進(jìn)入和退出 Shell Mode

輕輕的我走了,正如我輕輕的來;我輕輕的招手,作別西天的云彩。

但是在 Emacs Shell Mode 的缺省設(shè)計(jì)里面,沒有能夠讓我們?nèi)绱溯p松和優(yōu)雅的進(jìn)入與退出。這就是在這一節(jié)當(dāng)中我們要進(jìn)行定制和擴(kuò)展的地方。

Shell buffer 的進(jìn)入

首先是進(jìn)入。在本文的***部分有一個(gè)小技巧,介紹了在 GNU Emacs 中如何打開多個(gè) Shell buffer —— 我們需要將現(xiàn)有的 Shell  buffer 重命名,然后才能再次打開一個(gè)叫做 *shell*的 Shell buffer。這是 Emacs 創(chuàng)建 Shell buffer  時(shí)使用的默認(rèn)名稱。

這是一個(gè)很不優(yōu)雅的行為。這樣的細(xì)節(jié)工作應(yīng)該由 Emacs 事先料理好,我所需要的只是優(yōu)雅的進(jìn)入。實(shí)現(xiàn)這個(gè)目的有兩種做法,一種是在創(chuàng)建 Shell  buffer 的時(shí)候就把它修改成一個(gè)獨(dú)特的名字;另外一種做法是在創(chuàng)建出 Shell buffer 之后,根據(jù)用戶的使用情況來自動(dòng)修改 Shell buffer  的名稱。由于工作特點(diǎn)的關(guān)系,我選擇的是第二種方案。

在我的工作環(huán)境當(dāng)中,絕大多數(shù)時(shí)間都要登錄到遠(yuǎn)程的機(jī)器上去工作。所以我非常希望 Shell buffer  的名稱能夠被自動(dòng)修改成我所登錄的目標(biāo)機(jī)器的名稱,這樣在我登錄大量的機(jī)器進(jìn)行操作的時(shí)候,就可以方便的通過 buffer  名稱來進(jìn)行分辨。這就是我選擇第二套方案的原因。我首先接受 Emacs 創(chuàng)建出來的默認(rèn) buffer,然后在我登錄遠(yuǎn)程機(jī)器的時(shí)候 Emacs  會(huì)自動(dòng)為我改名。如果我沒有登錄遠(yuǎn)程機(jī)器,那么它將保持默認(rèn)的名稱,或者由我主動(dòng)的修改 buffer 名稱。

接受默認(rèn)的 buffer 名還有一個(gè)附加的好處——當(dāng)你打開大量的 buffer 進(jìn)行工作的時(shí)候,如果要回到這個(gè)默認(rèn)的 Shell  buffer,你不必在長長的 buffer 列表里面進(jìn)行切換,只需要執(zhí)行一個(gè)打開 Shell 的命令,也就是 M-x shell,Emacs  就會(huì)立刻把你帶到這個(gè)默認(rèn)的 Shell buffer 中來。為了能夠更加方便的打開 Shell,我把這個(gè)命令綁定到了 C-c z組合鍵上:

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

現(xiàn)在讓我們看一看 Emacs 是如何在我登錄遠(yuǎn)程機(jī)器的時(shí)候自動(dòng)修改 Shell buffer  的名稱的。實(shí)現(xiàn)這樣的功能首先需要編寫一個(gè)rename-buffer-in-ssh-login函數(shù):

清單 1. rename-buffer-in-ssh-login 函數(shù)

(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)
)
)
)

這個(gè)函數(shù)會(huì)分析提供給它的命令。如果匹配預(yù)先定義的正則表達(dá)式,則截取 @字符后面的機(jī)器名,然后使用 rename-buffer命令修改當(dāng)前 buffer  的名稱。另外,由于在 GNU Emacs 的默認(rèn)約定里將 Shell buffer 看作是一種臨時(shí) buffer,而臨時(shí) buffer 的名稱通常會(huì)以一個(gè)  *字符開頭,在這里仍然遵循這樣的命名約定,在機(jī)器名稱的前面添加一個(gè)了 *前綴。

要讓這個(gè)函數(shù)工作,我們需要把它加入到一個(gè) hook 變量 comint-input-filter-functions當(dāng)中。

(add-hook 'comint-input-filter-functions 'rename-buffer-in-ssh-login)

comint-input-filter-functions是一個(gè) comint-mode 的 hook。Shell-mode 實(shí)際上是由  comint-mode 派生出來的,所以 comint-mode 的 hook 在 Shell-mode 里面也能夠工作。

comint-mode 或者 Shell-mode 在將輸入到 buffer 中的命令傳遞給后臺(tái)進(jìn)程(在這里是 Shell  進(jìn)程)去執(zhí)行之前,會(huì)首先運(yùn)行comint-input-filter-functions hook  當(dāng)中的函數(shù),同時(shí)將輸入的命令作為參數(shù)傳遞給該中的函數(shù)。所以我們的 rename-buffer-in-ssh-login函數(shù)就可以跟蹤輸入到 buffer  當(dāng)中的每一條命令,當(dāng)發(fā)現(xiàn)有類似 ssh msg@hostA.cn.ibm.com 或者 ssh  msg@hostB這樣的命令的時(shí)候,就會(huì)執(zhí)行預(yù)定的操作。同時(shí)正則表達(dá)式的設(shè)計(jì)還避免了在類似 ssh msg@hostA.cn.ibm.com ls  /opt/IBM這樣不以登錄為目的的遠(yuǎn)程命令上面出現(xiàn)誤動(dòng)作的機(jī)會(huì)。

看到這里細(xì)心的讀者也許注意到了一個(gè)細(xì)節(jié),就是上面的代碼里面被注釋掉了兩行內(nèi)容。尤其是其中的***行將截取下來的機(jī)器名加入到了一個(gè)  shell-buffer-name-list的列表里面。實(shí)際上這段代碼的存在是為了跟蹤 Shell buffer 名稱的變化過程,然后配合另外一個(gè)函數(shù)  rename-buffer-in-ssh-exit,在退出每一次 ssh 登錄的時(shí)候?qū)?Shell buffer  的名稱再改回來原來的樣子。但是由于實(shí)際應(yīng)用的復(fù)雜性,目前為止還沒有找到一個(gè)十分滿意的實(shí)現(xiàn)方案。有興趣的讀者可以嘗試自己實(shí)現(xiàn)這個(gè)函數(shù)。

Shell buffer 的退出

進(jìn)入的問題解決了,下面讓我們來看一看退出的時(shí)候會(huì)有哪些問題。

當(dāng)用戶退出 Shell 會(huì)話之后,Emacs 并不會(huì)刪除這個(gè) Shell buffer,而是把它留在那里,等待用戶的進(jìn)一步的處理。

dove@bash-4.1$exit
exit
Process shell finished

如果用戶這個(gè)時(shí)候再次執(zhí)行 M-x shell命令,Emacs 會(huì)再次復(fù)用這個(gè) buffer。

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

首先這其實(shí)是一個(gè)非常正確的設(shè)計(jì)。因?yàn)?Shell buffer 里面的內(nèi)容通常是非常重要的。甚至于有些時(shí)候我會(huì)在結(jié)束一天的工作之后把某一些 Shell  buffer  保存成文件,以備日后查閱。這里面不僅僅有這一天以來執(zhí)行過的所以命令的記錄,還有所有這些命令的輸出信息,甚至當(dāng)我先后登錄了幾臺(tái)不同的機(jī)器進(jìn)行了不同的操作,所有這些工作也都記錄在這個(gè)  Shell buffer 當(dāng)中,可以說這個(gè) buffer  就是我這一天以來所有足跡的記錄。試想想,還有什么地方能夠提供這么完整、詳細(xì)的工作記錄?另外還有什么地方能夠提供如此方便的搜索功能?甚至連命令的輸出信息都可以隨意搜索?

但是,很快我就習(xí)慣了正確處理我的 Shell buffer。對于主要的 buffer 我已經(jīng)習(xí)慣在退出之前就把它保存好了,那么這個(gè)時(shí)候是不是可以告訴  Emacs 不用這么拘謹(jǐn)了呢?事實(shí)上這個(gè)事情還真不好辦。我曾經(jīng)試圖用 comint-output-filter-functionshook 去捕捉Process  shell finished這樣的信息,但是這樣的信息是在 comint-mode 已經(jīng)退出以后才由 Emacs 輸出的,因此在這個(gè) hook  里面完全捕捉不到。

直到有一天在翻看 Emacs 源代碼的時(shí)候突然看到了 set-process-sentinel這個(gè)函數(shù)才找到了解決方案。  set-process-sentinel函數(shù)可以對一個(gè)特定的進(jìn)程設(shè)置一個(gè)“哨兵”,當(dāng)這個(gè)進(jìn)程的狀態(tài)發(fā)生變化的時(shí)候(比如說進(jìn)程結(jié)束的時(shí)候),“哨兵”就會(huì)通知  Emacs 調(diào)用相應(yīng)的函數(shù)來完成預(yù)定的工作。有了這個(gè)方案,我們只需要把刪除 Shell buffer 的函數(shù)關(guān)聯(lián)到正確的進(jìn)程上就行了。

下面就是這兩個(gè)函數(shù):

清單 2. 兩個(gè)函數(shù)

(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的作用是刪除進(jìn)程對應(yīng)的 buffer; kill-shell-buffer-after-exit函數(shù)的作用就是把  kill-shell-buffer函數(shù)關(guān)聯(lián)到正確的進(jìn)程上去。然后當(dāng)我們把這個(gè)函數(shù)加入到 shell-mode-hook當(dāng)中后,就可以在每次打開 Shell  buffer 的時(shí)候得到正確的進(jìn)程信息了。

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

outline in Shell Mode

這一節(jié)我們談 outline-mode。Outline-mode 是 GNU Emacs 的一個(gè)非常好用的寫作模式。使用 outline-mode  可以輕松方便的操作結(jié)構(gòu)化文檔,可以將文檔內(nèi)容分級展開,或者逐級隱藏,既能總攬全局,又可深入細(xì)節(jié)。outline-mode 是如此精彩,以至于 Carsten  Dominik 教授在此基礎(chǔ)上開發(fā)出了強(qiáng)大的 orgmode。

在這一節(jié)當(dāng)中我們將要討論一下如何將 outline-mode 的強(qiáng)大功能應(yīng)用到 Shell-mode 當(dāng)中。在進(jìn)入細(xì)節(jié)之前,讓我們先對  Outline-mode 進(jìn)行一個(gè)簡單的介紹。

Outline mode  當(dāng)中,文檔中的內(nèi)容被分成兩種結(jié)構(gòu),一種是“標(biāo)題”,一種是“內(nèi)容”。其中的“標(biāo)題”又可以根據(jù)需要分成大小不同的級別。在對文檔的內(nèi)容進(jìn)行折疊和展開操作的時(shí)候就是以這些“標(biāo)題”的級別為依據(jù)的。例如下面這段摘自  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.

當(dāng)我們折疊起這段文檔的時(shí)候,分別可以折疊成這樣的形式

* Food...
* Shelter...

或者這樣的形式

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

或者我們又可以將 Delicious Food單獨(dú)展開

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

那么這些示例和 Shell mode 又有什么關(guān)系呢? 如果我們把 Shell buffer 里的 * 命令 * 看作 outline-mode  的“標(biāo)題”,將命令產(chǎn)生的輸出看作是“內(nèi)容”,那么是不是就可以像折疊起一篇普通的結(jié)構(gòu)化文檔那樣將所有的 Shell  命令都折疊起來呢?就像下面這個(gè)示例所展示的這樣:

清單 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$ ...

當(dāng)我們把 Shell buffer  里面的內(nèi)容全部折疊起來,我們就看到了一條時(shí)間線。既能夠于一瞥之間總覽全部的歷史,又可以隨時(shí)深入任何一條命令的細(xì)節(jié)。相比與僅能告訴我們曾經(jīng)做過什么的  history命令來說,這樣的場景更像是一部“時(shí)間機(jī)器”。

那么該怎樣實(shí)現(xiàn)這樣的夢想呢?其中的關(guān)鍵就是要讓 outline-mode 能夠認(rèn)出我們的“標(biāo)題”。在 outline-mode 里面缺省的“標(biāo)題”是一個(gè)  *,這個(gè) *從文本行的***個(gè)字符開始匹配,匹配上的,就是“標(biāo)題”,匹配不上的,就是“內(nèi)容”,匹配的次數(shù)越多,“標(biāo)題”的級別越低。我們可以通過設(shè)置  outline-regexp變量的值來定義我們自己的“標(biāo)題”。在 Shell mode 里面一個(gè)可行的辦法就是將 Shell  提示符的內(nèi)容定義為“標(biāo)題”。如同下面的示例這樣:

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

設(shè)置標(biāo)題以后,在 Shell mode 里面輸入 M-x outline-minor-mode就可以享受 outline-mode  帶來的便利了。例如上文示例中所示的結(jié)果使用一下三個(gè)操作就可以實(shí)現(xiàn):

  • 輸入 M-x hide-body或者 M-x hide-all命令折疊起 Shell buffer 里的所有命令

  • 移動(dòng)光標(biāo)到 ls *.el所在的行

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

Enhanced outline in Shell Mode

在上一節(jié)里面講述了通過設(shè)置 outline-regexp變量,使 outline-minor-mode可以在 shell-mode  中工作的方法,但是這樣簡單的設(shè)置很難避免會(huì)有一些負(fù)面的影響。因?yàn)?outline-regexp變量是一個(gè)全局變量,所以對  outline-regexp的值勢必改變其他模式中的outline-minor-mode的行為方式,而這肯定不是你所希望的。

所以我在工作當(dāng)中實(shí)際使用的是另外一種相對復(fù)雜一些的方法:使用一個(gè)函數(shù)為每一個(gè) buffer 設(shè)置分別的  outline-regexp,并且把outline-regexp變量修改為特定 buffer 范圍內(nèi)的局部變量。下面就是這個(gè)函數(shù):

清單 4. 設(shè)置 buffer 的函數(shù)

(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))))

這個(gè)函數(shù)首先定義了一個(gè)匿名函數(shù),存儲(chǔ)在 find-regexp變量中,這個(gè)函數(shù)通過遞歸的方式遍歷一個(gè)嵌套列表,直至找到與給定模式對應(yīng)的值;然后啟動(dòng)  outline-minor-mode,修改 outline-regexp為局部變量,然后調(diào)用上述的匿名函數(shù)設(shè)置正確的 outline-regexp。

要讓這個(gè)函數(shù)能夠工作,我們就需要把他加入到各個(gè)主模式的 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 )

但是細(xì)心的讀者應(yīng)該看到了,這個(gè) set-outline-minor-mode-regexp函數(shù)并沒有接受任何參數(shù),這是因?yàn)檫@些主模式在調(diào)用 hook  函數(shù)的時(shí)候是不會(huì)向它們傳遞任何參數(shù)的。那么我們需要的的數(shù)據(jù)從哪里來呢?顯然這里需要一個(gè)全局變量 outline-minor-mode-list來存儲(chǔ)  set-outline-minor-mode-regexp函數(shù)所需的所有數(shù)據(jù)。

清單 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 ")
))

有了這些擴(kuò)展,Emacs 就可以在創(chuàng)建一個(gè)新的 buffer 的時(shí)候,為這個(gè) buffer 設(shè)置正確的 outline-regexp值了。

延伸閱讀 hook

一些讀者可能注意到,在本文的敘述中多次提到了 hook 這一概念,那么 hook 究竟是什么東西?他在 Emacs  里面有起到什么作用呢?在這里我給大家做一個(gè)簡要的介紹。

簡單來講,hook 就是一個(gè)存儲(chǔ)函數(shù)列表的 Lisp 變量,該列表里的每一個(gè)函數(shù)被稱作這個(gè) hook 的一個(gè) hook 函數(shù)。GNU Emacs  的很多主模式(major modes)在完成初始化之后都會(huì)嘗試尋找并調(diào)用對應(yīng)該模式的 hook 變量里面的 hook 函數(shù)。因此 hook 就成為定制  Emacs 過程中一個(gè)非常重要的機(jī)制。我們可以通過添加 hook 函數(shù)的方式輕松的定制或擴(kuò)展 Emacs 的行為。

最簡單的 hook 用法就是直接調(diào)用已有的 Emacs 函數(shù),例如啟動(dòng)特定的子模式(minor modes):

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

更加復(fù)雜的用法就如上文所示,編寫自己的 hook 函數(shù)。

關(guān)于 hook 有幾個(gè)細(xì)節(jié)需要注意

  • 絕大多數(shù)普通 hook 變量的名稱都是在主模式的名稱后面加上 -hook后綴來構(gòu)成的

  • 但是,并不是所有 hook 變量都是這樣命名的

  • 絕大多數(shù)普通 hook 函數(shù)被調(diào)用的時(shí)候是不會(huì)向它傳遞任何參數(shù)的,同時(shí)也不會(huì)理會(huì)函數(shù)的返回結(jié)果的

  • 但是,并不是所有 hook 函數(shù)都是這樣調(diào)用的

  • 已經(jīng)裝入的 hook 函數(shù)將無法通過再次執(zhí)行 add-hook來進(jìn)行覆蓋或修改。實(shí)際的結(jié)果將會(huì)裝入該 hook 函數(shù)的多個(gè)版本。解決的辦法之一是清除  hook 變量,然后再次裝入:

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

以上是“Emacs中Shell環(huán)境如何擴(kuò)展和定制”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI