1
13
2024
10

使用 atuin 管理 shell 命令历史

atuin 是最近在群里看到的工具。功能和我自己用 skim 糊的脚本一样,搜索并执行 shell 的命令历史用的。但是,它的数据存储使用的是 SQLite3,并且它是使用 Rust 编程语言编写的。于是事情有了一些好的变化。

首先,因为 atuin 并不像 Web 服务那样,会持续打开并操作数据库,所以 SQLite3 并发容易报错的问题并不需要担心。而 atuin 会记录执行时间、耗时、工作目录和退出码等信息。更多的元信息,能给之后的搜索和分析提供更多帮助。

其次,因为搜索走的数据库查询,因此并不需要像我用 skim 那样,每次把全部历史加载到内存。这样就可以保留更多的历史记录而不用怕越用越慢了。不知道 SQLite3 的搜索功能效率如何,但我几万条记录,已经可以明显感觉到加载耗时的差异了。

最后,它是 Rust 写的——这点很重要,因为这大大地方便了我对它进行修改(而不像某 Zig 写的工具,我翻了半天文档都没改对最后只好放弃了)。

当然,让我没多犹豫就决定尝试 atuin 的最重要的原因是:它独立于 shell 原本的历史记录功能,并不会取而代之。所以它要是不合我意,我只要把它删掉就好了,原本的历史记录丝毫不受影响。

于是我到现在已经用半个月了,结果非常满意。不过也已经对它做了好多修改了。比较重要的如下:

  1. 支持 Shift+Del 键删除记录。有时候会不小心打错命令。这种命令记在历史里时不时被翻出来,不但占用显示空间,还容易不小心选错然后相同的错误又犯一遍,甚至因为没看清命令细节而不小心删掉好多文件(还好我有快照)。atuin 的作者最近也加了删除功能,但是是在另一个界面操作,对于我这种经常在找命令的时候要删除多条命令的用法来说,不光麻烦,而且上下文切换的代价很大,会忘记自己原本是要干什么的。

  2. 精确匹配模式,这是 skim 的叫法。你叫它多子字符串匹配也行。自从 fzf 流行以来,大家都迷上了子序列匹配的所谓「模糊匹配(fuzzy match)」。但是我不喜欢这种匹配方法,会给出太多不相关的结果,加大脑力负担。真正好的模糊匹配是 agrep 那种基于编辑距离的算法,打错点字没关系那种。所以我给 atuin 也加上了精确匹配模式,同时还提升了查询性能呢。

  3. 反转 UI 的 --invert 命令行选项。像我之前使用 skim 那样,当光标位于终端窗口的上半部分时,我希望我搜索时打字的地方在上方;而当光标来到终端窗口的下半部分时,搜索时光标也放下边。这样关注的焦点就不会跳很远,有连贯性,节省认知脑力。atuin 本身有反转 UI 的功能,但是是写在配置文件里的,而我需要视情况决定要不要反转 UI,所以还是得加个命令行选项。

  4. 更改了选中项的颜色。atuin 原本用的是红色,我总觉得是哪里有报错……

还有些不太重要的修改,可以来我的分支查看:https://github.com/lilydjwg/atuin/commits/lilydjwg。注意这个分支我会经常 rebase 的。

另外我修改过的 zsh 插件在 https://github.com/lilydjwg/dotzsh/blob/master/plugins/atuin.zsh

值得一提的是,atuin 还支持同步。同步的数据本身是加密的,但是还是会泄露一堆诸如什么时候在跑命令之类的元数据,所以我自己跑了个服务。服务很好跑,但是同步似乎有些问题。我两个系统,两边都导入了之前的历史记录并同步,但是后同步的那个系统上的历史,很难被同步过去。甚至 atuin 发现本地比远程多,就从最新开始慢慢上传,直到两边一样多;如果远程比本地多,那就把远程的删掉一些(我也不知道它删了哪些,我是看到访问日志里巨量的 DELETE 请求才意识到问题的)。总之经过我不懈地反复运行,最终它只比远程多几条记录了,并且绝大部分历史记录已经两边都有了。我猜它可能没想到我会从不同的系统上同步已有且已分歧的数据吧……

Category: shell | Tags: linux shell Rust zsh atuin
8
10
2022
2

tmux 状态栏优化

在 tmux 的状态栏里,通常会显示当前时间。配置起来也非常简单,%Y-%m-%d %H:%M:%S这样的时间格式化字符串扔过去就可以了。然而这样做有个小问题:这个时间只能精确到秒。我的意思不是说我想让它显示毫秒,而是希望它像电视台和广播电台的时间一样,显示(播报)「12:00:00」的时候,就刚好是这一秒的开始。

一般来说,这么延迟个一秒以内的随机数问题不大,除了你有多个这种时间戳的时候——

tmux inside tmux inside tmux

这些时间戳哪个先更新、哪个后更新可完全说不准的,你可能看到明明在地球另一边的服务器上先到某一秒,本地才跟上。甚至同一个 tmux 的不同客户端里,这个时间戳的更新时间都可能会有差异。

我想优化这个的另一个原因是,我经常使用 extrace 来查看程序调用另一程序使用的命令行参数,然而我本地连了多少个 tmux,每秒便会有多少个 sh + awk 进程出来读系统负载。尤其是我从 Awesome 换到 Wayfire 之后,顶栏改用 waybar 了,很多指示器都是内建或者自己写的外部脚本,不再需要每隔几秒跑个子进程去获取信息,这样 tmux 调用子进程来刷新状态造成的干扰就突显了出来。

于是就有了 accurate-time 程序。它每个整秒会去读系统负载,然后和当前时间一起送给 tmux 来显示。每秒一个进程,已经少了很多啦。

既然是我的程序自己来读负载,也就方便做更多事情了,比如根据负载情况使用不同的文字颜色:绿色表示低负载,灰白是稍微有点活干,蓝色和 cyan 是比较忙碌,黄色、品红表示已经忙不过来啦,红色就是要累趴下啦。之前偶然间发现 qemu-git 这个包使用 ninja、但是链接的时候又套了一层 make,造成系统负载冲到了两百多。但是无论高低,tmux 的负载显示都是红色,所以我可能之前已经视而不见许多次了。加上颜色之后,这类异常就更容易被注意到了。以前我本地每次风扇呼呼地转才发现系统负载高,但是我要是用耳机的话就听不到了,现在也多了个高负载的指示。

安装和配置很简单,cargo build --release 编译,然后把编译出来的 target/release/accurate-time 扔到 $PATH 里,再如下配置 tmux 状态栏右边即可:

if-shell "accurate-time tmux" {
  set -g status-interval 0
} {
  set -g status-interval 1
  set -g status-right "#[fg=red]#(awk '{print $1, $2, $3}' /proc/loadavg) #[fg=colour15]%Y-%m-%d %H:%M:%S"
}

本来我还打算给 waybar 上的时间也这么做一下的,不过程序写好了才发现 waybar 自己已经把时间对齐到更新间隔了。

Category: Linux | Tags: tmux Rust linux
8
26
2021
5

倾听蓝牙耳机的按键事件

缘起

我的蓝牙耳机有简单的多媒体按键:上一曲、下一曲、播放、暂停。这几个按键在 Android 手机上是开箱即用的,然而在 Arch Linux 上,尤其是我的 Awesome 桌面环境上,并不那么自动。

其实按键事件都能收到的啦。可以收到 XF86AudioPrev, XF86AudioNext, XF86AudioPlay, XF86AudioPause 这么几个按键。给它们绑定热键,去调用 MPRIS 就好了。我使用的是 playerctl 工具。mpv 的 MPRIS 支持用的是 mpv-mpris。火狐自动就支持了,不用做什么。

看起来这样就好了?我也以为如此,直到我离电脑远了一些……

问题

躺在床上玩手机时也可以用电脑听歌啦~你问我为什么不用手机听歌?因为我的电脑没有 NFC 功能,耳机切到手机碰一下就可以了,可是切到电脑上是要打命令抢连接的!所以就不切来切去啦,反正手机上的曲库和电脑上是同步的。

可是!耳机多媒体按键怎么不管用了呢?我瞟了一眼电脑,哦,它怎么屏幕还亮起来了?反复几次之后,我终于搞明白了——锁屏的时候按键事件全被锁屏软件给挡下来啦……

那怎么办呢?

我有看到 acpid 那边也收到了些事件,比如「cd/prev / CDPREV」和「cd/next / CDNEXT」。但是不是很稳定,时有时无,也没看到播放和暂停。再加上从作为系统服务的 acpid 将指令传到用户会话比较麻烦,就放弃了。

后来想到,既然能收到按键事件,那么应当有个输入设备在。xinput 看了一下,果然有个「WH-1000XM2 (AVRCP)」,用 evtest 在 /dev/input/ 也能找到对应的设备文件。那直接读这个设备文件不就好了?

解决

好是好,但是没权限啊。不过像/dev/video*之类的文件就有权限,是 systemd 拿 udev 规则给加上的。我之前也给 i2c 设备加过权限,只是那次是直接 chmod 了,这次想试试更优雅的方案——uaccess tag。

这个 uaccess tag 是 systemd 用来给当前会话的用户设备权限的,切换用户会话的时候权限会自动变化。不过没有文档 QAQ,所以只好自己研究了。最终的结果是这样:

SUBSYSTEM=="input", ATTRS{name}=="WH-1000XM2 (AVRCP)", TAG+="uaccess"

这个规则的序号需要小于70,不然赶不上处理 uaccess 的逻辑。sudo udevadm control --reload-rules 然后再 sudo udevadm trigger,就可以看到对应的 /dev/input/event* 文件上已经有 ACL 给我的用户权限了。不过多了写权限,问题不大。

然后就可以开始愉快地找设备文件、读取事件啦。我用 Rust 写的,日常练习嘛,顺便用用前不久看到的 eyre 和 tracing。有个 input-linux 库,不用自己拿 libc 调用 ioctl、定义 C 结构体了。不过这个库不支持从按键名到按键枚举值的转换,所以我 fork 了一下。蓝牙耳机说来来、说走走,所以 inotify 也是少不了的啦。然后还用 toml 整了个配置文件,好放出来给有需要的人用~

啊对了,程序里一上来就把对应的输入设备用 xinput 给禁用了。这样桌面环境就不会收到事件,不会唤醒屏幕,也不会有重复操作了。(不过它退出的时候并没有把设备重新启用,懒~)

代码

Category: Linux | Tags: 硬件 linux Rust
1
27
2020
3

自制大上 Paperlike HD「驱动」

大上 Paperlike HD 使用有一段时间了,然而有一点我对其非常不满:它需要以 root 权限运行一个图形界面的程序。具体麻烦的地方是:

  • 图形界面的程序不方便使用 systemd 管理,那个窗口我得找个地方安放,并且在登出图形界面或者 Xorg 出问题时会随之关闭
  • 即使持续运行此程序,当几秒内不使用键盘或者鼠标的时候屏幕就会休眠。这导致我无法将此屏幕用于关注程序日志或者聊天工具的新消息。
  • 它持续不断地执行多个线程的任务(读取键盘事件、读取鼠标事件、通过 ioctl 与设备通讯),耗费了不少 CPU
  • 在屏幕尚未连接时,它的运行会导致内核不断输出日志「drm_dp_i2c_do_msg: 2 callbacks suppressed」

我曾多次想自己实现一个符合自己使用习惯的方案。

首先当然是 strace 上去啦。这会得到许多类似这样的消息:

ioctl(9</dev/i2c-1<char 89:1>>, _IOC(_IOC_NONE, 0x7, 0x7, 0), 0x7f47d8805b70) = 1
nanosleep({tv_sec=0, tv_nsec=100000000}, NULL) = 0
ioctl(9</dev/i2c-1<char 89:1>>, _IOC(_IOC_NONE, 0x7, 0x7, 0), 0x7f47d8805be0) = 1
nanosleep({tv_sec=0, tv_nsec=200000000},  <unfinished ...>

可以看到它在对 /dev/i2c-1 这个文件进行操作,但是具体内容是个指针,strace 看不到。

我尝试过使用大名鼎鼎的 IDA 的免费版本来分析其具体行为。但我对 IDA 并不熟悉,并且 IDA 只支持 Intel 语法的汇编,而我见的 AT&T 语法的比较多,Intel 的很多表示法我不太能看懂。

后来根据 ioctl 的请求参数找到这个文档,里边有这些 i2c 消息的结构体定义。于是想着先把 ioctl 的数据弄出来看看。一开始尝试用 gdb 去看那个地址的数据,但想到数据是变动的,再加上 gdb 查看太累了,就想起了通过 LD_PRELOAD 去 hook ioctl。

所以又要写 C 了?并没有呢!C 写起来那么不舒服,还是用 Rust 吧~然后搜了一下,还真有现成的用于写 LD_PRELOAD 库的 crate,比如我用的 redhook。不用自己去 dlopen,不用在各处写很多错误处理代码,很容易就写好了。代码链接

拿到了 ioctl 里用的消息,我不用理会它具体是什么意思,也没办法去猜测,自然是把它按大上提供的程序那个样子给发过去了。于是又一个 Rust 程序出来了。

一开始写的时候不小心往 unsafe 代码块里传了个悬空的指针,导致程序不工作,调试了好久,甚至我都把完整的整个流程给复刻了一遍。这要是用 C 写文本解析的逻辑可头疼了,不过 Rust 写起来就跟 Python 差不多的了~

至于那个 bug,是 Rust 语句中的临时对象(此例中是包含一个对象的数组)会在语句结束之后就释放导致的。有点坑,但也没什么好的办法。

程序运行起来之后就会保持 Paperlike HD 显示器可用,不会报错让装驱动,也不会过几秒就休眠了。我大幅降低了消息发送的频率(由差不多每秒三次改成了三秒才一次),再加上不需要读取键鼠输入,所以 CPU 占用也会大幅减少。另外内核也不会再打印「drm_dp_i2c_do_msg: 2 callbacks suppressed」的消息了,大概是因为消息频率降低了?重新连接显示器之后,和大上原版程序一样有概率出现显示器亮蓝灯、屏幕不工作的情况。拔插一下电源可解。

当然啦,如果有人要用这个程序的话,记得先确认一下你的 i2c 设备文件路径(去 lsof 大上原版程序就行)。另外,使用此程序后果自负,由此造成的任何设备损坏或者其它损失,我都不负责任的哦~

Category: 硬件 | Tags: 显示器 linux Rust E-ink 硬件
4
4
2019
4

系统在解析哪些域名呢?

最近用 Rust 写了个叫 capture-dns 的小程序,实时显示 DNS 查询结果的。配合 ipmarkup 的效果是这样的:

>>> sudo capture-dns lo | ipmarkup
[sudo] lilydjwg 的密码:
github.com -> 52.74.223.119(新加坡Amazon数据中心)
github.com -> 13.229.188.59(新加坡Amazon数据中心)
github.com -> 13.250.177.223(新加坡Amazon数据中心)
live.github.com -> 192.30.253.125(美国弗吉尼亚州阿什本GitHub)
live.github.com -> 192.30.253.124(美国弗吉尼亚州阿什本GitHub)
collector.githubapp.com -> 34.193.248.191(美国弗吉尼亚州阿什本Amazon数据中心)
collector.githubapp.com -> 52.20.29.9(美国弗吉尼亚州阿什本Amazon数据中心)
collector.githubapp.com -> 34.197.57.23(美国弗吉尼亚州阿什本Amazon数据中心)
api.github.com -> 13.250.94.254(美国Amazon数据中心)
api.github.com -> 13.250.168.23(美国Amazon数据中心)
api.github.com -> 54.169.195.247(新加坡Amazon数据中心)
ocsp.digicert.com -> 117.18.237.29(澳大利亚美国MCI通信服务有限公司(韦里孙商业Verizon Business)EdgeCast亚太网络CDN节点)

可以看到本地的软件们都在查询哪些域名,得到的 IP 又是什么。抓取的是应答,所以没得到 IP 结果的不会显示。我抓取的是 lo 网络接口,因为我本地有用 dnsmasq 做缓存。

其实这个程序一开始不是这样子的。群里有人想抓取系统上进行的 DNS 查询的域名。一开始是用 tshark 抓取的,然而它太占用内存了。我粗略看了一下 Python 的 scapy 工具,也用掉了大几十M内存。那么,用 Rust 写一个好了,也顺便练习一下 Rust。

这个程序运行时只有几M的内存占用,CPU 占用也是非常低的。不过它并没有做完全的协议分析,而是假设抓得的包是以太网帧封装的 IPv4 报文封装的 UDP 数据包里包着 DNS 应答报文。所以如果你是在 eth0 上跑 PPPoE 的话,抓 eth0 上的包就不行了,得抓 ppp0 这种了。当然你要是 IPv6 啊 DoH、DoT 啥的就更抓不到了。

后来我用 bcc 的 tcpretrans 脚本查看我这里到哪些地方的 TCP 连接不太通畅,然而经常会看到一些我猜不到是干嘛的 IP。所以就把这个程序改了一下,把域名对应的解析结果显示出来了。

Rust 不仅节省资源,而且开发的体验真的很棒呢,编译成功之后就能按我预期的运行了。也不用担心什么时候遇到个有问题的报文导致程序崩掉,因为写的时候就已经处理好了出错的情况。不像 Python 写的脚本,刚写好,一跑就抛个异常出来,提示我哪里不小心写错了。好不容易调试好了,跑着跑着,遇到意外情况就挂掉了……

Category: 编程 | Tags: Rust linux 网络 DNS
2
16
2019
4

在 Linux 下整理磁盘碎片

磁盘碎片其实有两种:文件碎了,和空闲空间碎了。使用 FIEMAP 命令可以获取到文件在磁盘(的逻辑地址上)的分布情况。也是 filefrag -v 命令输出的东西。比如我的 pacman.log 就很碎:

Filesystem type is: 58465342
File size of /var/log/pacman.log is 11052443 (2699 blocks of 4096 bytes)
 ext:     logical_offset:        physical_offset: length:   expected: flags:
   0:        0..    2015:  170210423.. 170212438:   2016:
   1:     2016..    2017:  170567879.. 170567880:      2:  170212439:
   2:     2018..    2027:  170569969.. 170569978:     10:  170567881:
   3:     2028..    2030:  170574582.. 170574584:      3:  170569979:
   4:     2031..    2031:  170574631.. 170574631:      1:  170574585:
   5:     2032..    2033:  170592662.. 170592663:      2:  170574632:
....
 123:     2683..    2687:   56903805..  56903809:      5:   56906403:
 124:     2688..    2698:   56903011..  56903021:     11:   56903810: last,eof
/var/log/pacman.log: 125 extents found

整理的办法也很简单,复制一下,基本上就好了。只要剩余空间足够,小文件会变成一整块,大文件也是少数几块。如果非要弄一整块大的,比如我存放 pacman 数据库的那个小文件系统,可以用 fallocate -l 200M pacman.fs2 这样子的命令分配空间,然后把数据 dd 进去(cp 不行,因为它会先截断文件再写入,之前分配的空间就释放掉了)。

介绍完毕,重点来了:怎么找到那些被写得很碎很碎的文件呢?

对每个文件调用 filefrag 肯定太慢了,所以我写了个库和工具 fiemap-rs 直接调用 FIEMAP。它提供两个工具。一个是 fraghist,统计碎片数量分布直方图,用来了解一下某群文件有多碎。另一个是 fragmorethan,用来寻找碎到一定程度的文件。运行起来是这样子的:

/var/log:
# Number of samples = 712
# Min = 1
# Max = 297
#
# Mean = 11.338483146067423
# Standard deviation = 40.138129228003045
# Variance = 1611.0694179238724
#
# Each ∎ is a count of 13
#
  1 ..  31 [ 658 ]: ∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
 31 ..  61 [  11 ]:
 61 ..  91 [   9 ]:
 91 .. 121 [  10 ]:
121 .. 151 [   6 ]:
151 .. 181 [   5 ]:
181 .. 211 [   3 ]:
211 .. 241 [   2 ]:
241 .. 271 [   3 ]:
271 .. 301 [   5 ]:
/var/log/journal/00000000000000000000000000000000/system@xxx.journal: 271
/var/log/journal/00000000000000000000000000000000/system@xxx.journal: 277
/var/log/journal/00000000000000000000000000000000/system.journal: 274
/var/log/journal/00000000000000000000000000000000/system@xxx.journal: 297
/var/log/journal/00000000000000000000000000000000/system@xxx.journal: 274

我系统上最碎的两群文件是 journal 日志和 python2-carbon 的数据文件。carbon 优化做得挺不好的,明明是预分配的固定大小文件啊,不知道怎么的就弄得很碎了。部分程序的日志(如 pacman、getmail)和火狐的 SQLite 数据库也挺碎的。后边这些我已经处理掉了所以示例输出只好用 journal 的啦。

找到想要整理的过碎的文件之后,复制一下就好啦:

for f in $(<list); do sudo cp -a $f $f.new; sudo mv $f.new $f; done

啊对了,工具的编译方法是,获取源码并安装 Rust 之后,在项目根目录里 cargo build --release 然后就可以在 target/release 下找到新鲜的可执行文件了~顺便说一下,这东西是支持 Android 的哦。

Category: Linux | Tags: linux 文件系统 Rust
9
28
2018
20

每次修 Python 代码的 bug 的时候总会想念 Rust

俗话说:由俭入奢易,由奢入俭难。

之前写 Python,老是在实现完一个特性之后,弄出来几个 AttributeError: 'NoneType' object has no attribute 或者 TypeError: list indices must be integers or slices, not str,还有 TypeError: can only concatenate str (not "int") to str 这样的错误。一看就明白自己又是哪里一不小心疏忽了,稍微修一下就好。

后来啊,我遇见了 Rust,整个流程就变了。之前写的时候,基本上都是通过手动测试来发现这种问题。为了高效、不破坏性地测试,需要控制测试的数据量,需要保证出错的时候相关的数据不会处于某种中间状态。当然在服务器上跑的脚本,我还要来来回回地传更新的脚本,或者弄个本地测试环境。而这一切,可能不过是为了跑一个成功之后再也不会用到的小程序,比如之前分析抓包数据的那次。而在 Rust 里,这些最容易犯的错误,cargo check 一下,编译器基本上能全给你指出来。所以有时候写一些小工具我也用 Rust,虽然写起来慢,但写好就能正常运行,不用反复试错,多好啊!

最近给 Arch Linux 中文社区的自动打包机器人 lilac 增加新特性。结果实现完部署之后,夜里就被 lilac 叫起来修 bug 了,还一下子就是仨……(lilac 很难本地测试,而短暂地服务中断又没多大影响,所以我都是不进行本地测试的。)

第一个 bug 是,与 dict.get 不一样,getattr 是没有默认值的。Python 里这种不一致很多,比如 configparser 里默认值要用关键字参数指定。Rust 遇到类似的情况,就会返回一个 Option。或者如果 API 决定如果不存在就 panic 的话,那么它就会直接返回我要取的值的类型,而不会包一层 Option。而我后边的代码是预期到这里可能取不到那个属性的,所以弄错了就会类型不匹配。

第二个 bug 是局部变量在一个分支上没有初始化。Rust 当然不会允许这种情况了。实际上 C 都不用担心这种问题,编译器会给出警告的,还有一些 linter 可以用。而 Python,很遗憾的是,我所使用的 pyflakes 并没有对此发出警告。我当然知道 pylint 那些。我很讨厌 pylint 和 jslint 这种不区分潜在 bug 和风格问题的 linter。我只需要工具在我可能疏忽的时候提醒我,而不需要它对我的编码风格指指点点,特别是那些指指点点往往是不对的。比如我的文件描述符变量名不叫 fd 难道要叫 fildes?

第三个 bug 是一个可能为 None 的变量我忘了先作 is not None 判断。这段代码如果初写的话我肯定是会注意到的,但是改的时候,只想着如果 pkg 里有冒号我得处理一下,就忘记了根本没有关联的包名的情况。Python 的 None,以及 C 和 C++ 的 NULL、Java 的 null、Lua 和 Ruby 的 nil、JavaScript 的 undefined 和 null,被称作是十亿美元错误,给无数程序员和用户带来了无尽的 bug。幸好这个东西在 Rust 里不存在:表达「没有值」的值没有被作为特殊值存在于几乎所有类型中,而是作为一类类型的可能的值之一。想要使用「正常」的值,就需要显式地进行类型转换,所以不可能被不小心忽略掉。顺便说一下,Go 里也有 nil 这种东西,以至于会出现这种不容易发现的 bug

Python 现在也给出了解决方案:类型注解,提供类似的类型检查。不过检查器是第三方的,也并不十分完善。等我找到机会试用过之后再来写感想啦。

Category: python | Tags: python 编程 编程语言 Rust
9
16
2018
10

人生苦短,我用 skim

前两天我又看到了基于子序列匹配的字符串过滤工具 fzf 的绚丽效果了。实际上我很早就听说了这个工具,只是懒得动手配置。此次提及,我发现 fzf 已经在官方软件源里了,而我也正好有时间,所以打算试一试。

然后呢,Arch Linux CN 群组里艾穎初提到 skim 这么一个工具。了解了一下,这个就是 Rust 版本的 fzf,并且在 archlinuxcn 源里也有(git 版本,即 skim-git)。这太好了,就是它了!

skim 的操作很简单。文章开头的链接里已经有效果演示了。常用的也就是输入子序列去过滤,然后再输入一个进一步过滤,直到看到想要的。使用 ! 前缀可以反向过滤,^ 匹配开头 $ 匹配结尾。Ctrl-p/n 来上下移动。提示符那里也支持通常的行编辑。

到现在为止,我自行实现了 sk-cd、sk-search-history、sk-vim-mru 三个功能。另外使用了自带的 completion.zsh 文件。由于各种不满意,没有使用自带的 key-bindings.zsh 文件(也就包含 cd 和历史命令搜索功能啦)。

completion.zsh 里目前有两个功能。kill 时通过 ps 补全进程 pid。这个想法很好,以后我可能专门做一个通用的方便 strace 啊 lsof 啊 gdb 啊之类的用。

另一个是遇到两个星号(**)时按 Tab 补全,查找并替换成当前目录下的文件。

我实现的 sk-cd 是从 autojump 取目录列表,然后喂给 skim。于是就成了交互式的 autojump~这是一个我很需要的功能。原来我都是通过 Tab 补全列出可能的项,然后再 Tab 过去选的,有些慢也有些麻烦。

sk-search-history 就是在历史命令里找东西。因为遇到特殊字符时无法正确地加载预览,我并没有开启预览功能。反正找到的命令只会放在命令行上,并不会自动执行的,选错了可以及时取消。

以上两个功能分别绑定到 Alt-s d 和 Alt-s r 上。我使用 Alt-s 作为 skim 快捷键的开头,以便保留 zsh 原本的快捷键,避免冲突,特别是以后可能会有更多功能被加入。我在 Vim 里,也是类似的做法,Alt-q 是 easymotion 的开头快捷键,Alt-d 是 denite 的开头快捷键。

sk-vim-mru 仅仅是个命令了。使用的数据是 mru.vim 的历史记录文件。然后做了两个函数:vim-mru 使用 Vim 编辑文件,vv-mru 使用我自己的 vv 命令在已有的 gVim 里编辑文件。

我做的版本和 skim 自带版本,最大的差别在于,我的版本会尽量使用全部的窗口空间,而 skim 自带的总是会使用 40% 窗口高度。(所以我有个函数用来获取当前光标位置,有需要的可以自己拿去用。)

如果你想用我的配置,可以 wget https://github.com/lilydjwg/dotzsh/raw/master/plugins/sk-tools.zsh 回去,然后 source 一下就好。有需要的话(比如数据来源、键绑定等)可以自行修改。


2018年09月17日更新:我尝试了一下把 sk-search-history 映射到 Ctrl-r 上,然后很快就放弃了。因为 skim 的结果是不可预测的,而默认的 Ctrl-r 的结果是完全可预测的(只要还记得;当然你不能开(那个让我在服务器上误杀过进程的)实时历史共享)。可预测性对提高效率非常关键,因为你不需要中断思维,停下来等结果。

Category: shell | Tags: linux shell zsh Rust
6
14
2018
3

递归遍历目录:Python vs Go vs Rust

群友提出了一个简单的任务:递归遍历一个很大的目录,根据文件名数一下有多少 JPEG 文件。怎么最快呢?然后他用了 Go 语言实现。

我忽略想起 Python 3.5 的 What's New 里提到,他们优化了 os.scandir 使得目录遍历快了好几倍(PEP 471)。其核心思想是:不进行不必要的 stat 系统调用,因为读目录获得了不少信息,原来都是丢弃掉了,现在改成了通过 DirEntry 对象来返回。这些信息包括文件名等,刚好有我们需要的。

于是 Go 做了这个优化没有呢?

翻了一下代码。Go 自带的实现位于 src/path/filepath/path.go 文件中。可以看到,它对每一个文件都 lstat 了。后来一阁指出,不仅如此,而且它还莫名其妙地对目录下的文件名进行了排序

呃,前者可以说是疏忽了,毕竟 Python 也是直到 3.5 才优化的。可是,它排那个序干嘛呢……

然后我又想到,Rust 那边如何呢?

结果是,Rust 对它所包含的东西非常审慎,标准库里并没有递归遍历目录的函数。那我们自己写一个?才不呢,用第三方库啦!可以看到,它也是返回 DirEntry 对象的。

后来了解到,Go 也有一个第三方的实现 godirwalk,对这些细节进行了优化。

光是了解实现不够。我们让它们来比试一下吧。顺便,把 find 和 fd 也拖进来好了。

任务:数一数一个拥有近万文件的目录下有多少 JPEG 文件。

实现代码:walkdir-test

结果:

   Rust: top:    4.78, min:    4.72, avg:    4.90, max:    5.46, mdev:    0.17, cnt:  20
 Go_3rd: top:    7.71, min:    7.64, avg:    7.79, max:    8.41, mdev:    0.16, cnt:  20
   find: top:   11.49, min:   11.32, avg:   11.76, max:   14.18, mdev:    0.59, cnt:  20
     fd: top:   18.17, min:   15.18, avg:   21.29, max:   29.94, mdev:    3.84, cnt:  20
     Go: top:   21.08, min:   20.91, avg:   21.28, max:   22.70, mdev:    0.37, cnt:  20
 Python: top:   29.66, min:   29.51, avg:   30.43, max:   35.84, mdev:    1.45, cnt:  20
Python2: top:   30.37, min:   30.10, avg:   30.85, max:   33.15, mdev:    0.75, cnt:  20

Rust 如预期一样是最快的。Go_3rd 就是那个第三方库的实现,也非常快的。fd 是 Rust 实现的,目标之一是快,但是这次并没有比老牌的 find 快。Go 自带的那个实现,十分令人遗憾地连 find 都没比过呢,不过还是比 Python 快了不少。Python 2 这次终于没有跑在 Python 3 前边了(虽然差距很小),我猜是 PEP 471 那个优化的功劳。

对了,还有代码行数:

  15 Python/walk
  29 Rust/src/main.rs
  30 Go/walk.go
  33 Go_3rd/walk.go

Rust 竟然不是最长的。不过确实是字符数最多的。

话说 Go 的 } 竟然也是有规定的,结构体的不能另起一行写,只能跟 Lisp 的风格那样堆在一行的尾巴里。

PS: 没想到之前给 swapview 写的 benchmark 程序在另外的项目里用上了呢,果然写东西还是通用些的好。


更新:在群友的提示下,我找了一个更大的目录来测试,结果很不一样呢。这次遍历的目录是 /usr,共有 320397 个文件。

     fd: top:  265.80, min:  259.84, avg:  273.89, max:  319.76, mdev:   15.03, cnt:  20
   Rust: top:  269.98, min:  266.86, avg:  272.82, max:  282.84, mdev:    4.17, cnt:  20
 Go_3rd: top:  361.17, min:  359.05, avg:  363.82, max:  370.22, mdev:    3.31, cnt:  20
   find: top:  454.03, min:  450.79, avg:  458.51, max:  467.31, mdev:    5.08, cnt:  20
 Python: top:  624.80, min:  615.73, avg:  630.67, max:  640.88, mdev:    6.79, cnt:  20
     Go: top:  890.03, min:  876.98, avg:  910.63, max:  967.14, mdev:   24.84, cnt:  20
Python2: top: 1171.38, min: 1157.19, avg: 1189.99, max: 1228.09, mdev: 4186.28, cnt:  20

可以看到,唯一的并行版本 fd 胜出了~Rust 版本紧随其后,显然在此例中并行并没有多么有效。Go_3rd 还是慢于 Rust 但也并不多。然后,经过优化的 Python 终于在更大的数据量上明显胜过了 Go 以及 Python 2 这两个浪费了很多系统调用的版本。

Category: 编程 | Tags: python go 编程语言 Rust
7
14
2017
12

swapview 更新

距离上一次 swapview 的更新已经一年多了。在这段时间里,不少语言有了比较大的更新,所以再跑一次。

首先是运行不了或者有问题的语言和实现:

  • Julia: 新版本不向后兼容,运行不了。求修
  • Nim: 标准库有些函数的行为有改变:walkFiles 不再返回目录文件,split 不再将连续的空白符作为一个分隔符。
  • Erlang: 不再支持 ~.0f 格式化字符串。

其中不向后兼容的,Julia 和 Nim 还没到达 1.0 版,所以坑人了也就坑了。Erlang 不知道是怎么回事。

然后是有警告的:

  • R: 文件打开失败有警告。不是大问题,不过有点烦。如果你知道怎么去掉它,请告诉我。
  • Elixir: 函数无参调用时不加括号会触发警告。看来 Elixir 也不喜欢 Ruby 函数调用不加操作的行为了呢。

还有发行版的锅:

  • CSharp: mono 与 chicken 冲突,无法安装,所以跑不了。
  • Haskell: Arch Linux 不再支持静态链接了。需要改一下编译参数。

我还对代码做了一些改进:

  • Rust_parallel: 用 rayon 换掉了 threadpool。rayon 更适合这种并行任务处理。另外稍微改进了一下代码。
  • NodeJS: 使用 ECMAScript 6 语法(箭头函数和 const / let 变量声明)。去掉不必要的分号。
  • C: 支持 Android 平台。
  • 修正了一些实现的格式化输出(还剩下一些)。

最后结果如下。因为 CPU 换成了 i7-7700HQ,所以耗时都比之前少了不少。另外注意,前排几名只有前三名都是多线程的,所以 Go_goroutine 比那些 C 和 C++ 版本快很正常。

           Rust_parallel: top:   30.48, min:   27.76, avg:   32.48, max:   37.80, mdev:    2.78, cnt:  20
               C++98_omp: top:   31.24, min:   29.04, avg:   34.42, max:   49.48, mdev:    4.52, cnt:  20
            Go_goroutine: top:   68.30, min:   61.87, avg:   75.89, max:  142.91, mdev:   16.39, cnt:  20
                   C++14: top:   83.17, min:   82.23, avg:   84.71, max:   92.58, mdev:    2.76, cnt:  20
             C++14_boost: top:   83.58, min:   83.20, avg:   84.58, max:   91.00, mdev:    1.72, cnt:  20
                   C++98: top:   83.71, min:   83.09, avg:   85.19, max:   91.48, mdev:    2.44, cnt:  20
                    Rust: top:   91.45, min:   90.81, avg:   93.08, max:   99.38, mdev:    2.07, cnt:  20
                       C: top:   91.49, min:   90.49, avg:   93.41, max:   99.44, mdev:    2.53, cnt:  20
                   C++11: top:   91.81, min:   91.33, avg:   93.52, max:  102.80, mdev:    3.04, cnt:  20
                     PHP: top:   93.91, min:   93.37, avg:   94.98, max:   99.42, mdev:    1.47, cnt:  20
                   OCaml: top:  106.85, min:  105.75, avg:  109.34, max:  118.03, mdev:    3.37, cnt:  20
                     Nim: top:  109.28, min:  108.44, avg:  110.75, max:  117.43, mdev:    2.13, cnt:  20
         D_parallel_llvm: top:  111.25, min:  109.43, avg:  113.21, max:  117.26, mdev:    2.33, cnt:  20
              D_parallel: top:  116.77, min:  114.69, avg:  118.95, max:  125.45, mdev:    2.87, cnt:  20
                    PyPy: top:  126.23, min:  124.29, avg:  128.34, max:  134.07, mdev:    2.79, cnt:  20
                  D_llvm: top:  129.63, min:  128.52, avg:  131.32, max:  137.65, mdev:    2.41, cnt:  20
                  LuaJIT: top:  132.68, min:  131.31, avg:  134.36, max:  143.07, mdev:    2.57, cnt:  20
                      Go: top:  135.57, min:  132.37, avg:  139.25, max:  148.37, mdev:    4.50, cnt:  20
                       D: top:  146.30, min:  145.00, avg:  149.14, max:  159.02, mdev:    3.85, cnt:  20
                Haskell2: top:  150.92, min:  149.41, avg:  153.25, max:  164.60, mdev:    3.53, cnt:  20
                 Python2: top:  155.36, min:  152.26, avg:  158.55, max:  170.20, mdev:    4.60, cnt:  20
                    Vala: top:  159.55, min:  157.87, avg:  161.40, max:  166.52, mdev:    2.26, cnt:  20
                  Erlang: top:  163.00, min:  158.63, avg:  168.76, max:  181.77, mdev:    7.09, cnt:  20
                   Lua51: top:  166.58, min:  164.58, avg:  168.89, max:  181.71, mdev:    3.69, cnt:  20
                   Lua52: top:  168.48, min:  167.40, avg:  170.82, max:  178.11, mdev:    3.36, cnt:  20
           Python3_bytes: top:  174.30, min:  172.65, avg:  176.83, max:  181.64, mdev:    2.91, cnt:  20
                   Lua53: top:  180.20, min:  177.79, avg:  185.01, max:  199.41, mdev:    6.07, cnt:  20
                    Perl: top:  180.22, min:  177.30, avg:  182.21, max:  186.09, mdev:    2.44, cnt:  20
              FreePascal: top:  180.85, min:  179.35, avg:  184.23, max:  197.83, mdev:    4.84, cnt:  20
                 Python3: top:  181.72, min:  178.47, avg:  184.09, max:  189.67, mdev:    2.99, cnt:  20
                    Ruby: top:  199.82, min:  197.16, avg:  203.62, max:  218.32, mdev:    4.92, cnt:  20
                 Chicken: top:  234.69, min:  232.11, avg:  239.61, max:  248.39, mdev:    5.63, cnt:  20
             PyPy3_bytes: top:  238.55, min:  237.18, avg:  242.08, max:  253.68, mdev:    4.53, cnt:  20
                   Guile: top:  254.49, min:  249.14, avg:  260.40, max:  275.83, mdev:    7.12, cnt:  20
              ChezScheme: top:  265.63, min:  262.52, avg:  268.56, max:  278.53, mdev:    3.94, cnt:  20
                    Java: top:  291.35, min:  283.94, avg:  302.36, max:  324.82, mdev:   12.38, cnt:  20
                  NodeJS: top:  317.01, min:  314.61, avg:  321.04, max:  332.05, mdev:    4.71, cnt:  20
                    Dart: top:  329.39, min:  325.63, avg:  334.57, max:  351.19, mdev:    6.92, cnt:  20
           Ruby_rubinius: top:  359.76, min:  357.74, avg:  363.13, max:  373.02, mdev:    4.45, cnt:  20
          CommonLisp_opt: top:  360.57, min:  358.41, avg:  365.15, max:  378.44, mdev:    5.76, cnt:  20
                     Tcl: top:  367.38, min:  363.28, avg:  372.89, max:  388.57, mdev:    6.65, cnt:  20
          CommonLisp_old: top:  376.27, min:  371.99, avg:  379.66, max:  390.55, mdev:    4.33, cnt:  20
                   PyPy3: top:  384.12, min:  376.60, avg:  390.16, max:  401.39, mdev:    7.32, cnt:  20
            CoffeeScript: top:  414.40, min:  393.13, avg:  432.25, max:  466.42, mdev:   20.64, cnt:  20
   CoffeeScript_parallel: top:  451.12, min:  425.11, avg:  464.92, max:  491.52, mdev:   17.05, cnt:  20
            NodeJS_async: top:  454.78, min:  437.13, avg:  465.18, max:  489.06, mdev:   13.02, cnt:  20
         Racket_compiled: top:  510.97, min:  505.22, avg:  516.20, max:  527.69, mdev:    6.23, cnt:  20
                  Racket: top:  520.70, min:  515.11, avg:  525.28, max:  533.79, mdev:    5.87, cnt:  20
         NodeJS_parallel: top:  673.38, min:  664.38, avg:  687.60, max:  724.04, mdev:   16.32, cnt:  20
                   Scala: top:  719.27, min:  698.23, avg:  740.32, max:  815.95, mdev:   27.27, cnt:  20
           Bash_parallel: top:  769.14, min:  751.56, avg:  775.91, max:  791.40, mdev:    8.82, cnt:  20
                 Haskell: top: 1036.33, min: 1013.27, avg: 1048.70, max: 1090.21, mdev: 4186.25, cnt:  20
                  Elixir: top: 1097.32, min: 1075.24, avg: 1113.36, max: 1144.80, mdev: 4186.26, cnt:  20
                       R: top: 1141.37, min: 1120.69, avg: 1156.42, max: 1177.79, mdev: 4186.26, cnt:  20
                    Bash: top: 1368.00, min: 1323.22, avg: 1479.66, max: 1994.19, mdev: 4077.71, cnt:  20
              POSIX_dash: top: 1841.09, min: 1833.25, avg: 1851.09, max: 1881.68, mdev: 3897.64, cnt:  17
               POSIX_zsh: top: 2124.79, min: 2110.81, avg: 2134.32, max: 2156.40, mdev: 3841.56, cnt:  15
              POSIX_bash: top: 2200.64, min: 2195.09, avg: 2206.75, max: 2221.41, mdev: 3807.09, cnt:  14
                  CSharp: FAILED with entity not found
                   Julia: FAILED with entity not found

对比旧结果,可以看到有一些比较大的变化:

Rust 快了不少,并行版一跃成为最快的实现。C++98 OpenMP 版紧随其后。Rust 单线程版也上升了四名,与 C、C++ 版本接近,并超越了所有的 D 实现。Go 并行版也提升了不少,位居第三,但它花费的时间比前两名所花费时间的总和还要多……并且结果也不是很稳定(标准差比前二十名都要大不少)。

Nim 慢了不少,可能是因为没字符串分割函数可用,我改用了 pegs。这东西很慢的样子,也许正则还会快一点……C 也落后了一些,但是与 C++ 版本的差距不大。Haskell 大概是因为改用动态链接的原因,慢了少许。

PyPy 快了很多,竟然超越了 LuaJIT。Erlang、Guile、Rubinius 也都大幅上升,而 NodeJS 不知道怎么了,全面落后于 Python、Ruby、Lua。PHP 更新到 7 之后依旧非常非常快。

完整的排名变化可以看这里

Category: 编程 | Tags: go 编程语言 Rust

| Theme: Aeros 2.0 by TheBuckmaker.com