一些命令行工具的增强版

最近在 HN 有一篇《Cli improved》比较火,讲的是一些命令行工具的增强版,我觉得比较好,替换掉了我之前用的一些工具,在这里分享一下。内容基本上是从原文中意译过来的。

首先本文要介绍的工具基本都是原来工具的增强版,也就是说原来工具有的,增强版也都有。因为习惯很难改变,所以完全可以用 alias 替换掉。但是如果某些情况下想用原版的程序的话,可以使用下面的命令:

安装方法我就不说了,Mac 所有的软件都可以通过 brew 来安装,Linux 参考项目主页吧。

bat 替换 cat

cat 做的事情就是把文件内容打印出来,但是没有颜色高亮,很不方便(没有颜色我基本看不懂代码 > <)。ccat (Go语言写的)是有颜色的 cat。但是 bcat 不仅有颜色,还有行号、分页、git 加加减减的整合、类似 less 那样的搜索。下图是我自己的展示,最后两行带 + 的是新增的行,非常酷炫。

建议 alias cat=bat 。

prettyping 替代 ping

这个不用多介绍了,直接看下效果吧。

fzf 替换 Ctrl+R

Ctrl+R 可以在 history 搜索命令,不过用起来很反人类。fzf 使用效果如下,非常方便,从此再也不用畏惧长命令了。

除了查找历史命令,fzf 可以用来模糊查找文件,也很好用,直接设置一个命令,fzf 查找的结果调用 vim 编辑,效率很高。

htop 替换 top

这个应该很多人都知道,htop 提供的信息更明确,熟悉了快捷键效率很高,比如按 P 按照 CPU 排序,t 展示树形,k 来 kill 选中的进程等等。

diff-so-fancy 替换 diff

diff-so-fancy 带有高亮,代码的变更等,配合 git 使用可以让你的 git diff 显示效果和 github 上面的 diff 页面一样。

fd 替换 find

又一个 Rust 写出来的好东西:fd。find 的语法太难记了,fd 好用很多,显示还带高亮。效果如图。

ncdu 替换 du

ncdu 将参数配置好,显示的效果如下。我用的是原作者的的 alias,文件夹是 CPython 的源代码。

Ack 或 ag 替换 grep

这俩我都没用过,介绍一个我用的 rg 吧,主要是速度快。效果如下:

jq

操作 json 的一个命令行工具。再也不用组合复杂的 sed,awk,grep 来处理 json 了,我不确定是不是 jmespath 的语法。教程可以看下官方的,很好学。

类似的 for csv 的有一个叫 csvkit

z

一个根据你的路径历史来 fuzzy 跳转的东西,有一个竞品叫 autojump。不过我习惯了用 z 了,用了很久没有什么痛点。使用效果如图。

fpp

根据前一个命令的输入,自动识别输入中的文件名,然后可以使用快捷键打开。

比如 git status | fpp 的效果如下:

lnav

lnav 是一个日志查看工具,是一个 TUI 工具。

如上图所示,它的好处是处理了折行,对于长行的日志看起来是非常友好的。而且自动高亮了不同的内容,我们看日志的时候就可以方便地忽略每一行相同的部分,快速发现日志中的异常点。相比于 Vim,Vim 打开日志的话会有很多问题,是很危险的操作,因为 Vim 会将整个文件加载到内存,而 lnav 是用了 lseek 不会一下子占用很多内存。

未完待续…… 不定期更新,欢迎补充。

 

LBYL与EAFP漫谈

LBYL 的意思是“Look before you leap.” 指在程序执行之前做好检查。比如下面这段代码:

EAFP 的意思是“Easier to ask for forgiveness than permission.” 在编程方面指的是相信程序会正确执行,如果出错了再处理错误,比如上面这段代码用 EAFP 风格写就是下面这样:

很多情况下,两种方式的写法是可以互相替换的,但是 Python 鼓励 EAFP。原因是这种方法的可读性更高,速度也更快(只有在出错的时候才需要处理,而 LBYL 需要每次运行都检查)。

现在的大多数语言都有异常处理机制了,比如 Java, Python, Ruby 等,没有异常处理机制的都是一些古老的语言,比如 C 语言。但是没有异常处理机制并不代表程序总能运行正确,所以 C 语言需要其他形式来处理程序(函数)不能正确运行的情况,一般的方法是返回 0 表示运行正常,其他值表示运行异常。

但是这种处理形式有两个缺点(这部分可以参考《代码之髓》这本书的第 6 章):第一是可能遗漏错误。比如一个函数在大部分情况下都能正确运行,只有在很少的情况下会出错,或者只有改变了环境之后才会出错。那么很可能程序员会默认这个函数总是正确的,而忘记处理这个异常。如果是 Java 的话,程序员就必须在调用函数的地方处理掉所有可能的异常(虽然很多 Java 程序员喜欢不优雅地用 RuntimeError 处理所有的异常)。更让人头疼的事情是,一旦这“极少数”的情况发生了,那么错误经常不在错误出现的地方,而在很外层的一个调用处。你要花很多时间调试才能找到最终出错的地方。如果有异常处理机制的话,异常栈通常会直接给出 Exception 发生的地方。

第二个缺点是会使代码的可读性下降。因为要检查函数的返回值,要写很多 if-else ,程序真正的逻辑就变得难以阅读。我记得有位高人说过这么一句话,具体是谁说的记不得了,大意是:“高级语言和低级语言的区别是,需要不需要写很多与程序的逻辑无关的东西。” 很多 if-else ,很难看出这个只是判断,还是程序逻辑/业务的判断。如果用 try-catch ,那么 try 代码块里面可以只写程序的逻辑,在 except 里面处理所有的异常。

 

即使在有异常处理机制的语言中,比如 Python,很多人喜欢做的一个事情是在子函数用 if 判断,然后 logger.error + return。其实不如 raise ,然后在调用者那里 try-catch,更能表达逻辑。

此外,Python 语言内置的协议也大量使用了异常的机制。比如《fluent python》(314页)关于重载加运算符 __add__ 就提到,为了遵守鸭子精神,不要测试 other 操作数的类型,而是应该捕获异常,然后抛出 NotImplemented 。

这样的好处是,比如我们定义了一个新的数据类型,支持和 int 相加 a.__add__(4) 。但是内置的 int 可不支持和我们自定义的类型相加,4.__add__(a) 就会抛出异常。这时解释器尝试用 __radd__ 来处理(即 a.__add__(4) )。如果 int 的 __add__ 是实现是相加对象的类型,如果不符合预期就抛出一个 TypeError ,就没有这样的便利了。

 

但是 EAFP 在某些情况下可能是不可行的,它的一个问题就是等错误发生的时候,程序已经运行了一半,如果函数会造成一些副作用,那么这个时候副作用已经发生了。这种情况下,如果没有数据事务这种外部的东西来提供原子性的话,就比较麻烦了,需要手动清理副作用的状态。而 LBFY 这种风格可以尽量保证提供给程序的参数正确,可以顺利运行完成。

其实我想讨论这二者的区别的真正地方是,在生活中也会有这两种风格的处理方式。

团队管理上 try-catch 更自由,更人性化,大家可以关注自己“做事的逻辑”而没有很多条条框框,在程序上 if 可能就多了一次判断,但是在实际生活中的话,这种 if 可能是各种各样的沟通和 ask permission,效率可就不只低这么多了。但是换句话来说,严谨的系统容不得做到半路才出现 “Exception”,还是需要“Look before you leap”的。

 

参考资料:

  1. https://www.codeproject.com/Tips/490765/If-else-instead-of-try-catch
  2. https://www.quora.com/When-should-I-use-try-catch-instead-of-if-else
  3. https://en.wikiquote.org/wiki/Grace_Hopper
  4. https://stackoverflow.com/questions/12265451/ask-forgiveness-not-permission-explain
  5. https://stackoverflow.com/questions/11360858/what-is-the-eafp-principle-in-python
 

Lua 的 pairs 和 ipairs 的区别

最近在用 nginx_lua_module 模块写一个流量转发的东西,根据 Header, Body, Cookie 按照流量比例转发到另一个地方。看了前人写的代码,里面循环的时候有的用 pairs ,有的用 ipairs ,很不解。好在 Lua 官网就有电子版的《Programming in Lua》,学习非常方便。以下内容是我初学 Lua 的笔记和思考,如果不正确,欢迎指正。

一般的迭代器是在内部维护一个状态的(当前迭代的位置),但是 Lua 的迭代器是 Stateless(无状态的),这样的好处是可以重复多次迭代。不像 Python 的 Iterator 和 Iterable,如果多次迭代的话,需要从 Iterable 获得一个迭代器 Iterator。Lua 的迭代器需要循环的时候自己维护。

每一次迭代,for 都会调用迭代器函数,传入的参数有 2 个,一个是无状态的、要迭代的对象,一个就是控制参数(迭代的状态,1 2 3 …)。

比如下面这个循环:

首先 ipairs(a) 执行,返回三个值:iter 函数(从这里看出 Lua 和 Python 一样是有 “一等函数” 的),迭代的对象 a ,和迭代开始的下标 0 。然后第一次 for 循环调用 iter(a, 0) (参数如我们上面所说),得到返回值当前下标 i 和 a[i] 的值 v ,将这两个值赋值给 for 循环定义时候的变量 i 和 v 。用 Lua 实现这个逻辑,如下:

那么上面的 for 循环调用的逻辑类似下面这样,首先调用 ipairs 函数得到 iter 函数,然后每次调用 iter 函数。

另外一个要注意的点是,上面的 Lua 代码判断了 v ,如果不为 nil 才继续。而实际的 for 循环中也是这样的。比如我们下面这个循环,因为第二个值是 nil ,所以打印只会出现第一个元素。

然后我们在来说说 pairs 。其实从上面的描述中也可以发现,ipars 是从 1 开始取值到 nil 截止,那么如果 table 中如果有 nil 但是又想取出所有的元素,就很不方便了。这个时候就可以用 pairs 。

for 循环的逻辑在上面已经说了, pairs 在这里的不同是,它返回的三个元素是 next 函数,迭代的对象 a ,开始的状态 nil 。可以看到不同点主要有两个:第一个是函数 next ,它和 iter 的不同是,它返回的是下一个 key value ,并且顺序固定,直到没有任何 key value 对了,迭代结束。

我们可以通过几个例子看它们的区别。

打印值如下:

两个结果一样,因为在这个 table 中 key 都是 1 2 3 ,所以 pair 用 iter 循环(下标从 1 开始到第一个不是 nil 的值),还是 ipairs 用 next 循环(下标从 nil 开始遍历所有的 key value ),效果都是一样的。

结果是 pairs 可以打印出来结果,ipairs 打印的结果为空。因为 t[1] 的值是 nil,所以 ipairs 循环刚开始就停止了。

再来看最后一组例子(从参考资料1抄来的):

结果如注释中所示,就不必解释了吧。

了解了它们的区别,用起来就非常简单了。ipairs 一般用于需要下标、迭代 array 形式的 table;pairs 可以用来迭代字典形式的 table

 

参考资料:

  1. table 使用手册
  2. Programming in lua
  3. table tutorials
 

MIME types 详解

MIME type 的全称是 Multipurpose Internet Mail Extensions (MIME) ,可以标志一个文件的类型。IANA 的网站上有一个正式的 MIME type 的列表,为什么会有这个列表呢?因为并不是所有的文件类型都有 MIME type,这个MIME type 也不是服务器可以随意设置的,服务器/浏览器两头都要实现相同的标准。基本上只有使用广泛的、性能高的、安全的文件类型才会被加入,因为每加入一个,浏览器厂商、服务器厂商都要去实现,成本比较大;二来风险也比较大,如果压缩比率不高可能会浪费网络带宽,严重的可能带来安全问题。服务器正确的设置 MIME type 也非常重要,否则服务器不会解释资源,比如不会播放视频或音频等。

它的语法是 type/subtype ,中间是 / 隔开,不区分大小写(一般都是小写)。type 表示一个大类,可以是视频、音频、文本等。subtype 表示具体的格式,jpeg png 等。type 又分成两种,一种是 Discrete(独立) 的,另一种是 multpart 。

Discrete type 分为下面5种:

  1. text 文本
  2. image 图片
  3. audio 音频
  4. video 视频
  5. application 一般指二进制数据

如果不指定 subtype ,那对于文本文件默认的就是 text/pain ,二进制数据默认的是 application/octet-stream 。它表示的是“未知类型的文本文件”,并不是指所有的文本文件。举个例子,如果 <link> 标签里面的资源应该是 text/css ,如果使用 text/plain 的话,浏览器并不会把它当做一个 CSS 文件来解释。

application/octet-stream 实际上表示的是“未知的二进制数据”,因为安全考虑,浏览器不会执行它或者尝试解释它。如果 Header Content-Disposition 设置为attachment,浏览器会弹出“另外为”对话框提示保存。

Multipart 一般由多部分组成,比如 multipart/form-data 一般用于浏览器将 HTML 的表单发给服务器。其中用 Content-type 中定义的 boundary 来分割。每一个 “part” 都是一个实体,对于上传表单的字段,每一个 part 都有 HTTP header Content-Disposition 和 Content-Type。因为有 boundary 可以分割,所以 Content-Length 会被忽略。比如下面这样的一个表单:

将会发出的 HTTP 请求如下:

之前对接过一个接口,要求每个请求都要带上证书文件,我就是用的 multipart 发出的。

除了常见的这个,还有另一种叫做的 multipart/byteranges 的 MIME type。介绍这个类型之前,我们先了解一下 HTTP 协议支持的“部分相应内容”。HTTP 的请求可以设置 Range 字段,要求只返回请求部分的 bytes。(要求服务器支持,可以通过 Response 的 Accept-Ranges: bytes 判断是否支持)比如下面这个请求,就只会返回 1K 大小的内容。

发出的 HTTP 请求如下:

Response 如下:

注意这个 HTTP 响应的状态码是 “206 Partial Content”。表示返回的是部分内容。

由此,可能你已经想到,我可以一次请求多个“部分内容”吗?答案是肯定的,这个时候服务器返回的 Response 中,Header 的 Content-Type 就会是 multipart/byteranges。其中每一个“部分响应”都会带有 Header Content-Type 和 Content-Range。比如这个请求:

得到的 Response 如下:

关于 Range_requests 的更多内容可以参考 MDN 的有关部分

最后,如果 MIME type 缺失的话,客户端可能去尝试猜测它的类型。这在不同浏览器的表现是不同的,可能有安全隐患,比如某些被资源被认为是“可执行的”。服务器可以通过设置 X-Content-Type-Options 来禁止客户端进行猜测。

除了设置 MIME type之外,还有两种方法可以表示文件类型:

  1. 使用文件后缀名。在 Windows 系统中比较流行,但是这只是一种约定,并不是所有的文件的后缀名都是通用的,有意义的。特别是在 Unix 系的系统中,这只不过是“名字的一部分”而已。
  2. Magic numbers。比如 GIF89 的文件使用 47 49 46 38 39 开头,PNG 使用 89 50 4E 47 开头。但并不是所有文件都会有 Magic numbers,所以这种方法也不是100%可靠的。

在实现方面,一般 HTTP 服务器会帮你处理好 MIME type Header 的设置,比如 Nginx 会使用 mime.types 文件判断什么后缀的文件该返回什么样的 MIME type。

 

参考资料:

  1. MDN文档
  2. wiki
 

构建大型Cron系统的思考

之前因为项目需求,看过一些 Python 相关的定时任务实现,当时比较好奇的是它们是怎么解决重复执行问题的。如果部署多个副本,就会产生多次执行问题;如果部署单点可以保证只执行一次,但是高可用又是一个问题。

如果能有一个非常可靠的 Cron 系统,可以:

  1. 保证高可用
  2. 保证正确性(不能重复执行)
  3. 让我很方便地看到所有任务的执行情况,最好是一个 web 界面,不需要登陆机器看日志
  4. 方便配置

就能解决我的一大痛点了。这个想法在以前写爬虫的时候就有了。因为线上的爬虫肯定不是跑一次就完事了,需要每天执行/每周执行,保持追踪目标网站的更新。那时候我们是用的单机部署的 crontab,定时放进去任务,虽说运行了很长时间也没啥问题,但是只能说是运气吧,如果它挂了,我们可能要花很长时间发现、修复。

最近一直在读《Google SRE运维解密》,这本书介绍了各种大型系统的一些指导思想和经验。《24 分布式周期性任务系统》这一章又给了我一些新的启发。

总的来说实现一个高可用、正确的 Cron,我觉得可以有三个思路:

第一种是我在前文中提到的,使用一个分布式的任务队列,将任务的调度和执行分开。然后在定时放入任务的时候根据任务、时间生成一个唯一 token,然后拿 token 去队列里放任务。应该可以解决分布式的问题,即高可用又是幂等的。(只是我的一个想法,还没听说过谁是这么做的,不知道行不行得通)

第二种跟上面的思路一致,也是做调度和执行分离,但是用了外部的数据库保证一致性。(这么实践的公司应该比较多)

实际上上面两种是比较粗糙的,本质上的问题用了简单的冗余。

然后就是我在《Google SRE》中看到的第三种。Google 的 Cron 是用了分布式的方式(我认为这个思路才是正确的,高可用是分布式要解决的一个典型问题。用一个叫做 Paxos 的分布式协议,多个节点保持同步,只有一个节点 master 在工作,将任务的执行情况同步给从节点。如果 master down 了,马上通过选举出现一个新的 master,因为这个 master 一直在和从节点同步状态,所以新 master 也有之前任务执行情况的信息,任务不会被重复执行。Master 在切换时会有很多细节问题需要处理,比如 master 开始执行一个任务的时候,需要把开始执行的信息同步下去,而且必须是先同步再执行,否则执行期间出现了 master 切换,那么新 master 有可能不知道此任务已经被执行了。

这里有一个“部分失败”的问题。其实这个问题比较经典,不仅存在于 Cron。比如 Django 的 migrate 操作,每次失败了都很头疼,要手动恢复数据表重新执行。

《Google SRE》书中提到,要解决“部分失败”的问题,就要实现下面两种至少一个:

  • 所有操作都要是幂等的(这样重复执行不会有问题)
  • 任务的所有操作都可以通过一个外部系统查询所有操作的执行状态(这样切换过后可以针对此任务失败的部分重试)

不幸的是,这两种方案的成本都很大。

正确性和高可用这两个问题,暂时就想到上面三种方案。在我看来,Google 的做法是最好的。

另外还有一个小问题,就是写 crontab 的时候大家都喜欢写在 0 点执行,而文本的 crontab 又很难看到已有任务的分布情况,所以就导致最后 crontab 变得非常集中,0 点的任务特别多。Google 的方法是在 crontab 的语法中添加了 ? 符号,比如如果分钟上面是 ? 就表示任意分钟执行都可以。可以交给系统去调度。