(点击
上方公众号
,可快速关注)
编译:伯乐在线 - 阿喵,英文:Vladimir Iakovlev
如有好文章投稿,请点击 → 这里了解详情
【导读】:昨天文章《
命令行的生活是如此的 Fuck
》向大家介绍了一个命令行工具 The Fuck。这个工具并不是最新的,其开发者 Vladimir Iakovlev 在 2015 年就在 GitHub 上分享了。在今天这篇文章,作者讲述 The Fuck 的内部实现。
不久之前我写了一款实用的应用 — The Fuck,用来修复在命令行中上一条错误的命令。这款应用下载了几千次,在 GitHub 上有很多 star,并有几十个优秀的贡献者。本文介绍应用中有趣的内部实现。
另外,大约一周前我谈论过《开源软H件架构》一书,现在我觉得要是能在书中写一章关于 The Fuck 的内容将是很酷的事情。
管道:
Fuck可以简单理解成一个管道,从用户的角度来看,流程如下:
有些东西出错了 -> fuck -> “完事”
之所以这么简单是因为 fuck 只是一个别名(alias)罢了(用户也可以使用其他别名)。对错误的命令进行了一些处理,执行修改过了的命令并更新命令历史。比如对于 zsh 是这样的:
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:
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来进行单元测试。由于应用中存在规则,所以需要做很多测试来匹配和确认修正过的命令。所以,参数化的测试用例是很有用的,典型的测试是这样的:
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'
,