不久之前我寫了一款實用的應用 — The Fuck,用來修復在命令列中上一條錯誤的命令。這款應用下載了幾千次,在 GitHub 上有很多 star,並有幾十個優秀的貢獻者。本文介紹應用中有趣的內部實現。
另外,大約一週前我談論過《開源軟H件架構》一書,現在我覺得要是能在書中寫一章關於 The Fuck 的內容將是很酷的事情。
管道:
Fuck可以簡單理解成一個管道,從使用者的角度來看,流程如下:
有些東西出錯了 -> fuck -> “完事”
之所以這麼簡單是因為 fuck 只是一個別名(alias)罷了(使用者也可以使用其他別名)。對錯誤的命令進行了一些處理,執行修改過了的命令並更新命令歷史。比如對於 zsh 是這樣的:
1 |
TF_ALIAS=fuck alias fuck='eval $(thefuck $(fc -ln -1 | tail -n 1)); fc -R' |
讓我們再回到管道上來,對於在別名(alias)內執行的的 Fuck 來說,管道是這樣的:
出錯的命令 -> thefuck -> 修復好的命令
所有有趣的事都發生在 fuck 當中:
錯誤的命令 -> 匹配規則 -> 修正後的命令 -> 使用者選擇 -> 修改好的命令
這裡最重要的部分就是匹配規則了,規則是一個特殊模組集,它有兩個方法:
- match(command: Command) -> bool – 匹配上規則則返回True;
- get_new_command(command: Command) -> str|list[str] – 否則返回修改後的命令或命令列表(當有多個可能匹配項)
我想這個應用只是因為它的規則才這麼有趣,編寫自己的規則也很簡單。目前有75條可用的規則,大都是有第三方貢獻者寫的。命令是一個類似命名元組(namedtuple)這樣的資料結構:
Command(script: str, stdout: str, stderr: str)
其中script是與shell型別無關的錯誤命令。
處理不同Shell型別
在不同的shell中,描述 alias 的方式不同、語法不同(比如在 fish 中 && 表示為 and)、歷史命令的處理方法也不同,且 shell 還依賴特定的配置檔案(.bashre ,. zshrc 等)。為了避免這些麻煩,在程式中有一個 shells 的模組把這些與特定 shell 相關的命令轉化為與 sh 相容的型別,並展開別名和環境變數。 所以我們使用 shells.from_shell 方法來獲得 Command(前面的章節提到過的)的例項,在 sh 裡執行並且獲得stdout和stderr。
出錯的命令 -> from_shell 模組 -> 與 shell 型別無關的命令 -> (可以)在 sh 內執行 –> Command例項
對修改好的命令也做了相似地處理,即把與特定 shell 無關的命令通過 shells.to_shell 模組轉化為與 shell 相關的命令。
配置:
Fuck 是一個高可配置的應用,使用者可以開啟或關閉規則、配置 UI、設定規則選項還有進行其他的操作。使用者可以通過修改 ~/.thefuck/settring.py 檔案以及環境變數來配置應用:
預設配置 -> 通過 setting.py 檔案更新 -> 通過環境變數更新
之前版本中,配置物件以引數的形式傳遞到所有需要的場合,雖然那樣還不錯並且能夠測試,但存在過多的重複程式碼。而現在是一個單例(thefuck.conf.settings),類似Django中的django.conf.settings。
UI
Fuck 的UI很簡單,它允許使用者通過(上下)箭頭的方式在修正過的命令列表中進行選擇,使用 Enter 來確認選擇,Ctrl+C 來跳出程式。 不足的是在 Python 標準庫中沒有辦法在非Windows下不通過 curses 來讀取鍵盤輸入,由於別名(alias)的特性我們又不能在這裡使用 curses。但容易寫出針對Windows的 msvrt.getch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import tty import termios def getch(): fd = sys.stdin.fileno() old = termios.tcgetattr(fd) try: tty.setraw(fd) ch = sys.stdin.read(1) if ch == 'x03': # For compatibility with msvcrt.getch raise KeyboardInterrupt return ch finally: termios.tcsetattr(fd, termios.TCSADRAIN, old) |
另外UI也需要修復好的程式命令組成的有序列表,且規則匹配耗時應該儘量較短。而加入簡單的啟發式演算法後效果還不錯,首先我們按照優先順序來匹配規則,第一個返回的修復過的命令是有最大優先順序的命令。當使用者按下箭頭按鍵時再選擇其他的命令。所以在大多數的使用場景中都能很快完成任務。
整體來看:
如果從整體來看一下這個應用,會發現它很簡單:
其中 controller(控制器)是當使用者使用 fuck 來修復錯誤命令時的程式入口,它初始化設定、準備 shells 的互動環境、從 Corrector 來獲取修正過的命令並在 UI 中選擇。Corrector 使用所有可用的規則來匹配當前命令並且返回所有可用的修復過的命令。關於UI、設定和規則就說到這裡。
測試:
測試是所有軟體專案的最重要的部分之一。沒有測試,軟體可能會由於任一個改變而崩潰。我們使用pytest來進行單元測試。由於應用中存在規則,所以需要做很多測試來匹配和確認修正過的命令。所以,引數化的測試用例是很有用的,典型的測試是這樣的:
1 2 3 4 5 6 7 8 9 10 11 |
import pytest from thefuck.rules.cd_mkdir import match, get_new_command from tests.utils import Command @pytest.mark.parametrize('command', [ Command(script='cd foo', stderr='cd: foo: No such file or directory'), Command(script='cd foo/bar/baz', stderr='cd: foo: No such file or directory'), Command(script='cd foo/bar/baz', stderr='cd: can't cd to foo/bar/baz')]) def test_match(command): assert match(command) |
Fuck 與許多種類的 shell 共同工作,而每個 shell 又需要特定的別名。為了保證所有別名可用,需要用到功能測試,其中用到了我寫的 pytest-docker-pexpect 模組,在一個 docker 容器內設定一個場景來測試所有支援的命令。
釋出:
Fuck 應用的最麻煩的部分是它的安裝,應用通過pip來發布,由此產生了一些問題:
- 有些平臺上依賴python的標頭檔案(python-dev),所以我們需要告訴使用者手動地安裝;
- pip不支援安裝後自動完成一些自定義操作,所以使用者需要手動配置一個別名;
- 有些使用者使用不支援的python版本,應用只支援2.7或者3.3+的版本;
- 有些老版本的pip根本就不安裝依賴項;
- 有些版本的pip忽視Python版本的依賴關係,所以需要為早於3.4的版本安裝pathlib;
- 有趣的是有人對這個名字感到很憤怒並且嘗試從pypi中移除這個包;
大多數的問題可以通過使用專門的install指令碼解決,該指令碼在內部使用了pip,但在安裝前對系統進行一些準備工作,並在安裝後配置別名。